agent-file-tools 0.19.0

Agent File Tools — tree-sitter powered code analysis for AI agents
Documentation
use std::collections::BTreeMap;

use crate::compress::Compressor;

pub struct TscCompressor;

impl Compressor for TscCompressor {
    fn matches(&self, command: &str) -> bool {
        command.split_whitespace().any(|token| token == "tsc")
    }

    fn compress(&self, _command: &str, output: &str) -> String {
        compress_tsc(output)
    }
}

fn compress_tsc(output: &str) -> String {
    let lines: Vec<&str> = output.lines().collect();
    let error_lines: Vec<&str> = lines
        .iter()
        .copied()
        .filter(|line| is_tsc_error_line(line))
        .collect();

    if error_lines.is_empty() {
        return "No errors. [cmpaft]".to_string();
    }

    let mut by_file: BTreeMap<String, Vec<String>> = BTreeMap::new();
    let mut ungrouped = Vec::new();
    for line in error_lines {
        if let Some(file) = error_file(line) {
            by_file.entry(file).or_default().push(line.to_string());
        } else {
            ungrouped.push(line.to_string());
        }
    }

    let mut result = Vec::new();
    let mut emitted_files = 0usize;
    for errors in by_file.values() {
        if emitted_files >= 10 && by_file.len() > 20 {
            continue;
        }
        emitted_files += 1;
        if errors.len() > 30 {
            result.extend(errors.iter().take(10).cloned());
            result.push(format!(
                "... and {} more errors in this file",
                errors.len() - 10
            ));
        } else {
            result.extend(errors.iter().cloned());
        }
    }

    result.extend(ungrouped);
    if by_file.len() > 20 {
        result.push(format!(
            "... and {} more files with errors",
            by_file.len() - emitted_files
        ));
    }
    if let Some(summary) = lines.iter().rev().find(|line| is_tsc_summary(line)) {
        result.push((*summary).to_string());
    }

    trim_trailing_lines(&result.join("\n"))
}

fn is_tsc_error_line(line: &str) -> bool {
    line.contains(": error TS") && error_file(line).is_some()
}

fn error_file(line: &str) -> Option<String> {
    let marker = line.find("): error TS")?;
    let before = &line[..marker];
    let open = before.rfind('(')?;
    if before[open + 1..]
        .split(',')
        .all(|part| !part.is_empty() && part.chars().all(|char| char.is_ascii_digit()))
    {
        Some(before[..open].to_string())
    } else {
        None
    }
}

fn is_tsc_summary(line: &str) -> bool {
    let trimmed = line.trim();
    trimmed.starts_with("Found ") && trimmed.contains(" errors") && trimmed.contains(" files")
}

fn trim_trailing_lines(input: &str) -> String {
    input
        .lines()
        .map(str::trim_end)
        .collect::<Vec<_>>()
        .join("\n")
}