agent-file-tools 0.25.2

Agent File Tools — tree-sitter powered code analysis for AI agents
Documentation
use crate::compress::generic::GenericCompressor;
use crate::compress::Compressor;

pub struct NpmCompressor;

impl Compressor for NpmCompressor {
    fn matches(&self, command: &str) -> bool {
        command
            .split_whitespace()
            .next()
            .is_some_and(|head| head == "npm")
    }

    fn compress(&self, command: &str, output: &str) -> String {
        match npm_subcommand(command).as_deref() {
            Some("install" | "i" | "ci") => compress_install(output),
            Some("run" | "test") => GenericCompressor::compress_output(output),
            Some("audit") => compress_audit(output),
            Some("publish") => compress_install(output),
            _ => GenericCompressor::compress_output(output),
        }
    }
}

fn npm_subcommand(command: &str) -> Option<String> {
    command
        .split_whitespace()
        .skip_while(|token| *token != "npm")
        .skip(1)
        .find(|token| !token.starts_with('-'))
        .map(ToString::to_string)
}

fn compress_install(output: &str) -> String {
    let lines: Vec<&str> = output.lines().collect();
    let has_audit = lines
        .iter()
        .any(|line| line.trim_start().starts_with("audited "));
    let has_final_summary = lines.iter().any(|line| is_final_summary(line));
    let tail_start = lines.len().saturating_sub(5);
    let mut result = Vec::new();
    let mut deprecated_seen = 0usize;
    let mut deprecated_omitted = 0usize;

    for (index, line) in lines.iter().enumerate() {
        if is_npm_progress(line) {
            continue;
        }
        if line.trim_start().starts_with("npm WARN deprecated ") {
            deprecated_seen += 1;
            if deprecated_seen <= 5 {
                result.push((*line).to_string());
            } else {
                deprecated_omitted += 1;
            }
            continue;
        }
        if has_audit && has_final_summary && line.trim_start().starts_with("added ") {
            continue;
        }
        if index >= tail_start
            || line.trim_start().starts_with("npm ERR!")
            || is_final_summary(line)
        {
            result.push((*line).to_string());
        }
    }

    if deprecated_omitted > 0 {
        insert_after_deprecations(
            &mut result,
            format!("... and {deprecated_omitted} more deprecation warnings"),
        );
    }

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

fn is_npm_progress(line: &str) -> bool {
    let trimmed = line.trim_start();
    trimmed.starts_with("npm http fetch")
        || trimmed.starts_with("npm timing")
        || trimmed.starts_with("npm verb")
}

fn is_final_summary(line: &str) -> bool {
    let trimmed = line.trim_start();
    trimmed.starts_with("audited ")
        || trimmed.starts_with("found ")
        || trimmed.contains(" vulnerabilities")
        || trimmed.contains(" packages are looking for funding")
        || trimmed.starts_with("published ")
        || trimmed.starts_with("+ ")
}

fn insert_after_deprecations(result: &mut Vec<String>, summary: String) {
    let position = result
        .iter()
        .rposition(|line| line.trim_start().starts_with("npm WARN deprecated "))
        .map_or(0, |index| index + 1);
    result.insert(position, summary);
}

fn compress_audit(output: &str) -> String {
    let mut result = Vec::new();
    let mut vulnerabilities = 0usize;
    let mut omitted = 0usize;

    for line in output.lines() {
        if is_audit_vulnerability_line(line) {
            vulnerabilities += 1;
            if vulnerabilities <= 10 {
                result.push(line.to_string());
            } else {
                omitted += 1;
            }
            continue;
        }
        if is_audit_summary(line) || vulnerabilities <= 10 {
            result.push(line.to_string());
        }
    }

    if omitted > 0 {
        result.push(format!("... and {omitted} more vulnerabilities"));
    }

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

fn is_audit_vulnerability_line(line: &str) -> bool {
    let trimmed = line.trim_start();
    trimmed.starts_with("VULN ") || trimmed.starts_with("# ") && trimmed.contains(" - ")
}

fn is_audit_summary(line: &str) -> bool {
    let trimmed = line.trim_start();
    trimmed.starts_with("found ")
        || trimmed.starts_with("npm audit fix")
        || trimmed.contains(" vulnerabilities")
}

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