codex-recall 0.1.3

Local search and recall for Codex session JSONL archives
Documentation
use crate::commands::index::resolve_sources;
use crate::commands::watch::{build_status_report, status_json};
use crate::config::{default_db_path, default_state_path, DEFAULT_LAUNCH_AGENT_LABEL};
use crate::store::Store;
use anyhow::Result;
use clap::Args;
use serde_json::json;
use std::path::PathBuf;
use std::time::Duration;

#[derive(Debug, Clone, Args)]
pub struct DoctorArgs {
    #[arg(long, help = "SQLite index path")]
    pub db: Option<PathBuf>,
    #[arg(long, help = "Emit machine-readable JSON")]
    pub json: bool,
    #[arg(long, help = "Watch state JSON path")]
    pub state: Option<PathBuf>,
    #[arg(long = "source", help = "Session archive root to scan; repeatable")]
    pub sources: Vec<PathBuf>,
    #[arg(
        long,
        default_value_t = 3,
        help = "Seconds a file must be quiet before indexing"
    )]
    pub quiet_for: u64,
    #[arg(
        long,
        default_value = DEFAULT_LAUNCH_AGENT_LABEL,
        help = "LaunchAgent label"
    )]
    pub agent_label: String,
    #[arg(long, help = "LaunchAgent plist path")]
    pub agent_path: Option<PathBuf>,
}

#[derive(Debug, Clone, Args)]
pub struct StatsArgs {
    #[arg(long, help = "SQLite index path")]
    pub db: Option<PathBuf>,
}

pub fn run_doctor(args: DoctorArgs) -> Result<()> {
    let db_path = args.db.unwrap_or(default_db_path()?);
    let state_path = args.state.unwrap_or(default_state_path()?);
    let quiet_for = Duration::from_secs(args.quiet_for);
    let agent_path = args
        .agent_path
        .unwrap_or(default_launch_agent_path(&args.agent_label)?);
    let db_existed = db_path.exists();
    let sources = resolve_sources(args.sources)?;
    let status_report = build_status_report(
        db_path.clone(),
        state_path,
        sources.clone(),
        quiet_for,
        args.agent_label,
        agent_path,
    )?;
    let source_status = sources
        .iter()
        .map(|source| {
            json!({
                "path": source,
                "exists": source.exists(),
            })
        })
        .collect::<Vec<_>>();

    if !db_existed {
        if args.json {
            let value = json!({
                "ok": false,
                "db_path": db_path,
                "db_existed": false,
                "db_exists": false,
                "checks": {
                    "quick_check": "missing",
                    "fts_integrity": "missing",
                },
                "stats": {
                    "sessions": 0,
                    "events": 0,
                    "source_files": 0,
                    "duplicate_source_files": 0,
                },
                "freshness": {
                    "state": status_report.freshness.state,
                    "message": status_report.freshness.message,
                    "status": status_json(&status_report),
                },
                "sources": source_status,
            });
            println!("{}", serde_json::to_string_pretty(&value)?);
            return Ok(());
        }

        println!("database: {} (missing)", db_path.display());
        println!("quick_check: missing");
        println!("fts_integrity: missing");
        println!("stats: 0 sessions, 0 events, 0 source files, 0 duplicate source files");
        for source in sources {
            let status = if source.exists() { "exists" } else { "missing" };
            println!("source: {} ({status})", source.display());
        }
        println!("next: run codex-recall index");
        return Ok(());
    }

    let store = Store::open_readonly(&db_path)?;
    let stats = store.stats()?;
    let quick_check = match store.quick_check() {
        Ok(value) => value,
        Err(error) => format!("error: {error:#}"),
    };
    let fts_integrity = match store.fts_read_check() {
        Ok(()) => "ok".to_owned(),
        Err(error) => format!("error: {error:#}"),
    };
    let ok = quick_check == "ok" && fts_integrity == "ok";

    if args.json {
        let value = json!({
            "ok": ok,
            "db_path": db_path,
            "db_existed": db_existed,
            "db_exists": db_existed,
            "checks": {
                "quick_check": quick_check,
                "fts_integrity": fts_integrity,
            },
            "stats": {
                "sessions": stats.session_count,
                "events": stats.event_count,
                "source_files": stats.source_file_count,
                "duplicate_source_files": stats.duplicate_source_file_count,
            },
            "freshness": {
                "state": status_report.freshness.state,
                "message": status_report.freshness.message,
                "status": status_json(&status_report),
            },
            "sources": source_status,
        });
        println!("{}", serde_json::to_string_pretty(&value)?);
        return Ok(());
    }

    println!("database: {}", db_path.display());
    println!("quick_check: {quick_check}");
    println!("fts_integrity: {fts_integrity}");
    println!(
        "stats: {} sessions, {} events, {} source files, {} duplicate source files",
        stats.session_count,
        stats.event_count,
        stats.source_file_count,
        stats.duplicate_source_file_count
    );
    println!(
        "freshness: {} ({})",
        status_report.freshness.state, status_report.freshness.message
    );
    for source in sources {
        let status = if source.exists() { "exists" } else { "missing" };
        println!("source: {} ({status})", source.display());
    }
    Ok(())
}

fn default_launch_agent_path(label: &str) -> Result<PathBuf> {
    let home = std::env::var_os("HOME")
        .map(PathBuf::from)
        .ok_or_else(|| anyhow::anyhow!("HOME is not set"))?;
    Ok(home
        .join("Library")
        .join("LaunchAgents")
        .join(format!("{label}.plist")))
}

pub fn run_stats(args: StatsArgs) -> Result<()> {
    let db_path = args.db.unwrap_or(default_db_path()?);
    let stats = Store::open_readonly(db_path)?.stats()?;
    println!(
        "{} sessions, {} events, {} source files, {} duplicate source files",
        stats.session_count,
        stats.event_count,
        stats.source_file_count,
        stats.duplicate_source_file_count
    );
    Ok(())
}