use arrrg::CommandLine;
use arrrg_derive::CommandLine;
use claudius::{Anthropic, PromptTestConfig};
#[derive(Debug, Clone, PartialEq, Eq, Default)]
enum OutputFormat {
#[default]
Text,
Json,
Yaml,
}
impl std::fmt::Display for OutputFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OutputFormat::Text => write!(f, "text"),
OutputFormat::Json => write!(f, "json"),
OutputFormat::Yaml => write!(f, "yaml"),
}
}
}
impl std::str::FromStr for OutputFormat {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"text" => Ok(OutputFormat::Text),
"json" => Ok(OutputFormat::Json),
"yaml" | "yml" => Ok(OutputFormat::Yaml),
_ => Err(format!(
"Invalid output format: {}. Valid options: text, json, yaml",
s
)),
}
}
}
#[derive(CommandLine, Debug, Default, PartialEq, Eq)]
struct Args {
#[arrrg(optional, "Output format: text, json, yaml", "FORMAT")]
format: Option<String>,
#[arrrg(flag, "Test mode - run assertions and exit with status code")]
test: bool,
#[arrrg(flag, "Include timing and token usage information")]
verbose: bool,
#[arrrg(flag, "Show all content blocks and tool calls")]
show_details: bool,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let (args, files) = Args::from_command_line_relaxed("claudius-prompt [OPTIONS] <FILES>...");
if files.is_empty() {
eprintln!("Error: Must specify at least one prompt file or config file");
std::process::exit(1);
}
let client = Anthropic::new(None)?;
let output_format = if let Some(format_str) = args.format {
format_str
.parse()
.map_err(|e| format!("Invalid format: {}", e))?
} else {
OutputFormat::Text
};
let mut all_passed = true;
let mut failed_files = Vec::new();
for (i, file_path) in files.iter().enumerate() {
let test_config = if file_path.ends_with(".yaml") || file_path.ends_with(".yml") {
PromptTestConfig::from_file(file_path)?
} else {
let prompt_text = std::fs::read_to_string(file_path)?;
PromptTestConfig::new(prompt_text).with_name(file_path.clone())
};
let result = test_config.run(&client).await?;
if !result.assertions_passed {
all_passed = false;
failed_files.push((file_path.clone(), result.assertion_failures.len()));
}
match output_format {
OutputFormat::Text => {
if files.len() > 1 {
println!("=== {} ===", file_path);
}
if args.verbose {
if let Some(ref name) = result.config.name {
println!("Test: {}", name);
}
println!(
"Model: {}",
result.config.model.as_deref().unwrap_or("default")
);
println!("Duration: {:?}", result.duration);
println!("Input tokens: {}", result.input_tokens);
println!("Output tokens: {}", result.output_tokens);
if !result.assertion_failures.is_empty() {
println!("Assertion failures:");
for failure in &result.assertion_failures {
println!(" - {}", failure);
}
}
println!("---");
}
println!("{}", result.response);
if args.show_details {
if let Some(ref message) = result.message {
println!("\n--- Message Details ---");
println!("ID: {}", message.id);
println!("Model: {}", message.model);
if let Some(ref stop_reason) = message.stop_reason {
println!("Stop Reason: {:?}", stop_reason);
}
if let Some(ref stop_seq) = message.stop_sequence {
println!("Stop Sequence: {}", stop_seq);
}
println!("\n--- Content Blocks ---");
for (idx, block) in message.content.iter().enumerate() {
match block {
claudius::ContentBlock::Text(text_block) => {
println!("Block {}: Text", idx);
println!(" {}", text_block.text);
}
claudius::ContentBlock::ToolUse(tool_use_block) => {
println!("Block {}: Tool Use", idx);
println!(" Tool: {}", tool_use_block.name);
println!(" ID: {}", tool_use_block.id);
println!(
" Input: {}",
serde_json::to_string_pretty(&tool_use_block.input)
.unwrap_or_else(|_| "invalid JSON".to_string())
);
}
claudius::ContentBlock::ServerToolUse(server_tool_use_block) => {
println!("Block {}: Server Tool Use", idx);
println!(" Tool: {}", server_tool_use_block.name);
println!(" ID: {}", server_tool_use_block.id);
println!(
" Input: {}",
serde_json::to_string_pretty(&server_tool_use_block.input)
.unwrap_or_else(|_| "invalid JSON".to_string())
);
}
claudius::ContentBlock::Thinking(thinking_block) => {
println!("Block {}: Thinking", idx);
println!(" Signature: {}", thinking_block.signature);
println!(" Thinking: {}", thinking_block.thinking);
}
claudius::ContentBlock::RedactedThinking(redacted_thinking) => {
println!("Block {}: Redacted Thinking", idx);
println!(" Data: {}", redacted_thinking.data);
}
claudius::ContentBlock::Image(image_block) => {
println!("Block {}: Image", idx);
println!(" Source: {:?}", image_block.source);
}
claudius::ContentBlock::Document(document_block) => {
println!("Block {}: Document", idx);
println!(" Source: {:?}", document_block.source);
}
claudius::ContentBlock::ToolResult(tool_result_block) => {
println!("Block {}: Tool Result", idx);
println!(" Tool Use ID: {}", tool_result_block.tool_use_id);
println!(" Content: {:?}", tool_result_block.content);
}
claudius::ContentBlock::WebSearchToolResult(web_search_result) => {
println!("Block {}: Web Search Tool Result", idx);
println!(" Result: {:?}", web_search_result);
}
}
}
} else if !result.api_success {
println!("\n--- Error Details ---");
if let Some(ref err_msg) = result.error_message {
println!("Error: {}", err_msg);
}
}
}
if files.len() > 1 && i < files.len() - 1 {
println!();
}
}
OutputFormat::Json => {
let json = serde_json::to_string_pretty(&result)?;
println!("{}", json);
if i < files.len() - 1 {
println!();
}
}
OutputFormat::Yaml => {
let yaml = serde_yaml::to_string(&result)?;
print!("{}", yaml);
if i < files.len() - 1 {
println!("---");
}
}
}
}
if args.test {
if all_passed {
std::process::exit(0);
} else {
eprintln!(
"Tests failed: {}/{} files had assertion failures",
failed_files.len(),
files.len()
);
for (file_path, failure_count) in &failed_files {
eprintln!(" {}: {} failures", file_path, failure_count);
}
std::process::exit(1);
}
}
Ok(())
}