lean-ctx 3.1.3

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 regex::Regex;
use std::sync::OnceLock;

static MIGRATION_STATUS_RE: OnceLock<Regex> = OnceLock::new();
static ROUTE_RE: OnceLock<Regex> = OnceLock::new();
static TEST_RESULT_RE: OnceLock<Regex> = OnceLock::new();
static PEST_RESULT_RE: OnceLock<Regex> = OnceLock::new();

fn migration_status_re() -> &'static Regex {
    MIGRATION_STATUS_RE.get_or_init(|| Regex::new(r"\|\s*(Ran|Pending)\s*\|\s*(.+?)\s*\|").unwrap())
}
fn route_re() -> &'static Regex {
    ROUTE_RE.get_or_init(|| {
        Regex::new(r"(GET|POST|PUT|PATCH|DELETE|ANY)\s*\|\s*(\S+)\s*\|\s*(\S+)").unwrap()
    })
}
fn test_result_re() -> &'static Regex {
    TEST_RESULT_RE
        .get_or_init(|| Regex::new(r"Tests:\s*(\d+)\s*passed(?:,\s*(\d+)\s*failed)?").unwrap())
}
fn pest_result_re() -> &'static Regex {
    PEST_RESULT_RE
        .get_or_init(|| Regex::new(r"(\d+)\s*passed.*?(\d+)\s*failed|(\d+)\s*passed").unwrap())
}

pub fn compress(command: &str, output: &str) -> Option<String> {
    let trimmed = output.trim();
    if trimmed.is_empty() {
        return Some("ok".to_string());
    }

    if command.contains("migrate") && command.contains("--status") {
        return Some(compress_migrate_status(trimmed));
    }
    if command.contains("migrate") {
        return Some(compress_migrate(trimmed));
    }
    if command.contains("test") {
        return Some(compress_test(trimmed));
    }
    if command.contains("route:list") {
        return Some(compress_routes(trimmed));
    }
    if command.contains("make:") {
        return Some(compress_make(trimmed));
    }
    if command.contains("queue:work") || command.contains("queue:listen") {
        return Some(compress_queue(trimmed));
    }
    if command.contains("tinker") {
        return Some(compress_tinker(trimmed));
    }

    Some(compact_lines(trimmed, 10))
}

fn compress_migrate(output: &str) -> String {
    let mut ran = 0u32;
    let mut errors = Vec::new();

    for line in output.lines() {
        let t = line.trim();
        if t.contains("Migrating:") || t.contains("DONE") {
            ran += 1;
        }
        if t.starts_with("SQLSTATE") || t.contains("ERROR") || t.contains("Exception") {
            errors.push(t.to_string());
        }
    }

    if !errors.is_empty() {
        return format!("migrate FAILED:\n{}", errors.join("\n"));
    }
    if ran > 0 {
        format!("migrated {ran} tables")
    } else if output.contains("Nothing to migrate") {
        "nothing to migrate".to_string()
    } else {
        compact_lines(output, 5)
    }
}

fn compress_migrate_status(output: &str) -> String {
    let statuses: Vec<String> = migration_status_re()
        .captures_iter(output)
        .map(|c| {
            let status = if &c[1] == "Ran" { "+" } else { "-" };
            format!("{} {}", status, c[2].trim())
        })
        .collect();

    if statuses.is_empty() {
        return compact_lines(output, 10);
    }

    let ran = statuses.iter().filter(|s| s.starts_with('+')).count();
    let pending = statuses.iter().filter(|s| s.starts_with('-')).count();
    let mut result = format!("{} ran, {} pending:", ran, pending);

    for s in statuses.iter().rev().take(10) {
        result.push_str(&format!("\n  {s}"));
    }
    if statuses.len() > 10 {
        result.push_str(&format!("\n  ... +{} more", statuses.len() - 10));
    }
    result
}

fn compress_test(output: &str) -> String {
    let mut passed = 0u32;
    let mut failed = 0u32;
    let mut failures = Vec::new();
    let mut time = String::new();

    for line in output.lines() {
        let t = line.trim();
        if let Some(caps) = test_result_re().captures(t) {
            passed = caps[1].parse().unwrap_or(0);
            failed = caps
                .get(2)
                .and_then(|m| m.as_str().parse().ok())
                .unwrap_or(0);
        }
        if let Some(caps) = pest_result_re().captures(t) {
            if let Some(p) = caps.get(3) {
                passed = p.as_str().parse().unwrap_or(0);
            } else {
                passed = caps[1].parse().unwrap_or(0);
                failed = caps[2].parse().unwrap_or(0);
            }
        }
        if t.starts_with("FAIL") || t.starts_with("") || t.starts_with("×") {
            failures.push(t.to_string());
        }
        if t.contains("Time:") || t.contains("Duration:") {
            time = t.to_string();
        }
    }

    let status = if failed > 0 { "FAIL" } else { "ok" };
    let mut result = format!("{status}: {passed} passed, {failed} failed");
    if !time.is_empty() {
        result.push_str(&format!(" ({})", time.trim()));
    }
    if !failures.is_empty() {
        result.push_str("\nfailed:");
        for f in failures.iter().take(10) {
            result.push_str(&format!("\n  {f}"));
        }
    }
    result
}

