lc/cli/
logging.rs

1//! Logging and log management commands
2
3use anyhow::Result;
4use colored::Colorize;
5use std::io::{self, Write};
6
7use crate::cli::{AnswerCommands, LogCommands, RecentCommands};
8use crate::database;
9
10/// Handle log-related commands
11pub async fn handle(command: LogCommands) -> Result<()> {
12    let db = database::Database::new()?;
13
14    match command {
15        LogCommands::Show { minimal } => show_logs(&db, minimal).await,
16        LogCommands::Recent { command, count } => handle_recent(&db, command, count).await,
17        LogCommands::Current => show_current(&db).await,
18        LogCommands::Stats => show_stats(&db).await,
19        LogCommands::Purge {
20            yes,
21            older_than_days,
22            keep_recent,
23            max_size_mb,
24        } => handle_purge(&db, yes, older_than_days, keep_recent, max_size_mb).await,
25    }
26}
27
28async fn show_logs(db: &database::Database, minimal: bool) -> Result<()> {
29    let entries = db.get_all_logs()?;
30
31    if entries.is_empty() {
32        println!("No chat logs found.");
33        return Ok(());
34    }
35
36    if minimal {
37        use tabled::{Table, Tabled};
38
39        #[derive(Tabled)]
40        struct LogEntry {
41            #[tabled(rename = "Chat ID")]
42            chat_id: String,
43            #[tabled(rename = "Model")]
44            model: String,
45            #[tabled(rename = "Question")]
46            question: String,
47            #[tabled(rename = "Time")]
48            time: String,
49        }
50
51        let table_data: Vec<LogEntry> = entries
52            .into_iter()
53            .map(|entry| LogEntry {
54                chat_id: entry.chat_id[..8].to_string(),
55                model: entry.model,
56                question: if entry.question.len() > 50 {
57                    format!("{}...", &entry.question[..50])
58                } else {
59                    entry.question
60                },
61                time: entry.timestamp.format("%m-%d %H:%M").to_string(),
62            })
63            .collect();
64
65        let table = Table::new(table_data);
66        println!("{}", table);
67    } else {
68        println!("\n{}", "Chat Logs:".bold().blue());
69
70        for entry in entries {
71            println!(
72                "\n{} {} ({})",
73                "Session:".bold(),
74                &entry.chat_id[..8],
75                entry.timestamp.format("%Y-%m-%d %H:%M:%S")
76            );
77            println!("{} {}", "Model:".bold(), entry.model);
78
79            // Show token usage if available
80            if let (Some(input_tokens), Some(output_tokens)) =
81                (entry.input_tokens, entry.output_tokens)
82            {
83                println!(
84                    "{} {} input + {} output = {} total tokens",
85                    "Tokens:".bold(),
86                    input_tokens,
87                    output_tokens,
88                    input_tokens + output_tokens
89                );
90            }
91
92            println!("{} {}", "Q:".yellow(), entry.question);
93            println!(
94                "{} {}",
95                "A:".green(),
96                if entry.response.len() > 200 {
97                    format!("{}...", &entry.response[..200])
98                } else {
99                    entry.response
100                }
101            );
102            println!("{}", "─".repeat(80).dimmed());
103        }
104    }
105
106    Ok(())
107}
108
109async fn handle_recent(
110    db: &database::Database,
111    command: Option<RecentCommands>,
112    count: usize,
113) -> Result<()> {
114    match command {
115        Some(RecentCommands::Answer { command }) => {
116            let entries = db.get_all_logs()?;
117            if let Some(entry) = entries.first() {
118                match command {
119                    Some(AnswerCommands::Code) => {
120                        let code_blocks = extract_code_blocks(&entry.response);
121                        if code_blocks.is_empty() {
122                            anyhow::bail!("No code blocks found in the last answer");
123                        } else {
124                            for block in code_blocks {
125                                println!("{}", block);
126                            }
127                        }
128                    }
129                    None => {
130                        println!("{}", entry.response);
131                    }
132                }
133            } else {
134                anyhow::bail!("No recent logs found");
135            }
136        }
137        Some(RecentCommands::Question) => {
138            let entries = db.get_all_logs()?;
139            if let Some(entry) = entries.first() {
140                println!("{}", entry.question);
141            } else {
142                anyhow::bail!("No recent logs found");
143            }
144        }
145        Some(RecentCommands::Model) => {
146            let entries = db.get_all_logs()?;
147            if let Some(entry) = entries.first() {
148                println!("{}", entry.model);
149            } else {
150                anyhow::bail!("No recent logs found");
151            }
152        }
153        Some(RecentCommands::Session) => {
154            let entries = db.get_all_logs()?;
155            if let Some(entry) = entries.first() {
156                println!("{}", entry.chat_id);
157            } else {
158                anyhow::bail!("No recent logs found");
159            }
160        }
161        None => {
162            // Default behavior - show recent logs
163            let mut entries = db.get_all_logs()?;
164            entries.truncate(count);
165
166            if entries.is_empty() {
167                println!("No recent logs found.");
168                return Ok(());
169            }
170
171            println!(
172                "\n{} (showing {} entries)",
173                "Recent Logs:".bold().blue(),
174                entries.len()
175            );
176
177            for entry in entries {
178                println!(
179                    "\n{} {} ({})",
180                    "Session:".bold(),
181                    &entry.chat_id[..8],
182                    entry.timestamp.format("%Y-%m-%d %H:%M:%S")
183                );
184                println!("{} {}", "Model:".bold(), entry.model);
185
186                // Show token usage if available
187                if let (Some(input_tokens), Some(output_tokens)) =
188                    (entry.input_tokens, entry.output_tokens)
189                {
190                    println!(
191                        "{} {} input + {} output = {} total tokens",
192                        "Tokens:".bold(),
193                        input_tokens,
194                        output_tokens,
195                        input_tokens + output_tokens
196                    );
197                }
198
199                println!("{} {}", "Q:".yellow(), entry.question);
200                println!(
201                    "{} {}",
202                    "A:".green(),
203                    if entry.response.len() > 150 {
204                        format!("{}...", &entry.response[..150])
205                    } else {
206                        entry.response
207                    }
208                );
209                println!("{}", "─".repeat(60).dimmed());
210            }
211        }
212    }
213
214    Ok(())
215}
216
217async fn show_current(db: &database::Database) -> Result<()> {
218    if let Some(session_id) = db.get_current_session_id()? {
219        let history = db.get_chat_history(&session_id)?;
220
221        println!("\n{} {}", "Current Session:".bold().blue(), session_id);
222        println!("{} {} messages", "Messages:".bold(), history.len());
223
224        for (i, entry) in history.iter().enumerate() {
225            println!(
226                "\n{} {} ({})",
227                format!("Message {}:", i + 1).bold(),
228                entry.model,
229                entry.timestamp.format("%H:%M:%S")
230            );
231            println!("{} {}", "Q:".yellow(), entry.question);
232            println!(
233                "{} {}",
234                "A:".green(),
235                if entry.response.len() > 100 {
236                    format!("{}...", &entry.response[..100])
237                } else {
238                    entry.response.clone()
239                }
240            );
241        }
242    } else {
243        println!("No current session found.");
244    }
245
246    Ok(())
247}
248
249async fn show_stats(db: &database::Database) -> Result<()> {
250    let stats = db.get_stats()?;
251
252    println!("\n{}", "Database Statistics:".bold().blue());
253    println!();
254
255    // Basic stats
256    println!("{} {}", "Total Entries:".bold(), stats.total_entries);
257    println!("{} {}", "Unique Sessions:".bold(), stats.unique_sessions);
258
259    // File size formatting
260    let file_size_str = if stats.file_size_bytes < 1024 {
261        format!("{} bytes", stats.file_size_bytes)
262    } else if stats.file_size_bytes < 1024 * 1024 {
263        format!("{:.1} KB", stats.file_size_bytes as f64 / 1024.0)
264    } else {
265        format!("{:.1} MB", stats.file_size_bytes as f64 / (1024.0 * 1024.0))
266    };
267    println!("{} {}", "Database Size:".bold(), file_size_str);
268
269    // Date range
270    if let Some((earliest, latest)) = stats.date_range {
271        println!(
272            "{} {} to {}",
273            "Date Range:".bold(),
274            earliest.format("%Y-%m-%d %H:%M:%S"),
275            latest.format("%Y-%m-%d %H:%M:%S")
276        );
277    } else {
278        println!("{} {}", "Date Range:".bold(), "No entries".dimmed());
279    }
280
281    // Model usage
282    if !stats.model_usage.is_empty() {
283        println!("\n{}", "Model Usage:".bold().blue());
284        for (model, count) in stats.model_usage {
285            let percentage = if stats.total_entries > 0 {
286                (count as f64 / stats.total_entries as f64) * 100.0
287            } else {
288                0.0
289            };
290            println!(
291                "  {} {} ({} - {:.1}%)",
292                "•".blue(),
293                model.bold(),
294                count,
295                percentage
296            );
297        }
298    }
299
300    Ok(())
301}
302
303async fn handle_purge(
304    db: &database::Database,
305    yes: bool,
306    older_than_days: Option<u32>,
307    keep_recent: Option<usize>,
308    max_size_mb: Option<u64>,
309) -> Result<()> {
310    // Check if any specific purge options are provided
311    let has_specific_options =
312        older_than_days.is_some() || keep_recent.is_some() || max_size_mb.is_some();
313
314    if has_specific_options {
315        // Smart purge with specific options
316        let deleted_count = db.smart_purge(older_than_days, keep_recent, max_size_mb)?;
317
318        if deleted_count > 0 {
319            println!("{} Purged {} log entries", "✓".green(), deleted_count);
320
321            if let Some(days) = older_than_days {
322                println!("  - Removed entries older than {} days", days);
323            }
324            if let Some(count) = keep_recent {
325                println!("  - Kept only the {} most recent entries", count);
326            }
327            if let Some(size) = max_size_mb {
328                println!("  - Enforced maximum database size of {} MB", size);
329            }
330        } else {
331            println!("{} No logs needed to be purged", "ℹ️".blue());
332        }
333    } else {
334        // Full purge (existing behavior)
335        if !yes {
336            print!("Are you sure you want to purge all logs? This cannot be undone. (y/N): ");
337            // Deliberately flush stdout to ensure prompt appears before user input
338            io::stdout().flush()?;
339
340            let mut input = String::new();
341            io::stdin().read_line(&mut input)?;
342
343            if !input.trim().to_lowercase().starts_with('y') {
344                println!("Purge cancelled.");
345                return Ok(());
346            }
347        }
348
349        db.purge_all_logs()?;
350        println!("{} All logs purged successfully", "✓".green());
351    }
352
353    Ok(())
354}
355
356// Helper function to extract code blocks from markdown text
357fn extract_code_blocks(text: &str) -> Vec<String> {
358    let mut code_blocks = Vec::new();
359    let mut in_code_block = false;
360    let mut current_block = String::new();
361
362    for line in text.lines() {
363        if line.starts_with("```") {
364            if in_code_block {
365                // End of code block
366                if !current_block.trim().is_empty() {
367                    code_blocks.push(current_block.trim().to_string());
368                }
369                current_block.clear();
370                in_code_block = false;
371            } else {
372                // Start of code block
373                in_code_block = true;
374            }
375        } else if in_code_block {
376            current_block.push_str(line);
377            current_block.push('\n');
378        }
379    }
380
381    // Handle case where code block doesn't end properly
382    if in_code_block && !current_block.trim().is_empty() {
383        code_blocks.push(current_block.trim().to_string());
384    }
385
386    code_blocks
387}