use anyhow::{bail, Result};
use pushkin_core::edits::Replacement;
use serde_json::Value;
pub mod families;
mod shared;
use families::{
normalize_auggie, normalize_claude_family, normalize_codex, normalize_hermes,
normalize_opencode,
};
pub(crate) use families::claude_replacements;
pub const AGENTS: &[&str] = &["claude", "codex", "auggie", "hermes", "opencode"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Agent {
Claude,
Codex,
Auggie,
Hermes,
Opencode,
}
impl Agent {
pub const ALL: [Agent; 5] = [
Agent::Claude,
Agent::Codex,
Agent::Auggie,
Agent::Hermes,
Agent::Opencode,
];
pub fn parse(name: &str) -> Result<Self> {
match name {
"claude" => Ok(Agent::Claude),
"codex" => Ok(Agent::Codex),
"auggie" => Ok(Agent::Auggie),
"hermes" => Ok(Agent::Hermes),
"opencode" => Ok(Agent::Opencode),
unknown => {
let candidates = nearest_agents(unknown).join(", ");
bail!("unknown agent '{unknown}'; did you mean: {candidates}?")
}
}
}
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Agent::Claude => "claude",
Agent::Codex => "codex",
Agent::Auggie => "auggie",
Agent::Hermes => "hermes",
Agent::Opencode => "opencode",
}
}
}
#[derive(Debug, Clone)]
pub struct FileWrite {
pub path: String,
pub content: String,
pub edits: Vec<Replacement>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Intent {
Write,
ReadWhole,
ReadRange,
Shell,
MutateNoContent(&'static str),
Delete,
}
#[derive(Debug, Clone)]
pub struct ToolAction {
pub session: String,
pub files: Vec<FileWrite>,
pub is_stop: bool,
pub intent: Intent,
pub command: Option<String>,
}
pub type ParseOutcome = Result<ToolAction, Option<String>>;
pub fn normalize(agent: Agent, raw: &str) -> ParseOutcome {
let value: Value = serde_json::from_str(raw).map_err(|_| None)?;
match agent {
Agent::Claude => normalize_claude_family(&value),
Agent::Codex => normalize_codex(&value),
Agent::Auggie => normalize_auggie(&value),
Agent::Hermes => normalize_hermes(&value),
Agent::Opencode => normalize_opencode(&value),
}
}
fn nearest_agents(unknown: &str) -> Vec<&'static str> {
let mut scored: Vec<(usize, &'static str)> = AGENTS
.iter()
.map(|&agent| (levenshtein(unknown, agent), agent))
.collect();
scored.sort_unstable();
scored.truncate(2);
scored.into_iter().map(|(_, agent)| agent).collect()
}
fn levenshtein(a: &str, b: &str) -> usize {
let a_chars: Vec<char> = a.chars().collect();
let b_chars: Vec<char> = b.chars().collect();
let mut previous: Vec<usize> = (0..=b_chars.len()).collect();
let mut current = vec![0usize; b_chars.len() + 1];
for (i, &a_char) in a_chars.iter().enumerate() {
current[0] = i + 1;
for (j, &b_char) in b_chars.iter().enumerate() {
let substitution = usize::from(a_char != b_char);
current[j + 1] = (previous[j] + substitution)
.min(previous[j + 1] + 1)
.min(current[j] + 1);
}
std::mem::swap(&mut previous, &mut current);
}
previous[b_chars.len()]
}