mod cli; mod client; mod config; mod function; mod rag; mod render; mod repl; mod serve; #[macro_use] mod utils; #[macro_use] extern crate log; use crate::cli::Cli; use crate::client::{ call_chat_completions, call_chat_completions_streaming, list_models, ModelType, }; use crate::config::{ ensure_parent_exists, list_agents, load_env_file, macro_execute, Config, GlobalConfig, Input, WorkingMode, CODE_ROLE, EXPLAIN_SHELL_ROLE, SHELL_ROLE, TEMP_SESSION_NAME, }; use crate::render::render_error; use crate::repl::Repl; use crate::utils::*; use anyhow::{bail, Result}; use clap::Parser; use inquire::validator::Validation; use inquire::Text; use is_terminal::IsTerminal; use parking_lot::RwLock; use simplelog::{format_description, ConfigBuilder, LevelFilter, SimpleLogger, WriteLogger}; use std::{env, io::stdin, process, sync::Arc}; #[tokio::main] async fn main() -> Result<()> { load_env_file()?; let cli = Cli::parse(); let text = cli.text()?; let working_mode = if cli.serve.is_some() { WorkingMode::Serve } else if text.is_none() && cli.file.is_empty() { WorkingMode::Repl } else { WorkingMode::Cmd }; setup_logger(working_mode.is_serve())?; let config = Arc::new(RwLock::new(Config::init(working_mode)?)); if let Err(err) = run(config, cli, text).await { render_error(err); std::process::exit(1); } Ok(()) } async fn run(config: GlobalConfig, cli: Cli, text: Option) -> Result<()> { let abort_signal = create_abort_signal(); if let Some(addr) = cli.serve { return serve::run(config, addr).await; } if cli.info { config.write().info_flag = true; } if cli.list_models { for model in list_models(&config.read(), ModelType::Chat) { println!("{}", model.id()); } return Ok(()); } if cli.list_roles { let roles = Config::list_roles(true).join("\n"); println!("{roles}"); return Ok(()); } if cli.list_agents { let agents = list_agents().join("\n"); println!("{agents}"); return Ok(()); } if cli.list_rags { let rags = Config::list_rags().join("\n"); println!("{rags}"); return Ok(()); } if cli.list_macros { let macros = Config::list_macros().join("\n"); println!("{macros}"); return Ok(()); } if cli.dry_run { config.write().dry_run = true; } if let Some(agent) = &cli.agent { let session = cli.session.as_ref().map(|v| match v { Some(v) => v.as_str(), None => TEMP_SESSION_NAME, }); if !cli.agent_variable.is_empty() { config.write().agent_variables = Some( cli.agent_variable .chunks(2) .map(|v| (v[0].to_string(), v[1].to_string())) .collect(), ); } let ret = Config::use_agent(&config, agent, session, abort_signal.clone()).await; config.write().agent_variables = None; ret?; } else { if let Some(prompt) = &cli.prompt { config.write().use_prompt(prompt)?; } else if let Some(name) = &cli.role { config.write().use_role(name)?; } else if cli.execute { config.write().use_role(SHELL_ROLE)?; } else if cli.code { config.write().use_role(CODE_ROLE)?; } if let Some(session) = &cli.session { config .write() .use_session(session.as_ref().map(|v| v.as_str()))?; } if let Some(rag) = &cli.rag { Config::use_rag(&config, Some(rag), abort_signal.clone()).await?; } } if cli.list_sessions { let sessions = config.read().list_sessions().join("\n"); println!("{sessions}"); return Ok(()); } if let Some(model_id) = &cli.model { config.write().set_model(model_id)?; } if cli.no_stream { config.write().stream = false; } if cli.empty_session { config.write().empty_session()?; } if cli.save_session { config.write().set_save_session_this_time()?; } if cli.info { let info = config.read().info()?; println!("{}", info); return Ok(()); } let is_repl = config.read().working_mode.is_repl(); if cli.rebuild_rag { Config::rebuild_rag(&config, abort_signal.clone()).await?; if is_repl { return Ok(()); } } if let Some(name) = &cli.macro_name { macro_execute(&config, name, text.as_deref(), abort_signal.clone()).await?; return Ok(()); } if cli.execute && !is_repl { if cfg!(target_os = "macos") && !stdin().is_terminal() { bail!("Unable to read the pipe for shell execution on MacOS") } let input = create_input(&config, text, &cli.file, abort_signal.clone()).await?; shell_execute(&config, &SHELL, input, abort_signal.clone()).await?; return Ok(()); } config.write().apply_prelude()?; match is_repl { false => { let mut input = create_input(&config, text, &cli.file, abort_signal.clone()).await?; input.use_embeddings(abort_signal.clone()).await?; start_directive(&config, input, cli.code, abort_signal).await } true => { if !*IS_STDOUT_TERMINAL { bail!("No TTY for REPL") } start_interactive(&config).await } } } #[async_recursion::async_recursion] async fn start_directive( config: &GlobalConfig, input: Input, code_mode: bool, abort_signal: AbortSignal, ) -> Result<()> { let client = input.create_client()?; let extract_code = !*IS_STDOUT_TERMINAL && code_mode; config.write().before_chat_completion(&input)?; let (output, tool_results) = if !input.stream() || extract_code { call_chat_completions(&input, extract_code, client.as_ref(), abort_signal.clone()).await? } else { call_chat_completions_streaming(&input, client.as_ref(), abort_signal.clone()).await? }; config .write() .after_chat_completion(&input, &output, &tool_results)?; if !tool_results.is_empty() { start_directive( config, input.merge_tool_results(output, tool_results), code_mode, abort_signal, ) .await?; } config.write().exit_session()?; Ok(()) } async fn start_interactive(config: &GlobalConfig) -> Result<()> { let mut repl: Repl = Repl::init(config)?; repl.run().await } #[async_recursion::async_recursion] async fn shell_execute( config: &GlobalConfig, shell: &Shell, mut input: Input, abort_signal: AbortSignal, ) -> Result<()> { let client = input.create_client()?; config.write().before_chat_completion(&input)?; let ret = abortable_run_with_spinner( client.chat_completions(input.clone()), "Generating", abort_signal.clone(), ) .await; let mut eval_str = ret?.text; if let Ok(true) = CODE_BLOCK_RE.is_match(&eval_str) { eval_str = extract_block(&eval_str); } config .write() .after_chat_completion(&input, &eval_str, &[])?; if eval_str.is_empty() { bail!("No command generated"); } if config.read().dry_run { config.read().print_markdown(&eval_str)?; return Ok(()); } if *IS_STDOUT_TERMINAL { let options = ["execute", "revise", "describe", "copy", "quit"]; let command = color_text(eval_str.trim(), nu_ansi_term::Color::Rgb(255, 165, 0)); let first_letter_color = nu_ansi_term::Color::Cyan; let prompt_text = options .iter() .map(|v| format!("{}{}", color_text(&v[0..1], first_letter_color), &v[1..])) .collect::>() .join(&dimmed_text(" | ")); loop { println!("{command}"); let answer = Text::new(&format!("{prompt_text}:")) .with_default("e") .with_validator( |input: &str| match matches!(input, "e" | "r" | "d" | "c" | "q") { true => Ok(Validation::Valid), false => Ok(Validation::Invalid( "Invalid option, choice one of e, r, d, c or q".into(), )), }, ) .prompt()?; match answer.as_str() { "e" => { debug!("{} {:?}", shell.cmd, &[&shell.arg, &eval_str]); let code = run_command(&shell.cmd, &[&shell.arg, &eval_str], None)?; if code == 0 && config.read().save_shell_history { let _ = append_to_shell_history(&shell.name, &eval_str, code); } process::exit(code); } "r" => { let revision = Text::new("Enter your revision:").prompt()?; let text = format!("{}\n{revision}", input.text()); input.set_text(text); return shell_execute(config, shell, input, abort_signal.clone()).await; } "d" => { let role = config.read().retrieve_role(EXPLAIN_SHELL_ROLE)?; let input = Input::from_str(config, &eval_str, Some(role)); if input.stream() { call_chat_completions_streaming( &input, client.as_ref(), abort_signal.clone(), ) .await?; } else { call_chat_completions(&input, false, client.as_ref(), abort_signal.clone()) .await?; } println!(); continue; } "c" => { set_text(&eval_str)?; println!("{}", dimmed_text("✓ Copied the command.")); } _ => {} } break; } } else { println!("{}", eval_str); } Ok(()) } async fn create_input( config: &GlobalConfig, text: Option, file: &[String], abort_signal: AbortSignal, ) -> Result { let input = if file.is_empty() { Input::from_str(config, &text.unwrap_or_default(), None) } else { Input::from_files_with_spinner( config, &text.unwrap_or_default(), file.to_vec(), None, abort_signal, ) .await? }; if input.is_empty() { bail!("No input"); } Ok(input) } fn setup_logger(is_serve: bool) -> Result<()> { let (log_level, log_path) = Config::log_config(is_serve)?; if log_level == LevelFilter::Off { return Ok(()); } let crate_name = env!("CARGO_CRATE_NAME"); let log_filter = match std::env::var(get_env_name("log_filter")) { Ok(v) => v, Err(_) => match is_serve { true => format!("{crate_name}::serve"), false => crate_name.into(), }, }; let config = ConfigBuilder::new() .add_filter_allow(log_filter) .set_time_format_custom(format_description!( "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3]Z" )) .set_thread_level(LevelFilter::Off) .build(); match log_path { None => { SimpleLogger::init(log_level, config)?; } Some(log_path) => { ensure_parent_exists(&log_path)?; let log_file = std::fs::File::create(log_path)?; WriteLogger::init(log_level, config, log_file)?; } } Ok(()) }