#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CliTool {
Codex,
Gemini,
Custom {
name: String,
binary: String,
},
}
impl std::fmt::Display for CliTool {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CliTool::Codex => write!(f, "Codex"),
CliTool::Gemini => write!(f, "Gemini"),
CliTool::Custom { name, .. } => write!(f, "{name}"),
}
}
}
#[derive(Debug, Clone)]
pub struct CliDelegation {
pub tool: CliTool,
pub model: Option<String>,
pub description: String,
pub usage_hint: Option<String>,
}
impl CliDelegation {
pub fn codex() -> Self {
Self {
tool: CliTool::Codex,
model: None,
description: "When you need AI-powered code generation or implementation, use Codex via Bash.".into(),
usage_hint: None,
}
}
pub fn gemini(model: &str) -> Self {
Self {
tool: CliTool::Gemini,
model: Some(model.to_string()),
description: "When you need AI-powered analysis or review, use Gemini via Bash.".into(),
usage_hint: None,
}
}
pub fn custom(name: &str, binary: &str) -> Self {
Self {
tool: CliTool::Custom {
name: name.to_string(),
binary: binary.to_string(),
},
model: None,
description: format!("When appropriate, use {name} via Bash."),
usage_hint: None,
}
}
pub fn with_description(mut self, desc: &str) -> Self {
self.description = desc.to_string();
self
}
pub fn with_usage_hint(mut self, hint: &str) -> Self {
self.usage_hint = Some(hint.to_string());
self
}
pub fn with_model(mut self, model: &str) -> Self {
self.model = Some(model.to_string());
self
}
pub fn to_prompt_instructions(&self) -> String {
let tool_name = &self.tool;
let model_suffix = self
.model
.as_ref()
.map(|m| format!(" ({m})"))
.unwrap_or_default();
let usage = self
.usage_hint
.clone()
.unwrap_or_else(|| self.default_usage_hint());
format!(
"## CLI Delegation: {tool_name}{model_suffix}\n\
{description}\n\
```bash\n\
{usage}\n\
```",
description = self.description,
)
}
fn default_usage_hint(&self) -> String {
match &self.tool {
CliTool::Codex => {
"codex -q \"YOUR_PROMPT\" --approval-mode full-auto".to_string()
}
CliTool::Gemini => {
let model_flag = self
.model
.as_ref()
.map(|m| format!("-m {m} "))
.unwrap_or_default();
format!(
"gemini {model_flag}-y <<'PROMPT'\nYour prompt here\nPROMPT"
)
}
CliTool::Custom { binary, .. } => {
format!("{binary} \"YOUR_PROMPT\"")
}
}
}
}
pub fn format_delegation_prompt(delegations: &[CliDelegation]) -> String {
if delegations.is_empty() {
return String::new();
}
let mut sections = Vec::with_capacity(delegations.len() + 1);
sections.push(
"# CLI Tool Delegations\n\n\
You have access to the following CLI tools via Bash. \
Use them when appropriate for your tasks."
.to_string(),
);
for d in delegations {
sections.push(d.to_prompt_instructions());
}
sections.join("\n\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn codex_default_prompt() {
let d = CliDelegation::codex();
let prompt = d.to_prompt_instructions();
assert!(prompt.contains("## CLI Delegation: Codex"));
assert!(prompt.contains("codex -q"));
assert!(prompt.contains("--approval-mode full-auto"));
assert!(prompt.contains("code generation"));
}
#[test]
fn gemini_with_model() {
let d = CliDelegation::gemini("gemini-2.5-pro");
let prompt = d.to_prompt_instructions();
assert!(prompt.contains("## CLI Delegation: Gemini (gemini-2.5-pro)"));
assert!(prompt.contains("-m gemini-2.5-pro"));
assert!(prompt.contains("gemini"));
assert!(prompt.contains("PROMPT"));
}
#[test]
fn gemini_custom_description() {
let d = CliDelegation::gemini("gemini-2.5-flash")
.with_description("Use for quick architectural reviews only.");
let prompt = d.to_prompt_instructions();
assert!(prompt.contains("quick architectural reviews"));
assert!(!prompt.contains("analysis or review")); }
#[test]
fn custom_tool() {
let d = CliDelegation::custom("Aider", "aider");
let prompt = d.to_prompt_instructions();
assert!(prompt.contains("## CLI Delegation: Aider"));
assert!(prompt.contains("aider \"YOUR_PROMPT\""));
}
#[test]
fn custom_tool_with_usage_hint() {
let d = CliDelegation::custom("Aider", "aider")
.with_usage_hint("aider --model gpt-4.1 --yes-always \"YOUR_PROMPT\"");
let prompt = d.to_prompt_instructions();
assert!(prompt.contains("--yes-always"));
assert!(!prompt.contains("aider \"YOUR_PROMPT\"")); }
#[test]
fn custom_tool_with_model() {
let d = CliDelegation::custom("MyTool", "mytool").with_model("v2");
let prompt = d.to_prompt_instructions();
assert!(prompt.contains("## CLI Delegation: MyTool (v2)"));
}
#[test]
fn format_delegation_prompt_empty() {
assert_eq!(format_delegation_prompt(&[]), "");
}
#[test]
fn format_delegation_prompt_single() {
let delegations = vec![CliDelegation::codex()];
let prompt = format_delegation_prompt(&delegations);
assert!(prompt.starts_with("# CLI Tool Delegations"));
assert!(prompt.contains("## CLI Delegation: Codex"));
}
#[test]
fn format_delegation_prompt_multiple() {
let delegations = vec![
CliDelegation::codex(),
CliDelegation::gemini("gemini-2.5-pro"),
CliDelegation::custom("Aider", "aider"),
];
let prompt = format_delegation_prompt(&delegations);
assert!(prompt.contains("# CLI Tool Delegations"));
assert!(prompt.contains("## CLI Delegation: Codex"));
assert!(prompt.contains("## CLI Delegation: Gemini (gemini-2.5-pro)"));
assert!(prompt.contains("## CLI Delegation: Aider"));
}
#[test]
fn cli_tool_display() {
assert_eq!(CliTool::Codex.to_string(), "Codex");
assert_eq!(CliTool::Gemini.to_string(), "Gemini");
assert_eq!(
CliTool::Custom {
name: "Aider".into(),
binary: "aider".into()
}
.to_string(),
"Aider"
);
}
#[test]
fn cli_delegation_clone_and_debug() {
let d = CliDelegation::codex();
let d2 = d.clone();
assert_eq!(d.tool, d2.tool);
let debug = format!("{d:?}");
assert!(debug.contains("CliDelegation"));
}
}