fn compress_routes(output: &str) -> String {
    let routes: Vec<String> = route_re()
        .captures_iter(output)
        .map(|c| format!("{} {}{}", &c[1], &c[2], &c[3]))
        .collect();

    if routes.is_empty() {
        return compact_lines(output, 15);
    }

    let mut result = format!("{} routes:", routes.len());
    for r in routes.iter().take(20) {
        result.push_str(&format!("\n  {r}"));
    }
    if routes.len() > 20 {
        result.push_str(&format!("\n  ... +{} more", routes.len() - 20));
    }
    result
}

fn compress_make(output: &str) -> String {
    let lines: Vec<&str> = output
        .lines()
        .filter(|l| {
            let t = l.trim();
            !t.is_empty() && !t.starts_with("INFO")
        })
        .collect();

    if lines.is_empty() {
        return "created".to_string();
    }

    let created = output
        .lines()
        .find(|l| l.contains("created successfully") || l.contains(".php"));

    if let Some(c) = created {
        c.trim().to_string()
    } else {
        compact_lines(output, 3)
    }
}

fn compress_queue(output: &str) -> String {
    let mut processed = 0u32;
    let mut failed = 0u32;
    let mut last_job = String::new();

    for line in output.lines() {
        let t = line.trim();
        if t.contains("Processed") || t.contains("[DONE]") {
            processed += 1;
            if let Some(job) = t.split_whitespace().last() {
                last_job = job.to_string();
            }
        }
        if t.contains("FAILED") || t.contains("[ERROR]") {
            failed += 1;
        }
    }

    if processed == 0 && failed == 0 {
        return compact_lines(output, 5);
    }

    let mut result = format!("queue: {processed} processed");
    if failed > 0 {
        result.push_str(&format!(", {failed} failed"));
    }
    if !last_job.is_empty() {
        result.push_str(&format!(" (last: {last_job})"));
    }
    result
}

fn compress_tinker(output: &str) -> String {
    let lines: Vec<&str> = output
        .lines()
        .filter(|l| {
            let t = l.trim();
            !t.is_empty()
                && !t.starts_with("Psy Shell")
                && !t.starts_with(">>>")
                && !t.starts_with("...")
        })
        .collect();

    if lines.is_empty() {
        return "tinker (no output)".to_string();
    }
    if lines.len() <= 10 {
        return lines.join("\n");
    }
    format!(
        "{}\n... ({} more lines)",
        lines[..8].join("\n"),
        lines.len() - 8
    )
}

fn compact_lines(text: &str, max: usize) -> String {
    let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
    if lines.len() <= max {
        return lines.join("\n");
    }
    format!(
        "{}\n... ({} more lines)",
        lines[..max].join("\n"),
        lines.len() - max
    )
}

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

    #[test]
    fn artisan_migrate_success() {
        let output =
            "Migrating: 2026_01_01_create_users_table\nMigrating: 2026_01_02_create_posts_table";
        let result = compress("php artisan migrate", output).unwrap();
        assert!(result.contains("migrated 2"), "shows count: {result}");
    }

    #[test]
    fn artisan_migrate_nothing() {
        let output = "Nothing to migrate.";
        let result = compress("php artisan migrate", output).unwrap();
        assert!(result.contains("nothing to migrate"), "{result}");
    }

    #[test]
    fn artisan_test_success() {
        let output = "  PASS  Tests\\Unit\\UserTest\n  ✓ it can create user\n  ✓ it validates email\n\n  Tests:  2 passed\n  Time:   0.45s";
        let result = compress("php artisan test", output).unwrap();
        assert!(result.contains("ok: 2 passed"), "{result}");
    }

    #[test]
    fn artisan_test_failure() {
        let output = "  FAIL  Tests\\Unit\\UserTest\n  ✕ it validates email\n\n  Tests:  1 passed, 1 failed\n  Time:   0.52s";
        let result = compress("php artisan test", output).unwrap();
        assert!(result.contains("FAIL: 1 passed, 1 failed"), "{result}");
    }

    #[test]
    fn artisan_make_model() {
        let output = "\n   INFO  Model [app/Models/Invoice.php] created successfully.\n";
        let result = compress("php artisan make:model Invoice", output).unwrap();
        assert!(
            result.contains("Invoice") || result.contains("created"),
            "{result}"
        );
    }

    #[test]
    fn pest_test_output() {
        let output = "  PASS  Tests\\Feature\\AuthTest\n  ✓ login works\n  ✓ register works\n\n  3 passed (0.8s)";
        let result = compress("./vendor/bin/pest", output).unwrap();
        assert!(result.contains("3 passed"), "{result}");
    }

    #[test]
    fn route_list_compression() {
        let output = "  GET|HEAD  /api/users ................. UserController@index\n  POST      /api/users ................. UserController@store\n  GET|HEAD  /api/users/{user} .......... UserController@show\n  PUT|PATCH /api/users/{user} .......... UserController@update\n  DELETE    /api/users/{user} .......... UserController@destroy";
        let result = compress("php artisan route:list", output).unwrap();
        assert!(result.len() < output.len(), "should compress");
    }
}