cached-context 0.3.0

File cache with diff tracking for AI coding agents
Documentation
//! Cachebro - Main entry point

use cached_context::cache::CacheStore;
use cached_context::types::CacheConfig;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
use tracing::info;

/// CLI commands
#[derive(Subcommand)]
enum Commands {
    /// Start the MCP server (default)
    Serve {
        /// Database path (overrides CACHEBRO_DIR env var)
        #[arg(short, long)]
        db_path: Option<PathBuf>,

        /// Working directory for relative paths
        #[arg(short, long)]
        workdir: Option<PathBuf>,
    },
    /// Show cache statistics
    Status {
        /// Database path (overrides CACHEBRO_DIR env var)
        #[arg(short, long)]
        db_path: Option<PathBuf>,

        /// Working directory
        #[arg(short, long)]
        workdir: Option<PathBuf>,
    },
    /// Clear the cache
    Clear {
        /// Database path (overrides CACHEBRO_DIR env var)
        #[arg(short, long)]
        db_path: Option<PathBuf>,

        /// Working directory
        #[arg(short, long)]
        workdir: Option<PathBuf>,
    },
}

/// CLI argument parser
#[derive(Parser)]
#[command(name = "cached-context")]
#[command(about = "cached-context - File cache with diff tracking cache management service", long_about = None)]
struct Args {
    #[command(subcommand)]
    command: Option<Commands>,

    /// Enable verbose logging (overridden by RUST_LOG env var)
    #[arg(short, long, global = true)]
    verbose: bool,
}

/// Return the default DB path.
///
/// Prefers the `CACHEBRO_DIR` environment variable; falls back to `.cached-context/cache.db`.
fn default_db_path() -> PathBuf {
    std::env::var("CACHEBRO_DIR")
        .map(|dir| PathBuf::from(dir).join("cache.db"))
        .unwrap_or_else(|_| PathBuf::from(".cached-context/cache.db"))
}

/// Initialize logging.
///
/// `RUST_LOG` environment variable takes priority over the `--verbose` flag.
fn init_logging(verbose: bool) {
    // RUST_LOG env var takes priority; --verbose flag is a convenience fallback.
    let filter = std::env::var("RUST_LOG")
        .ok()
        .and_then(|v| v.parse::<tracing_subscriber::EnvFilter>().ok())
        .unwrap_or_else(|| {
            tracing_subscriber::EnvFilter::new(if verbose { "debug" } else { "info" })
        });

    tracing_subscriber::fmt()
        .with_env_filter(filter)
        .with_target(false)
        .with_thread_ids(false)
        .with_file(true)
        .with_line_number(true)
        .with_writer(std::io::stderr)
        .init();
}

/// Create a cache store with the given config
async fn create_cache(db_path: PathBuf, workdir: Option<PathBuf>) -> Result<CacheStore, String> {
    let workdir = workdir.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));

    let config = CacheConfig {
        db_path,
        session_id: uuid::Uuid::new_v4().to_string(),
        workdir,
    };

    let store = CacheStore::new(config).map_err(|e| format!("Failed to create cache: {}", e))?;
    store.init().await.map_err(|e| format!("Failed to initialize cache: {}", e))?;

    Ok(store)
}

/// Handle the status command
async fn handle_status(db_path: PathBuf, workdir: Option<PathBuf>) -> Result<(), String> {
    let store = create_cache(db_path, workdir).await?;
    let stats = store.get_stats().await.map_err(|e| format!("Failed to get stats: {}", e))?;

    println!("Cache Statistics:");
    println!("  Files tracked: {}", stats.files_tracked);
    println!("  Total tokens saved: {}", stats.tokens_saved);
    println!("  Session tokens saved: {}", stats.session_tokens_saved);

    Ok(())
}

/// Handle the clear command
async fn handle_clear(db_path: PathBuf, workdir: Option<PathBuf>) -> Result<(), String> {
    let store = create_cache(db_path, workdir).await?;
    store.clear().await.map_err(|e| format!("Failed to clear cache: {}", e))?;

    println!("Cache cleared successfully");
    Ok(())
}

/// Run the MCP server
async fn run_server(db_path: PathBuf, workdir: Option<PathBuf>) -> Result<(), String> {
    let store = create_cache(db_path, workdir).await?;

    info!("Starting MCP server...");

    // Start the MCP server - this blocks until stdin closes (MCP over stdio)
    cached_context::mcp::start_mcp_server_with_store(store)
        .await
        .map_err(|e| format!("MCP server error: {}", e))?;

    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), String> {
    let args = Args::parse();

    // Initialize logging
    init_logging(args.verbose);

    // Default to serve command if no subcommand provided
    let command = args.command.unwrap_or(Commands::Serve {
        db_path: None,
        workdir: None,
    });

    match command {
        Commands::Serve { db_path, workdir } => {
            run_server(db_path.unwrap_or_else(default_db_path), workdir).await?;
        }
        Commands::Status { db_path, workdir } => {
            handle_status(db_path.unwrap_or_else(default_db_path), workdir).await?;
        }
        Commands::Clear { db_path, workdir } => {
            handle_clear(db_path.unwrap_or_else(default_db_path), workdir).await?;
        }
    }

    Ok(())
}