mod config;
mod prompt;
mod llm;
mod output;
mod utils;
mod store;
use clap::{Parser, Subcommand};
use config::load_config;
use prompt::{load_prompt_config, find_command, process_template_with_args};
use llm::process_with_llm;
use output::render_output;
use utils::copy_to_clipboard;
use store::{add_secret_with_tag, search_secret};
#[derive(Parser)]
#[command(name = "xa")]
#[command(about = "Execute Anything via LLM - A CLI tool for arbitrary text processing using LLMs")]
#[command(after_help = "Short aliases: as (add-secret), se (search)\nExamples: xa as mysecret \"token\", xa se \"query\", xa ls prompts")]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
#[arg(long = "no-stream", global = true)]
no_stream: bool,
#[arg(long = "debug", global = true)]
debug: bool,
input: Option<String>,
#[arg(trailing_var_arg = true)]
args: Vec<String>,
}
#[derive(Subcommand)]
enum Commands {
#[command(short_flag = 's')]
Set {
config_type: String,
},
#[command(short_flag = 'l', alias = "list")]
Ls {
#[arg(value_name = "TYPE")]
list_type: Option<String>,
},
#[command(short_flag = 'a')]
Add,
#[command(short_flag = 'r')]
Rm {
command_name: String,
},
#[command(alias = "reset")]
ResetDefaults,
#[command(short_flag = 'A', visible_alias = "as")]
AddSecret {
secret: String,
note: String,
},
#[command(visible_alias = "se")]
Search {
query: String,
},
Ask,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
match cli.command {
Some(Commands::Set { config_type }) => {
if config_type == "openai" {
config::configure_openai().await?;
return Ok(());
} else {
eprintln!("Unknown configuration type: {}", config_type);
eprintln!("Available configuration types: openai");
std::process::exit(1);
}
}
Some(Commands::Ls { list_type }) => {
match list_type.as_deref() {
Some("prompts") => {
prompt::list_prompts().await?;
return Ok(());
}
Some("stores") => {
store::list_stores().await?;
return Ok(());
}
Some(other) => {
eprintln!("Unknown list type: {}", other);
eprintln!("Available list types: prompts, stores");
eprintln!("Usage: xa ls prompts or xa ls stores");
std::process::exit(1);
}
None => {
prompt::list_commands().await?;
return Ok(());
}
}
}
Some(Commands::Add) => {
prompt::add_command().await?;
return Ok(());
}
Some(Commands::Rm { command_name }) => {
prompt::remove_command(&command_name).await?;
return Ok(());
}
Some(Commands::ResetDefaults) => {
prompt::reset_default_prompts()?;
return Ok(());
}
Some(Commands::AddSecret { secret, note }) => {
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);
}
add_secret_with_tag(&config, &secret, ¬e).await?;
return Ok(());
}
Some(Commands::Search { query }) => {
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);
}
search_secret(&config, &query).await?;
return Ok(());
}
Some(Commands::Ask) => {
if cli.input.is_some() {
process_command_with_args(&cli, "ask").await?;
} else {
start_interactive_mode().await?;
}
return Ok(());
}
None => {
if cli.input.is_some() {
eprintln!("Error: No command provided");
std::process::exit(1);
} else {
println!("{}", get_help_text());
}
return Ok(());
}
}
}
async fn process_command_with_args(cli: &Cli, command_name: &str) -> Result<(), Box<dyn std::error::Error>> {
let input = cli.input.as_ref().unwrap();
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_name, &prompt_config.prompts);
match matched_command {
Some(cmd) => {
let prompt_entry = &prompt_config.prompts[&cmd];
let (processed_input, processed_args) = if cmd == "translate" {
if input.chars().all(|c| c.is_ascii_alphabetic()) && input.len() >= 2 && input.len() <= 3
&& !cli.args.is_empty() {
let text_to_translate = &cli.args[0];
(text_to_translate.clone(), vec![input.to_string()])
} else {
(input.to_string(), cli.args.clone())
}
} else {
(input.to_string(), cli.args.clone())
};
let filled_prompt = process_template_with_args(
&prompt_entry.template,
&processed_input,
&processed_args,
prompt_entry.args.as_ref()
);
if cli.debug {
eprintln!("[DEBUG] Debug mode is ON");
eprintln!("[DEBUG] Filled prompt:");
eprintln!("---");
eprintln!("{}", filled_prompt);
eprintln!("---");
eprintln!("[DEBUG] End of filled prompt\n");
}
let result = process_with_llm(&config, &filled_prompt, !cli.no_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_name);
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] [ARGS]...
COMMANDS:
set, -s <CONFIG_TYPE> Configure API settings (e.g., xa set openai)
ls, -l [TYPE] List commands (TYPE: prompts, stores, or omit for all)
add, -a Add a new command/prompt interactively
rm, -r <COMMAND_NAME> Remove a command/prompt
reset-defaults Reset to default prompts
add-secret, -A, as Add a secret: xa as <secret> <note>
search, se Search secrets: xa se <query>
ask Interactive conversation mode
OPTIONS:
--no-stream Disable streaming mode
--debug Enable debug mode to print filled prompt
-h, --help Print help
EXAMPLES:
xa set openai # Configure API
xa ls # List all commands
xa ls prompts # List prompt templates
xa ls stores # List stored secrets
xa as mysecret "gitcode token" # Add secret (short alias)
xa se "my token" # Search secrets (short alias)
xa translate "Hello" # Translate text
xa polish "draft" --no-stream # Polish without streaming
xa --debug trans zh "Hello" # Translate with debug
For more information, visit the project repository."#.to_string()
}