lean-ctx 3.1.4

Context Runtime for AI Agents with CCP. 42 MCP tools, 10 read modes, 90+ 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::path::Path;

use ignore::WalkBuilder;

use crate::core::protocol;
use crate::core::tokens::count_tokens;

pub fn handle(path: &str, depth: usize, show_hidden: bool) -> (String, usize) {
    let root = Path::new(path);
    if !root.is_dir() {
        return (format!("ERROR: {path} is not a directory"), 0);
    }

    let raw_output = generate_raw_tree(root, depth, show_hidden);
    let compact_output = generate_compact_tree(root, depth, show_hidden);

    let raw_tokens = count_tokens(&raw_output);
    let compact_tokens = count_tokens(&compact_output);
    let savings = protocol::format_savings(raw_tokens, compact_tokens);

    (format!("{compact_output}\n{savings}"), raw_tokens)
}

fn generate_compact_tree(root: &Path, max_depth: usize, show_hidden: bool) -> String {
    let mut lines = Vec::new();
    let mut entries: Vec<(usize, String, bool, usize)> = Vec::new();

    let walker = WalkBuilder::new(root)
        .hidden(!show_hidden)
        .git_ignore(true)
        .git_global(true)
        .git_exclude(true)
        .max_depth(Some(max_depth))
        .sort_by_file_name(|a, b| a.cmp(b))
        .build();

    for entry in walker.filter_map(|e| e.ok()) {
        if entry.depth() == 0 {
            continue;
        }

        let name = entry.file_name().to_string_lossy().to_string();

        let depth = entry.depth();
        let is_dir = entry.file_type().is_some_and(|ft| ft.is_dir());

        let file_count = if is_dir {
            count_files_in_dir(entry.path())
        } else {
            0
        };

        entries.push((depth, name, is_dir, file_count));
    }

    for (depth, name, is_dir, file_count) in &entries {
        let indent = "  ".repeat(depth.saturating_sub(1));
        if *is_dir {
            lines.push(format!("{indent}{name}/ ({file_count})"));
        } else {
            lines.push(format!("{indent}{name}"));
        }
    }

    lines.join("\n")
}

fn generate_raw_tree(root: &Path, depth: usize, show_hidden: bool) -> String {
    let mut lines = Vec::new();

    let walker = WalkBuilder::new(root)
        .hidden(!show_hidden)
        .git_ignore(true)
        .git_global(true)
        .git_exclude(true)
        .max_depth(Some(depth))
        .sort_by_file_name(|a, b| a.cmp(b))
        .build();

    for entry in walker.filter_map(|e| e.ok()) {
        if entry.depth() == 0 {
            continue;
        }
        let rel = entry
            .path()
            .strip_prefix(root)
            .unwrap_or(entry.path())
            .to_string_lossy();
        lines.push(rel.to_string());
    }

    lines.join("\n")
}

fn count_files_in_dir(dir: &Path) -> usize {
    WalkBuilder::new(dir)
        .hidden(false)
        .git_ignore(true)
        .max_depth(Some(5))
        .build()
        .filter_map(|e| e.ok())
        .filter(|e| e.file_type().is_some_and(|ft| ft.is_file()))
        .count()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn tree_savings_are_reasonable() {
        let dir = env!("CARGO_MANIFEST_DIR");
        let (output, original) = handle(dir, 3, false);
        let compact_tokens = count_tokens(&output);

        eprintln!("=== ctx_tree savings test ===");
        eprintln!("  original (raw) tokens: {original}");
        eprintln!("  compact tokens:        {compact_tokens}");
        eprintln!(
            "  savings:               {}",
            original.saturating_sub(compact_tokens)
        );

        assert!(
            original < 5000,
            "raw tree at depth 3 should be < 5000 tokens, got {original}"
        );
        assert!(original > 0, "raw tree should have some tokens");
        if original > compact_tokens {
            let ratio = (original - compact_tokens) as f64 / original as f64;
            eprintln!("  savings ratio:         {:.1}%", ratio * 100.0);
            assert!(
                ratio < 0.90,
                "savings ratio should be < 90% for same-depth comparison, got {:.1}%",
                ratio * 100.0
            );
        }
    }
}