mod config;
mod prompt;
mod llm;
mod output;
mod utils;
use clap::{Parser, ArgAction};
use config::load_config;
use prompt::{load_prompt_config, find_command};
use llm::process_with_llm;
use output::render_output;
use utils::copy_to_clipboard;
#[derive(Parser)]
#[command(name = "xa")]
#[command(about = "Execute Anything via LLM - A CLI tool for arbitrary text processing using LLMs", long_about = None)]
struct Cli {
#[arg(short = 's', long = "set", value_name = "CONFIG_TYPE", conflicts_with_all = &["list", "add", "rm"])]
set: Option<String>,
#[arg(short = 'l', long = "ls", action = ArgAction::SetTrue, conflicts_with_all = &["set", "add", "rm"])]
list: bool,
#[arg(short = 'a', long = "add", action = ArgAction::SetTrue, conflicts_with_all = &["set", "list", "rm"])]
add: bool,
#[arg(short = 'r', long = "rm", value_name = "COMMAND_NAME", conflicts_with_all = &["set", "list", "add"])]
rm: Option<String>,
#[arg(long = "no-stream", action = ArgAction::SetTrue)]
no_stream: bool,
command: Option<String>,
input: Option<String>,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
if let Some(config_type) = cli.set {
if config_type == "openai" {
config::configure_openai().await?;
return Ok(());
}
}
if cli.list {
prompt::list_commands().await?;
return Ok(());
}
if cli.add {
prompt::add_command().await?;
return Ok(());
}
if let Some(command_to_remove) = cli.rm {
prompt::remove_command(&command_to_remove).await?;
return Ok(());
}
if let Some(command) = cli.command {
if command == "ask" {
if cli.no_stream {
if let Some(input) = cli.input {
process_command(command, input, false).await?;
} else {
eprintln!("Error: No input provided for command '{}'", command);
std::process::exit(1);
}
} else {
start_interactive_mode().await?;
}
} else {
if let Some(input) = cli.input {
process_command(command, input, !cli.no_stream).await?;
} else {
eprintln!("Error: No input provided for command '{}'", command);
std::process::exit(1);
}
}
} else if cli.input.is_some() {
eprintln!("Error: No command provided");
std::process::exit(1);
} else {
println!("{}", get_help_text());
}
Ok(())
}
async fn process_command(command: String, input: String, stream: bool) -> Result<(), Box<dyn std::error::Error>> {
let config = load_config().await?;
if config.api_key.is_empty() {
eprintln!("Error: API key not configured. Please run 'xa -set openai' first.");
std::process::exit(1);
}
let prompt_config = load_prompt_config().await?;
let matched_command = find_command(&command, &prompt_config.prompts);
match matched_command {
Some(cmd) => {
let prompt_entry = &prompt_config.prompts[&cmd];
let filled_prompt = prompt_entry.template.replace("{input}", &input);
let result = process_with_llm(&config, &filled_prompt, stream).await?;
if let Err(e) = copy_to_clipboard(&result) {
eprintln!("Warning: Could not copy to clipboard: {}", e);
}
render_output(&result, true);
Ok(())
}
None => {
eprintln!("Error: Command '{}' not found. Use 'xa -ls' to see available commands.", command);
std::process::exit(1);
}
}
}
use std::io::{self, Write};
use termimad::{MadSkin, ansi};
async fn start_interactive_mode() -> Result<(), Box<dyn std::error::Error>> {
let config = load_config().await?;
if config.api_key.is_empty() {
eprintln!("Error: API key not configured. Please run 'xa -set openai' first.");
std::process::exit(1);
}
let mut skin = MadSkin::default();
skin.set_headers_fg(ansi(35)); skin.bold.set_fg(ansi(33));
skin.print_text("## Welcome to xa Interactive Mode\n\n");
println!("{}", "\x1b[90mType your message and press Enter. Type 'exit', 'quit', or 'bye' to end, or press Ctrl+C to exit.\x1b[0m");
println!("{}", "\x1b[90mUse 'clear' to clear conversation history, 'history' to view recent exchanges.\x1b[0m");
println!();
let mut conversation_history = Vec::new();
loop {
print!("\x1b[36m>\x1b[0m "); io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
if input.is_empty() {
continue;
}
match input.to_lowercase().as_str() {
"exit" | "quit" | "bye" => {
println!("{}", "\x1b[90mGoodbye! Thanks for using xa.\x1b[0m");
break;
}
"clear" => {
conversation_history.clear();
println!("{}", "\x1b[90mConversation history cleared.\x1b[0m");
continue;
}
"history" => {
if conversation_history.is_empty() {
println!("{}", "\x1b[90mNo conversation history yet.\x1b[0m");
} else {
println!("{}", "\x1b[90mRecent conversation history:\x1b[0m");
for (i, (user_msg, ai_resp)) in conversation_history.iter().enumerate() {
println!("\x1b[90m[{}]\x1b[0m \x1b[33mYou:\x1b[0m {}", i + 1, user_msg);
println!("\x1b[90m \x1b[32mAI:\x1b[0m {}", ai_resp);
println!();
}
}
continue;
}
_ => {}
}
conversation_history.push((input.to_string(), String::new()));
let mut full_prompt = String::new();
full_prompt.push_str("You are a helpful assistant called xa, execute anything by your side.\n\n");
if !conversation_history.is_empty() {
full_prompt.push_str("Previous conversation:\n");
for (user_msg, ai_resp) in &conversation_history[..conversation_history.len()-1] {
full_prompt.push_str(&format!("User: {}\n", user_msg));
if !ai_resp.is_empty() {
full_prompt.push_str(&format!("Assistant: {}\n", ai_resp));
}
}
full_prompt.push_str("\n");
}
full_prompt.push_str(&format!("Current message: {}", input));
let result = process_with_llm(&config, &full_prompt, true).await?;
if let Err(e) = copy_to_clipboard(&result) {
eprintln!("Warning: Could not copy to clipboard: {}", e);
}
if let Some(last) = conversation_history.last_mut() {
last.1 = result.clone();
}
println!(); }
Ok(())
}
fn get_help_text() -> String {
r#"xa - Execute Anything via LLM
USAGE:
xa [OPTIONS] [COMMAND] [INPUT]
OPTIONS:
-s, --set <CONFIG_TYPE> Configure API settings (e.g., xa -set openai)
-l, --ls List all available commands
-a, --add Add a new command/prompt
-r, --rm <COMMAND_NAME> Remove a command/prompt
--no-stream Disable streaming mode
EXAMPLES:
xa --set openai # Configure OpenAI-compatible API
xa --ls # List all commands
xa --add # Add a new command
xa --rm summarize # Remove the 'summarize' command
xa translate "Hello" # Translate text
xa trans "Hello" # Translate using fuzzy matching
xa polish "This is a draft text" --no-stream # Polish text without streaming
For more information, visit the project repository."#.to_string()
}