claco 2.0.0

a CLI tool for boosting Claude Code productive.
Documentation
use anyhow::Result;
use claco::{claude_home, SessionEntry};
use regex::Regex;
use std::fs;
use std::io::{BufRead, BufReader};

use super::format_timestamp_local;

/// Display history of user messages for the current project
///
/// Shows all user input messages from Claude Code sessions in the current directory.
/// Can optionally filter by a specific session ID.
///
/// # Arguments
/// * `session_id` - Optional session ID to filter messages
pub fn handle_history(session_id: Option<String>) -> Result<()> {
    // Get current working directory
    let cwd = std::env::current_dir()?;
    let cwd_str = cwd.to_string_lossy();

    let projects_dir = claude_home()?.join("projects");

    if !projects_dir.exists() {
        println!("No Claude projects directory found");
        return Ok(());
    }

    // Find the project directory that matches the current working directory
    let mut matched_project_path = None;

    'outer: for entry in fs::read_dir(&projects_dir)? {
        let entry = entry?;
        let path = entry.path();

        if !path.is_dir() {
            continue;
        }

        // Try to read the actual cwd from any JSONL file in this project
        for session_entry in fs::read_dir(&path)? {
            let session_entry = session_entry?;
            let session_path = session_entry.path();

            if session_path.extension().and_then(|s| s.to_str()) == Some("jsonl") {
                if let Ok(file) = fs::File::open(&session_path) {
                    let reader = BufReader::new(file);
                    if let Some(Ok(first_line)) = reader.lines().next() {
                        if let Ok(entry) = serde_json::from_str::<SessionEntry>(&first_line) {
                            if let Some(ref entry_cwd) = entry.cwd {
                                if entry_cwd == &cwd_str {
                                    matched_project_path = Some(path);
                                    break 'outer;
                                }
                            }
                        }
                    }
                }
            }
        }

        if matched_project_path.is_some() {
            break;
        }
    }

    let project_path = match matched_project_path {
        Some(path) => path,
        None => {
            println!("No Claude project found for current directory: {cwd_str}");
            return Ok(());
        }
    };

    // Read all session files or just the specified one
    let entries = fs::read_dir(&project_path)?;

    // Compile regex once for performance
    let command_regex = Regex::new(r"<command-name>(/[^<]+)</command-name>").unwrap();

    for entry in entries {
        let entry = entry?;
        let path = entry.path();

        if path.extension().and_then(|s| s.to_str()) == Some("jsonl") {
            let file_name = match path.file_stem() {
                Some(stem) => stem.to_string_lossy(),
                None => {
                    eprintln!("warning: could not get file stem from: {}", path.display());
                    continue;
                }
            };

            // If session_id is specified, only process that session
            if let Some(ref sid) = session_id {
                if file_name != *sid {
                    continue;
                }
            }

            // Read and parse JSONL file
            let file = fs::File::open(&path)?;
            let reader = BufReader::new(file);

            let mut skip_next = false;

            for line in reader.lines() {
                let line = line?;
                if line.trim().is_empty() {
                    continue;
                }

                // Skip this line if previous was a slash command
                if skip_next {
                    skip_next = false;
                    continue;
                }

                // Only process lines that contain "isSidechain":false, "type":"user" and "role":"user"
                if !(line.contains(r#""type":"user""#)
                    && line.contains(r#""role":"user""#)
                    && line.contains(r#""isSidechain":false"#))
                {
                    continue;
                }

                // Hard-coded caveat message to skip
                if line.contains(r#"Caveat: The messages below were generated by the user while running local commands."#) {
                    continue;
                }

                // Now try to parse the user message
                if let Ok(entry) = serde_json::from_str::<SessionEntry>(&line) {
                    // Check if the content contains a slash command
                    if let (Some(ref message), Some(ref timestamp)) =
                        (&entry.message, &entry.timestamp)
                    {
                        if let Some(captures) = command_regex.captures(&message.content) {
                            // Print only the slash command
                            if let Some(command) = captures.get(1) {
                                println!(
                                    "{}: {}",
                                    format_timestamp_local(timestamp),
                                    command.as_str()
                                );
                                // Skip the next line after a slash command
                                skip_next = true;
                            }
                        } else {
                            // No command-name tag found, print the full content if not blank
                            let content = message.content.trim();
                            if !content.is_empty() {
                                println!(
                                    "{}: {}",
                                    format_timestamp_local(timestamp),
                                    message.content
                                );
                            }
                        }
                    }
                }
            }
        }
    }

    Ok(())
}