#![allow(dead_code)]
use anyhow::Result;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AgentType {
#[default]
Claude,
Gemini,
Codex,
OpenCode,
Amp,
Pi,
Hermes,
Symphony,
}
impl AgentType {
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"claude" | "claude-code" => Some(Self::Claude),
"gemini" | "gemini-cli" => Some(Self::Gemini),
"codex" | "openai-codex" => Some(Self::Codex),
"opencode" | "open-code" => Some(Self::OpenCode),
"amp" | "ampcode" => Some(Self::Amp),
"pi" | "pi-coding-agent" => Some(Self::Pi),
"hermes" | "hermes-agent" => Some(Self::Hermes),
"symphony" | "openai-symphony" => Some(Self::Symphony),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
Self::Claude => "Claude Code",
Self::Gemini => "Gemini CLI",
Self::Codex => "Codex",
Self::OpenCode => "OpenCode",
Self::Amp => "Amp",
Self::Pi => "Pi",
Self::Hermes => "Hermes Agent",
Self::Symphony => "Symphony",
}
}
pub fn command(&self) -> &'static str {
match self {
Self::Claude => "claude",
Self::Gemini => "gemini",
Self::Codex => "codex",
Self::OpenCode => "opencode",
Self::Amp => "amp",
Self::Pi => "pi",
Self::Hermes => "hermes",
Self::Symphony => "symphony",
}
}
}
#[derive(Debug, Clone, Default)]
#[allow(dead_code)]
pub struct AgentConfig {
pub agent_type: AgentType,
pub env_vars: HashMap<String, String>,
pub args: Vec<String>,
pub working_dir: Option<String>,
}
#[allow(dead_code)]
impl AgentConfig {
pub fn for_agent(agent_type: AgentType) -> Self {
Self {
agent_type,
..Default::default()
}
}
pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env_vars.insert(key.into(), value.into());
self
}
pub fn with_args(mut self, args: Vec<String>) -> Self {
self.args = args;
self
}
pub fn with_working_dir(mut self, dir: impl Into<String>) -> Self {
self.working_dir = Some(dir.into());
self
}
}
pub trait Agent {
fn agent_type(&self) -> AgentType;
fn launch_command(&self) -> Vec<String>;
fn env_vars(&self) -> &HashMap<String, String>;
fn api_key_env_var(&self) -> Option<&'static str>;
fn is_available(&self) -> bool;
fn install_instructions(&self) -> &'static str;
}
pub struct ClaudeAgent {
config: AgentConfig,
}
impl ClaudeAgent {
pub fn new(config: AgentConfig) -> Self {
Self { config }
}
}
impl Agent for ClaudeAgent {
fn agent_type(&self) -> AgentType {
AgentType::Claude
}
fn launch_command(&self) -> Vec<String> {
let mut cmd = vec!["claude".to_string()];
cmd.extend(self.config.args.clone());
cmd
}
fn env_vars(&self) -> &HashMap<String, String> {
&self.config.env_vars
}
fn api_key_env_var(&self) -> Option<&'static str> {
Some("ANTHROPIC_API_KEY")
}
fn is_available(&self) -> bool {
std::process::Command::new("claude")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn install_instructions(&self) -> &'static str {
"Install Claude Code: npm install -g @anthropic-ai/claude-code"
}
}
pub struct GeminiAgent {
config: AgentConfig,
}
impl GeminiAgent {
pub fn new(config: AgentConfig) -> Self {
Self { config }
}
}
impl Agent for GeminiAgent {
fn agent_type(&self) -> AgentType {
AgentType::Gemini
}
fn launch_command(&self) -> Vec<String> {
let mut cmd = vec!["gemini".to_string()];
cmd.extend(self.config.args.clone());
cmd
}
fn env_vars(&self) -> &HashMap<String, String> {
&self.config.env_vars
}
fn api_key_env_var(&self) -> Option<&'static str> {
Some("GOOGLE_API_KEY")
}
fn is_available(&self) -> bool {
std::process::Command::new("gemini")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn install_instructions(&self) -> &'static str {
"Install Gemini CLI: pip install google-generativeai"
}
}
pub struct CodexAgent {
config: AgentConfig,
}
impl CodexAgent {
pub fn new(config: AgentConfig) -> Self {
Self { config }
}
}
impl Agent for CodexAgent {
fn agent_type(&self) -> AgentType {
AgentType::Codex
}
fn launch_command(&self) -> Vec<String> {
let mut cmd = vec!["codex".to_string()];
cmd.extend(self.config.args.clone());
cmd
}
fn env_vars(&self) -> &HashMap<String, String> {
&self.config.env_vars
}
fn api_key_env_var(&self) -> Option<&'static str> {
Some("OPENAI_API_KEY")
}
fn is_available(&self) -> bool {
std::process::Command::new("codex")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn install_instructions(&self) -> &'static str {
"Install Codex CLI: npm install -g @openai/codex"
}
}
pub struct OpenCodeAgent {
config: AgentConfig,
}
impl OpenCodeAgent {
pub fn new(config: AgentConfig) -> Self {
Self { config }
}
}
impl Agent for OpenCodeAgent {
fn agent_type(&self) -> AgentType {
AgentType::OpenCode
}
fn launch_command(&self) -> Vec<String> {
let mut cmd = vec!["opencode".to_string()];
cmd.extend(self.config.args.clone());
cmd
}
fn env_vars(&self) -> &HashMap<String, String> {
&self.config.env_vars
}
fn api_key_env_var(&self) -> Option<&'static str> {
None
}
fn is_available(&self) -> bool {
std::process::Command::new("opencode")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn install_instructions(&self) -> &'static str {
"Install OpenCode: cargo install opencode"
}
}
pub struct AmpAgent {
config: AgentConfig,
}
impl AmpAgent {
pub fn new(config: AgentConfig) -> Self {
Self { config }
}
}
impl Agent for AmpAgent {
fn agent_type(&self) -> AgentType {
AgentType::Amp
}
fn launch_command(&self) -> Vec<String> {
let mut cmd = vec!["amp".to_string()];
cmd.extend(self.config.args.clone());
cmd
}
fn env_vars(&self) -> &HashMap<String, String> {
&self.config.env_vars
}
fn api_key_env_var(&self) -> Option<&'static str> {
Some("ANTHROPIC_API_KEY")
}
fn is_available(&self) -> bool {
std::process::Command::new("amp")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn install_instructions(&self) -> &'static str {
"Install Amp: npm install -g @sourcegraph/amp"
}
}
pub struct PiAgent {
config: AgentConfig,
}
impl PiAgent {
pub fn new(config: AgentConfig) -> Self {
Self { config }
}
}
impl Agent for PiAgent {
fn agent_type(&self) -> AgentType {
AgentType::Pi
}
fn launch_command(&self) -> Vec<String> {
let mut cmd = vec!["pi".to_string()];
cmd.extend(self.config.args.clone());
cmd
}
fn env_vars(&self) -> &HashMap<String, String> {
&self.config.env_vars
}
fn api_key_env_var(&self) -> Option<&'static str> {
None
}
fn is_available(&self) -> bool {
std::process::Command::new("pi")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn install_instructions(&self) -> &'static str {
"Install Pi: npm install -g @mariozechner/pi-coding-agent"
}
}
pub struct HermesAgent {
config: AgentConfig,
}
impl HermesAgent {
pub fn new(config: AgentConfig) -> Self {
Self { config }
}
}
impl Agent for HermesAgent {
fn agent_type(&self) -> AgentType {
AgentType::Hermes
}
fn launch_command(&self) -> Vec<String> {
let mut cmd = vec!["hermes".to_string()];
cmd.extend(self.config.args.clone());
cmd
}
fn env_vars(&self) -> &HashMap<String, String> {
&self.config.env_vars
}
fn api_key_env_var(&self) -> Option<&'static str> {
None
}
fn is_available(&self) -> bool {
std::process::Command::new("hermes")
.arg("--help")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn install_instructions(&self) -> &'static str {
"Install Hermes Agent: curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash"
}
}
pub struct SymphonyAgent {
config: AgentConfig,
}
impl SymphonyAgent {
pub fn new(config: AgentConfig) -> Self {
Self { config }
}
}
impl Agent for SymphonyAgent {
fn agent_type(&self) -> AgentType {
AgentType::Symphony
}
fn launch_command(&self) -> Vec<String> {
let mut cmd = vec!["symphony".to_string()];
cmd.extend(self.config.args.clone());
cmd
}
fn env_vars(&self) -> &HashMap<String, String> {
&self.config.env_vars
}
fn api_key_env_var(&self) -> Option<&'static str> {
Some("OPENAI_API_KEY")
}
fn is_available(&self) -> bool {
std::process::Command::new("symphony")
.arg("--help")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn install_instructions(&self) -> &'static str {
"Install Symphony: git clone https://github.com/openai/symphony && cd symphony/elixir && mix setup && mix build"
}
}
pub fn create_agent(agent_type: AgentType, config: Option<AgentConfig>) -> Box<dyn Agent> {
let config = config.unwrap_or_else(|| AgentConfig::for_agent(agent_type));
match agent_type {
AgentType::Claude => Box::new(ClaudeAgent::new(config)),
AgentType::Gemini => Box::new(GeminiAgent::new(config)),
AgentType::Codex => Box::new(CodexAgent::new(config)),
AgentType::OpenCode => Box::new(OpenCodeAgent::new(config)),
AgentType::Amp => Box::new(AmpAgent::new(config)),
AgentType::Pi => Box::new(PiAgent::new(config)),
AgentType::Hermes => Box::new(HermesAgent::new(config)),
AgentType::Symphony => Box::new(SymphonyAgent::new(config)),
}
}
pub fn create_agent_from_str(agent_name: &str) -> Result<Box<dyn Agent>> {
let agent_type = AgentType::from_str(agent_name)
.ok_or_else(|| anyhow::anyhow!("Unknown agent type: {}", agent_name))?;
Ok(create_agent(agent_type, None))
}
pub fn check_agent_availability(agent_type: AgentType) -> AgentStatus {
let agent = create_agent(agent_type, None);
let installed = agent.is_available();
let api_key_set = agent
.api_key_env_var()
.map(|var| std::env::var(var).is_ok())
.unwrap_or(true);
AgentStatus {
agent_type,
installed,
api_key_set,
install_instructions: agent.install_instructions().to_string(),
}
}
#[derive(Debug, Clone)]
pub struct AgentStatus {
pub agent_type: AgentType,
pub installed: bool,
pub api_key_set: bool,
pub install_instructions: String,
}
impl AgentStatus {
pub fn is_ready(&self) -> bool {
self.installed && self.api_key_set
}
pub fn print(&self) {
let status = if self.is_ready() {
"ready"
} else if self.installed {
"api key missing"
} else {
"not installed"
};
println!("{:<15} {}", self.agent_type.name(), status);
if !self.installed {
println!(" {}", self.install_instructions);
}
}
}
pub fn list_agents() -> Vec<AgentStatus> {
vec![
check_agent_availability(AgentType::Claude),
check_agent_availability(AgentType::Gemini),
check_agent_availability(AgentType::Codex),
check_agent_availability(AgentType::OpenCode),
check_agent_availability(AgentType::Amp),
check_agent_availability(AgentType::Pi),
check_agent_availability(AgentType::Hermes),
check_agent_availability(AgentType::Symphony),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_agent_type_from_str() {
assert_eq!(AgentType::from_str("claude"), Some(AgentType::Claude));
assert_eq!(AgentType::from_str("Claude"), Some(AgentType::Claude));
assert_eq!(AgentType::from_str("gemini"), Some(AgentType::Gemini));
assert_eq!(AgentType::from_str("codex"), Some(AgentType::Codex));
assert_eq!(AgentType::from_str("opencode"), Some(AgentType::OpenCode));
assert_eq!(AgentType::from_str("amp"), Some(AgentType::Amp));
assert_eq!(AgentType::from_str("ampcode"), Some(AgentType::Amp));
assert_eq!(AgentType::from_str("pi"), Some(AgentType::Pi));
assert_eq!(AgentType::from_str("pi-coding-agent"), Some(AgentType::Pi));
assert_eq!(AgentType::from_str("hermes"), Some(AgentType::Hermes));
assert_eq!(AgentType::from_str("hermes-agent"), Some(AgentType::Hermes));
assert_eq!(AgentType::from_str("symphony"), Some(AgentType::Symphony));
assert_eq!(
AgentType::from_str("openai-symphony"),
Some(AgentType::Symphony)
);
assert_eq!(AgentType::from_str("unknown"), None);
}
#[test]
fn test_agent_config() {
let config = AgentConfig::for_agent(AgentType::Claude)
.with_env("CUSTOM_VAR", "value")
.with_args(vec!["--flag".to_string()]);
assert_eq!(config.agent_type, AgentType::Claude);
assert_eq!(
config.env_vars.get("CUSTOM_VAR"),
Some(&"value".to_string())
);
assert_eq!(config.args, vec!["--flag".to_string()]);
}
#[test]
fn test_create_agent() {
let agent = create_agent(AgentType::Claude, None);
assert_eq!(agent.agent_type(), AgentType::Claude);
assert_eq!(agent.api_key_env_var(), Some("ANTHROPIC_API_KEY"));
}
}