lean-ctx 3.5.22

Context Runtime for AI Agents with CCP. 63 MCP tools, 10 read modes, 95+ compression patterns, cross-session memory (CCP), persistent AI knowledge with temporal facts + contradiction detection, multi-agent context sharing + diaries, LITM-aware positioning, AAAK compact format, adaptive compression with Thompson Sampling bandits. Supports 24 AI tools. Reduces LLM token consumption by up to 99%.
Documentation
use std::collections::HashMap;
use std::path::Path;
use std::time::{Duration, Instant, UNIX_EPOCH};

pub(crate) fn cmd_index(args: &[String]) {
    let project_root = super::common::detect_project_root(args);
    let root = Path::new(&project_root);

    let sub = args
        .iter()
        .find(|a| !a.starts_with("--"))
        .map(String::as_str);
    match sub {
        Some("status") => {
            println!(
                "{}",
                crate::core::index_orchestrator::status_json(&project_root)
            );
        }
        Some("build") => {
            crate::core::index_orchestrator::ensure_all_background(&project_root);
            println!("started");
        }
        Some("build-full") => {
            // Force rebuild by deleting existing on-disk indexes first.
            let bm25 = crate::core::bm25_index::BM25Index::index_file_path(root);
            let _ = std::fs::remove_file(&bm25);
            if let Some(dir) = crate::core::graph_index::ProjectIndex::index_dir(&project_root) {
                let _ = std::fs::remove_file(dir.join("index.json"));
            }
            crate::core::index_orchestrator::ensure_all_background(&project_root);
            println!("started");
        }
        Some("build-graph") => {
            let root_str = project_root.clone();
            let result =
                crate::tools::ctx_impact::handle("build", None, &root_str, None, Some("text"));
            println!("{result}");
        }
        Some("watch") => run_watcher(root),
        _ => {
            eprintln!(
                "Usage: lean-ctx index <status|build|build-full|build-graph|watch> [--root <path>]\n\
                 Examples:\n\
                   lean-ctx index status\n\
                   lean-ctx index build          (BM25 + JSON graph index)\n\
                   lean-ctx index build-full     (force rebuild all indexes)\n\
                   lean-ctx index build-graph    (SQLite property graph for impact analysis)\n\
                   lean-ctx index watch"
            );
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct FileState {
    mtime_ms: u64,
    size_bytes: u64,
}

fn run_watcher(project_root: &Path) {
    let hash = crate::core::index_namespace::namespace_hash(project_root);
    let lock_name = format!("index-watch-{}", &hash[..8.min(hash.len())]);
    let Some(lock) = crate::core::startup_guard::try_acquire_lock(
        &lock_name,
        Duration::from_millis(800),
        Duration::from_secs(8),
    ) else {
        eprintln!("index watcher already running");
        return;
    };

    let mut last = snapshot_code_files(project_root);
    let mut pending: Option<Instant> = None;
    let poll = Duration::from_millis(700);
    let debounce = Duration::from_millis(900);

    loop {
        lock.touch();
        std::thread::sleep(poll);

        let cur = snapshot_code_files(project_root);
        if cur != last {
            last = cur;
            pending = Some(Instant::now());
            continue;
        }

        if let Some(t) = pending {
            if t.elapsed() >= debounce {
                crate::core::index_orchestrator::ensure_all_background(
                    project_root.to_string_lossy().as_ref(),
                );
                pending = None;
            }
        }
    }
}

fn snapshot_code_files(project_root: &Path) -> HashMap<String, FileState> {
    let walker = ignore::WalkBuilder::new(project_root)
        .hidden(true)
        .git_ignore(true)
        .git_global(true)
        .git_exclude(true)
        .build();

    let mut out: HashMap<String, FileState> = HashMap::new();
    for entry in walker.flatten() {
        let path = entry.path();
        if !path.is_file() {
            continue;
        }
        if path.components().any(|c| c.as_os_str() == ".git") {
            continue;
        }
        if !is_code_file(path) {
            continue;
        }
        let Ok(meta) = path.metadata() else {
            continue;
        };
        let Ok(modified) = meta.modified() else {
            continue;
        };
        let Some(mtime_ms) = modified
            .duration_since(UNIX_EPOCH)
            .ok()
            .map(|d| d.as_millis() as u64)
        else {
            continue;
        };

        let rel = path
            .strip_prefix(project_root)
            .unwrap_or(path)
            .to_string_lossy()
            .to_string();
        if rel.is_empty() {
            continue;
        }

        out.insert(
            rel,
            FileState {
                mtime_ms,
                size_bytes: meta.len(),
            },
        );
    }
    out
}

fn is_code_file(path: &Path) -> bool {
    crate::core::bm25_index::is_code_file(path)
}