pub mod claude_code;
pub mod codex;
pub mod copilot;
pub mod gemini;
pub mod opencode;
pub use claude_code::ClaudeCodeAdapter;
pub use codex::CodexCliAdapter;
pub use copilot::CopilotCliAdapter;
pub use gemini::GeminiCliAdapter;
pub use opencode::OpenCodeAdapter;
use std::path::{Path, PathBuf};
use crate::config::Config;
use crate::memory::Summary;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct HostCaps(pub u8);
impl HostCaps {
pub const NONE: Self = Self(0);
pub const BASH_WRAP: Self = Self(0b0001);
pub const SESSION_MEM: Self = Self(0b0010);
pub const BUDGET_HARD: Self = Self(0b0100);
pub const BUDGET_SOFT: Self = Self(0b1000);
pub const fn contains(self, other: Self) -> bool {
(self.0 & other.0) == other.0
}
}
impl std::ops::BitOr for HostCaps {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
Self(self.0 | rhs.0)
}
}
pub trait HostAdapter {
fn name(&self) -> &'static str;
fn is_installed(&self) -> bool;
fn data_dir(&self) -> PathBuf;
fn capabilities(&self) -> HostCaps;
fn install(&self, bin_path: &Path) -> std::io::Result<()>;
fn uninstall(&self) -> std::io::Result<()>;
fn inject_memory(&self, cfg: &Config, summaries: &[Summary]) -> std::io::Result<()>;
}
pub fn all_hosts() -> Vec<Box<dyn HostAdapter>> {
vec![
Box::new(ClaudeCodeAdapter),
Box::new(CopilotCliAdapter),
Box::new(OpenCodeAdapter),
Box::new(GeminiCliAdapter),
Box::new(CodexCliAdapter),
]
}
pub fn find(slug: &str) -> Option<Box<dyn HostAdapter>> {
all_hosts().into_iter().find(|h| h.name() == slug)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bash_wrap_flag_round_trips() {
let c = HostCaps::BASH_WRAP | HostCaps::SESSION_MEM;
assert!(c.contains(HostCaps::BASH_WRAP));
assert!(c.contains(HostCaps::SESSION_MEM));
assert!(!c.contains(HostCaps::BUDGET_HARD));
}
#[test]
fn find_returns_claude_code() {
let a = find("claude-code").expect("adapter");
assert_eq!(a.name(), "claude-code");
}
#[test]
fn find_returns_none_for_unknown() {
assert!(find("nonexistent").is_none());
}
}