1pub mod amazonq;
12pub mod claude;
13pub mod codex;
14pub mod copilot;
15pub mod gemini;
16pub mod opencode;
17
18use std::fmt;
19use std::str::FromStr;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
24pub enum AgentKind {
25 Claude,
26 Gemini,
27 Codex,
28 #[cfg_attr(feature = "clap", value(name = "amazonq"))]
29 AmazonQ,
30 #[cfg_attr(feature = "clap", value(name = "opencode"))]
31 OpenCode,
32 Copilot,
33}
34
35impl fmt::Display for AgentKind {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 match self {
38 AgentKind::Claude => write!(f, "claude"),
39 AgentKind::Gemini => write!(f, "gemini"),
40 AgentKind::Codex => write!(f, "codex"),
41 AgentKind::AmazonQ => write!(f, "amazonq"),
42 AgentKind::OpenCode => write!(f, "opencode"),
43 AgentKind::Copilot => write!(f, "copilot"),
44 }
45 }
46}
47
48impl FromStr for AgentKind {
49 type Err = String;
50
51 fn from_str(s: &str) -> Result<Self, Self::Err> {
52 match s.to_lowercase().as_str() {
53 "claude" => Ok(AgentKind::Claude),
54 "gemini" => Ok(AgentKind::Gemini),
55 "codex" => Ok(AgentKind::Codex),
56 "amazonq" | "amazon-q" | "amazon_q" => Ok(AgentKind::AmazonQ),
57 "opencode" | "open-code" => Ok(AgentKind::OpenCode),
58 "copilot" => Ok(AgentKind::Copilot),
59 _ => Err(format!("unknown agent: {s}")),
60 }
61 }
62}
63
64struct ModeAlias {
73 canonical: &'static str,
74 agent_names: &'static [(AgentKind, &'static str)],
75}
76
77const MODE_ALIASES: &[ModeAlias] = &[
78 ModeAlias {
79 canonical: "default",
80 agent_names: &[(AgentKind::Claude, "default")],
81 },
82 ModeAlias {
83 canonical: "plan",
84 agent_names: &[(AgentKind::Claude, "plan")],
85 },
86 ModeAlias {
87 canonical: "edit",
88 agent_names: &[(AgentKind::Claude, "edit")],
89 },
90 ModeAlias {
91 canonical: "unrestricted",
92 agent_names: &[(AgentKind::Claude, "dangerously_skip_permissions")],
93 },
94];
95
96pub fn resolve_permission_mode<'a>(agent: AgentKind, native_mode: &'a str) -> &'a str {
100 let lower = native_mode.to_lowercase();
101 for alias in MODE_ALIASES {
102 for &(a, name) in alias.agent_names {
103 if a == agent && name.to_lowercase() == lower {
104 return alias.canonical;
105 }
106 }
107 }
108 native_mode
109}
110
111struct ToolAlias {
120 canonical: &'static str,
122 internal: &'static str,
124 agent_names: &'static [(AgentKind, &'static str)],
126}
127
128const TOOL_ALIASES: &[ToolAlias] = &[
133 ToolAlias {
134 canonical: "shell",
135 internal: "Bash",
136 agent_names: &[
137 (AgentKind::Claude, "Bash"),
138 (AgentKind::Gemini, "run_shell_command"),
139 (AgentKind::Codex, "shell"),
140 (AgentKind::AmazonQ, "execute_bash"),
141 (AgentKind::OpenCode, "bash"),
142 (AgentKind::Copilot, "bash"),
143 ],
144 },
145 ToolAlias {
146 canonical: "read",
147 internal: "Read",
148 agent_names: &[
149 (AgentKind::Claude, "Read"),
150 (AgentKind::Gemini, "read_file"),
151 (AgentKind::AmazonQ, "fs_read"),
152 (AgentKind::OpenCode, "read"),
153 (AgentKind::Copilot, "view"),
154 ],
155 },
156 ToolAlias {
157 canonical: "write",
158 internal: "Write",
159 agent_names: &[
160 (AgentKind::Claude, "Write"),
161 (AgentKind::Gemini, "write_file"),
162 (AgentKind::AmazonQ, "fs_write"),
163 (AgentKind::OpenCode, "write"),
164 ],
165 },
166 ToolAlias {
167 canonical: "edit",
168 internal: "Edit",
169 agent_names: &[
170 (AgentKind::Claude, "Edit"),
171 (AgentKind::Gemini, "replace"),
172 (AgentKind::OpenCode, "edit"),
173 (AgentKind::Copilot, "edit"),
174 ],
175 },
176 ToolAlias {
177 canonical: "glob",
178 internal: "Glob",
179 agent_names: &[
180 (AgentKind::Claude, "Glob"),
181 (AgentKind::Gemini, "glob"),
182 (AgentKind::OpenCode, "glob"),
183 ],
184 },
185 ToolAlias {
186 canonical: "grep",
187 internal: "Grep",
188 agent_names: &[
189 (AgentKind::Claude, "Grep"),
190 (AgentKind::Gemini, "grep_search"),
191 (AgentKind::OpenCode, "grep"),
192 ],
193 },
194 ToolAlias {
195 canonical: "web_fetch",
196 internal: "WebFetch",
197 agent_names: &[
198 (AgentKind::Claude, "WebFetch"),
199 (AgentKind::Gemini, "web_fetch"),
200 (AgentKind::OpenCode, "webfetch"),
201 ],
202 },
203 ToolAlias {
204 canonical: "web_search",
205 internal: "WebSearch",
206 agent_names: &[
207 (AgentKind::Claude, "WebSearch"),
208 (AgentKind::Gemini, "google_web_search"),
209 (AgentKind::Codex, "web_search"),
210 (AgentKind::OpenCode, "websearch"),
211 ],
212 },
213];
214
215pub fn resolve_tool_name(agent: AgentKind, native_name: &str) -> &str {
220 let lower = native_name.to_lowercase();
221 for alias in TOOL_ALIASES {
222 for &(a, name) in alias.agent_names {
223 if a == agent && name.to_lowercase() == lower {
224 return alias.internal;
225 }
226 }
227 }
228 native_name
229}
230
231pub fn canonical_to_internal(clash_name: &str) -> Option<&'static str> {
235 let lower = clash_name.to_lowercase();
236 TOOL_ALIASES
237 .iter()
238 .find(|a| a.canonical.to_lowercase() == lower)
239 .map(|a| a.internal)
240}
241
242pub fn resolve_any_to_internal(name: &str) -> Option<&'static str> {
248 let lower = name.to_lowercase();
249 for alias in TOOL_ALIASES {
250 if alias.canonical.to_lowercase() == lower {
251 return Some(alias.internal);
252 }
253 if alias.internal.to_lowercase() == lower {
254 return Some(alias.internal);
255 }
256 for &(_, agent_name) in alias.agent_names {
257 if agent_name.to_lowercase() == lower {
258 return Some(alias.internal);
259 }
260 }
261 }
262 None
263}
264
265pub fn internal_to_canonical(internal_name: &str) -> Option<&'static str> {
269 let lower = internal_name.to_lowercase();
270 TOOL_ALIASES
271 .iter()
272 .find(|a| a.internal.to_lowercase() == lower)
273 .map(|a| a.canonical)
274}
275
276pub fn display_name(internal_name: &str) -> &str {
281 internal_to_canonical(internal_name).unwrap_or(internal_name)
282}
283
284pub fn internal_to_agent(agent: AgentKind, internal_name: &str) -> Option<&'static str> {
288 let lower = internal_name.to_lowercase();
289 for alias in TOOL_ALIASES {
290 if alias.internal.to_lowercase() == lower {
291 for &(a, name) in alias.agent_names {
292 if a == agent {
293 return Some(name);
294 }
295 }
296 }
297 }
298 None
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[test]
306 fn resolve_claude_bash() {
307 assert_eq!(resolve_tool_name(AgentKind::Claude, "Bash"), "Bash");
308 }
309
310 #[test]
311 fn resolve_gemini_shell() {
312 assert_eq!(
313 resolve_tool_name(AgentKind::Gemini, "run_shell_command"),
314 "Bash"
315 );
316 }
317
318 #[test]
319 fn resolve_codex_shell() {
320 assert_eq!(resolve_tool_name(AgentKind::Codex, "shell"), "Bash");
321 }
322
323 #[test]
324 fn resolve_amazonq_bash() {
325 assert_eq!(
326 resolve_tool_name(AgentKind::AmazonQ, "execute_bash"),
327 "Bash"
328 );
329 }
330
331 #[test]
332 fn resolve_case_insensitive() {
333 assert_eq!(resolve_tool_name(AgentKind::Claude, "bash"), "Bash");
334 assert_eq!(resolve_tool_name(AgentKind::Claude, "BASH"), "Bash");
335 assert_eq!(
336 resolve_tool_name(AgentKind::Gemini, "RUN_SHELL_COMMAND"),
337 "Bash"
338 );
339 }
340
341 #[test]
342 fn resolve_unknown_passthrough() {
343 assert_eq!(
344 resolve_tool_name(AgentKind::Claude, "SomeCustomTool"),
345 "SomeCustomTool"
346 );
347 }
348
349 #[test]
350 fn canonical_to_internal_works() {
351 assert_eq!(canonical_to_internal("shell"), Some("Bash"));
352 assert_eq!(canonical_to_internal("read"), Some("Read"));
353 assert_eq!(canonical_to_internal("SHELL"), Some("Bash"));
354 assert_eq!(canonical_to_internal("unknown"), None);
355 }
356
357 #[test]
358 fn internal_to_canonical_works() {
359 assert_eq!(internal_to_canonical("Bash"), Some("shell"));
360 assert_eq!(internal_to_canonical("Read"), Some("read"));
361 assert_eq!(internal_to_canonical("UnknownTool"), None);
362 }
363
364 #[test]
365 fn internal_to_agent_works() {
366 assert_eq!(
367 internal_to_agent(AgentKind::Gemini, "Bash"),
368 Some("run_shell_command")
369 );
370 assert_eq!(
371 internal_to_agent(AgentKind::AmazonQ, "Read"),
372 Some("fs_read")
373 );
374 assert_eq!(internal_to_agent(AgentKind::Codex, "Glob"), None);
375 }
376
377 #[test]
378 fn resolve_any_canonical() {
379 assert_eq!(resolve_any_to_internal("shell"), Some("Bash"));
380 assert_eq!(resolve_any_to_internal("read"), Some("Read"));
381 }
382
383 #[test]
384 fn resolve_any_internal() {
385 assert_eq!(resolve_any_to_internal("Bash"), Some("Bash"));
386 assert_eq!(resolve_any_to_internal("bash"), Some("Bash"));
387 assert_eq!(resolve_any_to_internal("BASH"), Some("Bash"));
388 }
389
390 #[test]
391 fn resolve_any_agent_native() {
392 assert_eq!(resolve_any_to_internal("run_shell_command"), Some("Bash"));
393 assert_eq!(resolve_any_to_internal("execute_bash"), Some("Bash"));
394 assert_eq!(resolve_any_to_internal("fs_read"), Some("Read"));
395 }
396
397 #[test]
398 fn resolve_any_unknown() {
399 assert_eq!(resolve_any_to_internal("CustomTool"), None);
400 }
401
402 #[test]
403 fn resolve_mode_claude_default() {
404 assert_eq!(
405 resolve_permission_mode(AgentKind::Claude, "default"),
406 "default"
407 );
408 }
409
410 #[test]
411 fn resolve_mode_claude_plan() {
412 assert_eq!(resolve_permission_mode(AgentKind::Claude, "plan"), "plan");
413 }
414
415 #[test]
416 fn resolve_mode_claude_dangerously_skip() {
417 assert_eq!(
418 resolve_permission_mode(AgentKind::Claude, "dangerously_skip_permissions"),
419 "unrestricted"
420 );
421 }
422
423 #[test]
424 fn resolve_mode_case_insensitive() {
425 assert_eq!(
426 resolve_permission_mode(AgentKind::Claude, "DANGEROUSLY_SKIP_PERMISSIONS"),
427 "unrestricted"
428 );
429 }
430
431 #[test]
432 fn resolve_mode_unknown_passthrough() {
433 assert_eq!(
434 resolve_permission_mode(AgentKind::Claude, "custom_mode"),
435 "custom_mode"
436 );
437 }
438
439 #[test]
440 fn resolve_mode_other_agents_default() {
441 assert_eq!(
442 resolve_permission_mode(AgentKind::Gemini, "default"),
443 "default"
444 );
445 assert_eq!(
446 resolve_permission_mode(AgentKind::Codex, "default"),
447 "default"
448 );
449 }
450
451 #[test]
452 fn all_aliases_have_consistent_internal_names() {
453 let claude_names: Vec<&str> = TOOL_ALIASES
454 .iter()
455 .flat_map(|a| {
456 a.agent_names
457 .iter()
458 .filter(|(ak, _)| *ak == AgentKind::Claude)
459 })
460 .map(|(_, name)| *name)
461 .collect();
462 for alias in TOOL_ALIASES {
463 assert!(
464 claude_names.contains(&alias.internal),
465 "internal name '{}' for canonical '{}' is not a Claude tool name",
466 alias.internal,
467 alias.canonical
468 );
469 }
470 }
471}