lean-ctx 3.5.16

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;

pub fn compress(output: &str) -> Option<String> {
    let lines: Vec<&str> = output.lines().filter(|l| !l.trim().is_empty()).collect();
    if lines.len() < 5 {
        return None;
    }

    let mut by_dir: HashMap<String, Vec<String>> = HashMap::new();
    let mut total_files = 0usize;

    for line in &lines {
        let path = line.trim();
        if should_skip(path) {
            continue;
        }

        total_files += 1;

        if let Some(slash_pos) = path.rfind('/') {
            let dir = &path[..slash_pos];
            let file = &path[slash_pos + 1..];
            by_dir
                .entry(dir.to_string())
                .or_default()
                .push(file.to_string());
        } else {
            by_dir
                .entry(".".to_string())
                .or_default()
                .push(path.to_string());
        }
    }

    if total_files == 0 {
        return None;
    }

    let mut result = format!("{total_files}F {}D:\n", by_dir.len());
    let mut sorted_dirs: Vec<_> = by_dir.iter().collect();
    sorted_dirs.sort_by_key(|(dir, _)| (*dir).clone());

    for (dir, files) in &sorted_dirs {
        result.push_str(&format!("\n{dir}/"));
        let show: Vec<_> = files.iter().take(10).collect();
        let mut line_buf = String::new();
        for f in &show {
            if line_buf.len() + f.len() + 1 > 60 {
                result.push_str(&format!("\n  {line_buf}"));
                line_buf.clear();
            }
            if !line_buf.is_empty() {
                line_buf.push(' ');
            }
            line_buf.push_str(f);
        }
        if !line_buf.is_empty() {
            result.push_str(&format!("\n  {line_buf}"));
        }
        if files.len() > 10 {
            result.push_str(&format!("\n  ... +{} more", files.len() - 10));
        }
    }

    Some(result)
}

fn should_skip(path: &str) -> bool {
    const SKIP: &[&str] = &[
        "node_modules/",
        ".git/",
        "target/debug/",
        "target/release/",
        "__pycache__/",
        ".svelte-kit/",
        ".next/",
        "dist/",
        ".DS_Store",
    ];
    SKIP.iter().any(|d| path.contains(d))
}

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

    #[test]
    fn groups_files_by_directory() {
        let output = "src/main.rs\nsrc/lib.rs\nsrc/util/helpers.rs\nsrc/util/math.rs\ntests/integration.rs\n";
        let result = compress(output).unwrap();
        assert!(result.contains("5F"), "should count 5 files");
        assert!(result.contains("src/"), "should group by src");
        assert!(result.contains("tests/"), "should group by tests");
    }

    #[test]
    fn skips_noisy_dirs() {
        let output = "node_modules/foo/bar.js\nsrc/a.rs\nsrc/b.rs\nsrc/c.rs\nsrc/d.rs\nsrc/e.rs\n";
        let result = compress(output).unwrap();
        assert!(!result.contains("node_modules"), "should skip node_modules");
    }

    #[test]
    fn too_few_lines_returns_none() {
        assert!(compress("a.rs\nb.rs\n").is_none());
    }
}