diff options
Diffstat (limited to 'crates/atuin-ai/src')
| -rw-r--r-- | crates/atuin-ai/src/commands.rs | 103 | ||||
| -rw-r--r-- | crates/atuin-ai/src/commands/inline.rs | 40 |
2 files changed, 110 insertions, 33 deletions
diff --git a/crates/atuin-ai/src/commands.rs b/crates/atuin-ai/src/commands.rs index 7d5ca16b..b35cec9e 100644 --- a/crates/atuin-ai/src/commands.rs +++ b/crates/atuin-ai/src/commands.rs @@ -1,8 +1,13 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + use atuin_common::shell::Shell; use clap::{Parser, Subcommand}; -use tracing::Level; +use eyre::Result; +use tracing_appender::rolling::{RollingFileAppender, Rotation}; use tracing_subscriber::{EnvFilter, Layer, fmt, layer::SubscriberExt, util::SubscriberInitExt}; - #[cfg(debug_assertions)] pub mod debug_render; @@ -72,7 +77,11 @@ enum Commands { pub async fn run() -> eyre::Result<()> { let cli = Cli::parse(); - init_tracing(cli.verbose); + let settings = atuin_client::settings::Settings::new()?; + + if settings.logs.ai_enabled() { + init_logging(&settings, cli.verbose)?; + } match cli.command { Commands::Init { shell } => init::run(shell).await, @@ -89,6 +98,7 @@ pub async fn run() -> eyre::Result<()> { cli.api_token, keep, debug_state, + &settings, ) .await } @@ -104,39 +114,90 @@ pub async fn run() -> eyre::Result<()> { } } -fn init_tracing(verbose: bool) { - let level = if verbose { Level::DEBUG } else { Level::INFO }; +pub fn detect_shell() -> Option<String> { + Some(Shell::current().to_string()) +} - // Create env filter - let env_filter = EnvFilter::from_default_env().add_directive( - format!("atuin_ai={}", level.as_str().to_lowercase()) - .parse() - .unwrap(), - ); +/// Initializes logging for the AI commands. +fn init_logging(settings: &atuin_client::settings::Settings, verbose: bool) -> Result<()> { + // ATUIN_LOG env var overrides config file level settings + let env_log_set = std::env::var("ATUIN_LOG").is_ok(); + + // Base filter from env var (or empty if not set) + let base_filter = + EnvFilter::from_env("ATUIN_LOG").add_directive("sqlx_sqlite::regexp=off".parse()?); + + // Use config level unless ATUIN_LOG is set + let filter = if env_log_set { + base_filter + } else { + EnvFilter::default() + .add_directive(settings.logs.ai_level().as_directive().parse()?) + .add_directive("sqlx_sqlite::regexp=off".parse()?) + }; + + let log_dir = PathBuf::from(&settings.logs.dir); + fs::create_dir_all(&log_dir)?; + + let filename = settings.logs.ai.file.clone(); + + // Clean up old log files + cleanup_old_logs(&log_dir, &filename, settings.logs.ai_retention()); - // Create console layer (only for verbose mode) let console_layer = if verbose { Some( fmt::layer() .with_writer(std::io::stderr) .with_ansi(true) .with_target(false) - .with_filter(env_filter), + .with_filter(filter.clone()), ) } else { None }; - // Initialize subscriber - let subscriber = tracing_subscriber::registry(); + let file_appender = RollingFileAppender::new(Rotation::DAILY, &log_dir, &filename); - if let Some(console) = console_layer { - subscriber.with(console).init(); + let base = tracing_subscriber::registry().with( + fmt::layer() + .with_writer(file_appender) + .with_ansi(false) + .with_filter(filter), + ); + + if let Some(console_layer) = console_layer { + base.with(console_layer).init(); } else { - subscriber.init(); - } + base.init(); + }; + + Ok(()) } -pub fn detect_shell() -> Option<String> { - Some(Shell::current().to_string()) +fn cleanup_old_logs(log_dir: &Path, prefix: &str, retention_days: u64) { + let cutoff = std::time::SystemTime::now() + - std::time::Duration::from_secs(retention_days * 24 * 60 * 60); + + let Ok(entries) = fs::read_dir(log_dir) else { + return; + }; + + for entry in entries.flatten() { + let path = entry.path(); + let Some(name) = path.file_name().and_then(|n| n.to_str()) else { + continue; + }; + + // Match files like "search.log.2024-02-23" or "daemon.log.2024-02-23" + if !name.starts_with(prefix) || name == prefix { + continue; + } + + if let Ok(metadata) = entry.metadata() + && let Ok(modified) = metadata.modified() + && modified < cutoff + { + let _ = fs::remove_file(&path); + } + } } diff --git a/crates/atuin-ai/src/commands/inline.rs b/crates/atuin-ai/src/commands/inline.rs index 3f9278a2..b49bfece 100644 --- a/crates/atuin-ai/src/commands/inline.rs +++ b/crates/atuin-ai/src/commands/inline.rs @@ -15,6 +15,7 @@ use eyre::{Context as _, Result, bail}; use futures::StreamExt; use reqwest::Url; use std::io::Write; +use tracing::{debug, error, info, trace}; pub async fn run( initial_command: Option<String>, @@ -23,6 +24,7 @@ pub async fn run( api_token: Option<String>, keep_output: bool, debug_state_file: Option<String>, + settings: &atuin_client::settings::Settings, ) -> Result<()> { // Install panic hook once at entry point to ensure terminal restoration install_panic_hook(); @@ -31,7 +33,6 @@ pub async fn run( // 1. Command line arguments/environment variables // 2. Settings file // 3. Default - let settings = atuin_client::settings::Settings::new()?; let endpoint = api_endpoint.as_deref().unwrap_or( settings .ai @@ -44,7 +45,7 @@ pub async fn run( let token = if let Some(token) = &api_token { token.to_string() } else { - ensure_hub_session(&settings, endpoint).await? + ensure_hub_session(settings, endpoint).await? }; let action = run_inline_tui( @@ -57,6 +58,7 @@ pub async fn run( }, keep_output, debug_state_file, + settings, ) .await?; emit_shell_result(action.0, &action.1); @@ -69,9 +71,12 @@ async fn ensure_hub_session( hub_address: &str, ) -> Result<String> { if let Some(token) = atuin_client::hub::get_session_token().await? { + debug!("Found Hub session, using existing token"); return Ok(token); } + info!("No Hub session found, prompting for authentication"); + println!("Atuin AI requires authenticating with Atuin Hub."); println!("This is separate from your sync setup."); println!("Press enter to begin (or esc to cancel)."); @@ -79,6 +84,8 @@ async fn ensure_hub_session( bail!("authentication canceled"); } + debug!("Starting Atuin Hub authentication..."); + println!("Authenticating with Atuin Hub..."); let mut auth_settings = settings.clone(); auth_settings.hub_address = hub_address.to_string(); @@ -93,6 +100,8 @@ async fn ensure_hub_session( ) .await?; + info!("Authentication complete, saving session token"); + atuin_client::hub::save_session(&token).await?; Ok(token) } @@ -141,6 +150,8 @@ fn create_chat_stream( } }; + debug!("Sending SSE request to {endpoint}"); + // Build request body let mut request_body = serde_json::json!({ "messages": messages, @@ -155,6 +166,7 @@ fn create_chat_stream( // Include session_id only if present (not on first request) if let Some(ref sid) = session_id { + trace!("Including session_id in request: {sid}"); request_body["session_id"] = serde_json::json!(sid); } @@ -178,12 +190,14 @@ fn create_chat_stream( let status = response.status(); if status == reqwest::StatusCode::UNAUTHORIZED { // Clear saved session on auth error + error!("SSE request failed with status: {status}, clearing session"); let _ = atuin_client::hub::delete_session().await; yield Err(eyre::eyre!("Hub session expired. Re-run to authenticate again.")); return; } if !status.is_success() { let body = response.text().await.unwrap_or_default(); + error!("SSE request failed ({}): {}", status, body); yield Err(eyre::eyre!("SSE request failed ({}): {}", status, body)); return; } @@ -197,7 +211,7 @@ fn create_chat_stream( let event_type = sse_event.event.as_str(); let data = sse_event.data.clone(); - tracing::debug!(event_type = %event_type, data = %data, "SSE event received"); + debug!(event_type = %event_type, "SSE event received"); match event_type { "text" => { @@ -245,8 +259,10 @@ fn create_chat_stream( "error" => { if let Ok(json) = serde_json::from_str::<serde_json::Value>(&data) { let message = json.get("message").and_then(|v| v.as_str()).unwrap_or("Unknown error").to_string(); + error!("SSE error: {}", message); yield Ok(ChatStreamEvent::Error(message)); } else { + error!("SSE error: {}", data); yield Ok(ChatStreamEvent::Error(data)); } break; @@ -391,6 +407,7 @@ async fn run_inline_tui( initial_prompt: Option<String>, keep_output: bool, debug_state_file: Option<String>, + settings: &atuin_client::settings::Settings, ) -> Result<(Action, String)> { // Initialize terminal guard and app state let mut guard = TerminalGuard::new(keep_output)?; @@ -425,7 +442,6 @@ async fn run_inline_tui( log_state!("init"); // Load theme - let settings = atuin_client::settings::Settings::new()?; let mut theme_manager = ThemeManager::new(None, None); let theme = theme_manager.load_theme(&settings.theme.name, None); @@ -486,12 +502,12 @@ async fn run_inline_tui( match stream.as_mut().poll_next(&mut cx) { std::task::Poll::Ready(Some(Ok(event))) => match event { ChatStreamEvent::TextChunk(text) => { - tracing::debug!(text = %text, "Processing TextChunk"); + trace!(text = %text, "Processing TextChunk"); app.state.append_streaming_text(&text); log_state!("text_chunk"); } ChatStreamEvent::ToolCall { id, name, input } => { - tracing::debug!(id = %id, name = %name, "Processing ToolCall"); + trace!(id = %id, name = %name, "Processing ToolCall"); app.state.add_tool_call(id, name, input); log_state!("tool_call"); } @@ -500,17 +516,17 @@ async fn run_inline_tui( content, is_error, } => { - tracing::debug!(tool_use_id = %tool_use_id, "Processing ToolResult"); + trace!(tool_use_id = %tool_use_id, "Processing ToolResult"); app.state.add_tool_result(tool_use_id, content, is_error); log_state!("tool_result"); } ChatStreamEvent::Status(status) => { - tracing::debug!(status = %status, "Processing Status"); + trace!(status = %status, "Processing Status"); app.state.update_streaming_status(&status); log_state!("status"); } ChatStreamEvent::Done { session_id } => { - tracing::debug!(session_id = %session_id, "Processing Done"); + trace!(session_id = %session_id, "Processing Done"); chat_stream = None; if !session_id.is_empty() { app.state.store_session_id(session_id); @@ -519,7 +535,7 @@ async fn run_inline_tui( log_state!("done"); } ChatStreamEvent::Error(msg) => { - tracing::debug!(error = %msg, "Processing Error"); + trace!(error = %msg, "Processing Error"); chat_stream = None; app.state.streaming_error(msg); log_state!("error"); @@ -544,7 +560,7 @@ async fn run_inline_tui( // Handle user cancellation (Esc during streaming) - drop the stream if app.state.was_interrupted && chat_stream.is_some() { - tracing::debug!("User cancelled streaming, dropping chat stream"); + debug!("User cancelled streaming, dropping chat stream"); chat_stream = None; app.state.was_interrupted = false; // Reset the flag } @@ -579,7 +595,7 @@ async fn run_inline_tui( token.clone(), app.state.session_id.clone(), messages, - &settings, + settings, )); } } |
