use serde::Deserialize;
use std::io::ErrorKind;
use std::path::Path;
use crate::paths::{agent_toml_path, agents_dir};
#[path = "claude_code/setup.rs"]
mod claude_code;
#[path = "codex/setup.rs"]
mod codex;
#[path = "hermes/setup.rs"]
mod hermes;
#[path = "pi/setup.rs"]
mod pi;
pub(crate) const DEFAULT_INSTRUCTIONS_FILE: &str = "CLAUDE.md";
fn default_instructions_file() -> String {
DEFAULT_INSTRUCTIONS_FILE.to_string()
}
#[derive(Debug, Clone, Deserialize)]
pub struct AgentCommand {
pub command: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default = "default_instructions_file")]
pub instructions_file: String,
#[serde(default)]
pub setup: Option<String>,
}
#[derive(Debug)]
pub enum AgentLoadError {
Missing,
Unreadable(String),
Parse(String),
}
impl std::fmt::Display for AgentLoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Missing => write!(f, "agent config not found"),
Self::Unreadable(err) => write!(f, "unreadable agent config: {err}"),
Self::Parse(err) => write!(f, "malformed agent TOML: {err}"),
}
}
}
pub fn load_agent_command(name: &str) -> Result<AgentCommand, AgentLoadError> {
let text = std::fs::read_to_string(agent_toml_path(name)).map_err(|err| {
if err.kind() == ErrorKind::NotFound {
AgentLoadError::Missing
} else {
AgentLoadError::Unreadable(err.to_string())
}
})?;
toml::from_str(&text).map_err(|err| AgentLoadError::Parse(err.to_string()))
}
const DEFAULT_AGENT_CONFIGS: &[(&str, &str)] = &[
(claude_code::NAME, claude_code::CONFIG),
(codex::NAME, codex::CONFIG),
(hermes::NAME, hermes::CONFIG),
(pi::NAME, pi::CONFIG),
];
fn builtin_agent_names() -> Vec<String> {
DEFAULT_AGENT_CONFIGS
.iter()
.map(|(n, _)| n.to_string())
.collect()
}
pub fn available_agents() -> Vec<String> {
available_agents_in(&agents_dir())
}
pub(crate) fn available_agents_in(dir: &Path) -> Vec<String> {
let Ok(entries) = std::fs::read_dir(dir) else {
return builtin_agent_names();
};
let mut names: Vec<String> = entries
.filter_map(Result::ok)
.filter_map(|entry| {
let path = entry.path();
if path.extension()? != "toml" {
return None;
}
path.file_stem()
.and_then(|stem| stem.to_str())
.map(str::to_string)
})
.collect();
if names.is_empty() {
return builtin_agent_names();
}
names.sort();
names
}
const MANAGED_HEADER_PREFIX: &str = "# moadim:managed ";
fn fingerprint(data: &str) -> String {
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0100_0000_01b3;
let mut hash = FNV_OFFSET;
for byte in data.as_bytes() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
format!("{hash:016x}")
}
fn render_managed(contents: &str) -> String {
format!(
"{MANAGED_HEADER_PREFIX}{}\n{contents}",
fingerprint(contents)
)
}
fn parse_managed(text: &str) -> Option<(&str, &str)> {
let rest = text.strip_prefix(MANAGED_HEADER_PREFIX)?;
rest.split_once('\n')
}
pub fn ensure_default_agents() {
ensure_default_agents_in(&agents_dir());
}
pub(crate) fn ensure_default_agents_in(dir: &Path) {
if let Err(err) = crate::utils::fs_perms::create_private_dir_all(dir) {
log::warn!(
"ensure_default_agents: failed to create {}: {err}",
dir.display()
);
return;
}
for (name, contents) in DEFAULT_AGENT_CONFIGS {
let path = dir.join(format!("{name}.toml"));
match std::fs::read_to_string(&path) {
Err(err) if err.kind() == ErrorKind::NotFound => {
if let Err(err) = std::fs::write(&path, render_managed(contents)) {
log::warn!(
"ensure_default_agents: failed to write {}: {err}",
path.display()
);
}
}
Err(err) => {
log::warn!(
"ensure_default_agents: failed to read {}: {err}",
path.display()
);
}
Ok(existing) => {
let Some((recorded_fingerprint, body)) = parse_managed(&existing) else {
continue;
};
if body == *contents {
continue; }
if fingerprint(body) != recorded_fingerprint {
continue;
}
if let Err(err) = std::fs::write(&path, render_managed(contents)) {
log::warn!(
"ensure_default_agents: failed to rewrite stale default {}: {err}",
path.display()
);
} else {
log::info!(
"ensure_default_agents: upgraded stale default {} to the current built-in",
path.display()
);
}
}
}
}
}
#[cfg(test)]
#[path = "agents_tests.rs"]
mod agents_tests;