pub mod claude_code;
pub mod opencode;
pub mod pi_coding_agent;
#[derive(Clone, Copy, Debug, Eq, PartialEq, clap::ValueEnum)]
pub enum AgentName {
#[value(name = "claude-code")]
ClaudeCode,
#[value(name = "pi-coding-agent")]
PiCodingAgent,
#[value(name = "opencode")]
Opencode,
}
impl AgentName {
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::ClaudeCode => "claude-code",
Self::PiCodingAgent => "pi-coding-agent",
Self::Opencode => "opencode",
}
}
#[must_use]
pub fn agent(self) -> Box<dyn Agent> {
match self {
Self::ClaudeCode => Box::new(claude_code::ClaudeCodeAgent),
Self::PiCodingAgent => Box::new(pi_coding_agent::PiCodingAgent),
Self::Opencode => Box::new(opencode::OpencodeAgent),
}
}
}
pub trait Agent {
fn name(&self) -> &'static str;
fn extract_session_id(&self, stdin_json: &str) -> Option<String>;
fn extract_message(&self, _stdin_json: &str) -> Option<String> {
None
}
}
#[must_use]
pub fn by_name(name: &str) -> Option<Box<dyn Agent>> {
match name {
"claude-code" => Some(Box::new(claude_code::ClaudeCodeAgent)),
"opencode" => Some(Box::new(opencode::OpencodeAgent)),
"pi-coding-agent" => Some(Box::new(pi_coding_agent::PiCodingAgent)),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn by_name_resolves_claude_code() {
let agent = by_name("claude-code").expect("claude-code is a registered agent");
assert_eq!(agent.name(), "claude-code");
}
#[test]
fn by_name_returns_none_for_unknown() {
assert!(by_name("frobnicator").is_none());
}
#[test]
fn by_name_is_case_sensitive() {
assert!(by_name("Claude-Code").is_none());
}
#[test]
fn by_name_resolves_pi_coding_agent() {
let agent = by_name("pi-coding-agent").expect("pi-coding-agent is a registered agent");
assert_eq!(agent.name(), "pi-coding-agent");
}
#[test]
fn by_name_resolves_opencode() {
let agent = by_name("opencode").expect("opencode is a registered agent");
assert_eq!(agent.name(), "opencode");
}
#[test]
fn agent_name_returns_wire_string_for_each_variant() {
assert_eq!(AgentName::ClaudeCode.name(), "claude-code");
assert_eq!(AgentName::PiCodingAgent.name(), "pi-coding-agent");
assert_eq!(AgentName::Opencode.name(), "opencode");
}
#[test]
fn agent_name_dispatch_returns_matching_trait_object() {
assert_eq!(AgentName::ClaudeCode.agent().name(), "claude-code");
assert_eq!(AgentName::PiCodingAgent.agent().name(), "pi-coding-agent");
assert_eq!(AgentName::Opencode.agent().name(), "opencode");
}
#[test]
fn agent_name_parses_kebab_case_via_value_enum() {
use clap::ValueEnum;
assert_eq!(
AgentName::from_str("claude-code", true).unwrap(),
AgentName::ClaudeCode,
);
assert_eq!(
AgentName::from_str("pi-coding-agent", true).unwrap(),
AgentName::PiCodingAgent,
);
assert_eq!(
AgentName::from_str("opencode", true).unwrap(),
AgentName::Opencode,
);
assert!(AgentName::from_str("frobnicator", true).is_err());
}
#[test]
fn extract_message_default_returns_none() {
struct NoopAgent;
impl Agent for NoopAgent {
fn name(&self) -> &'static str { "noop" }
fn extract_session_id(&self, _: &str) -> Option<String> { None }
}
assert!(NoopAgent.extract_message(r#"{"message":"hi"}"#).is_none());
}
}