agent-file-tools 0.36.0

Agent File Tools — tree-sitter powered code analysis for AI agents
Documentation
use std::path::Path;
use std::time::Instant;

use crate::commands::callgraph_store_adapter::{
    building_response, callers_result, store_error_response, unavailable_response,
};
use crate::context::{AppContext, CallgraphStoreAccess};
use crate::protocol::{RawRequest, Response};
use crate::{slog_info, slog_warn};

/// Handle a `callers` request.
pub fn handle_callers(req: &RawRequest, ctx: &AppContext) -> Response {
    let file = match req.params.get("file").and_then(|v| v.as_str()) {
        Some(f) => f,
        None => {
            return Response::error(
                &req.id,
                "invalid_request",
                "callers: missing required param 'file'",
            );
        }
    };

    let symbol = match req.params.get("symbol").and_then(|v| v.as_str()) {
        Some(s) => s,
        None => {
            return Response::error(
                &req.id,
                "invalid_request",
                "callers: missing required param 'symbol'",
            );
        }
    };

    let depth = req
        .params
        .get("depth")
        .and_then(|v| v.as_u64())
        .unwrap_or(1)
        .min(100) as usize;

    let file_path = match ctx.validate_path(&req.id, Path::new(file)) {
        Ok(path) => path,
        Err(resp) => return resp,
    };

    let project_root = ctx.config().project_root.clone();
    if let Some(project_root) = project_root {
        let canonical_root = std::fs::canonicalize(&project_root).unwrap_or(project_root.clone());
        let input_for_resolution = if file_path.is_relative() {
            project_root.join(&file_path)
        } else {
            file_path.clone()
        };
        let canonical_input =
            std::fs::canonicalize(&input_for_resolution).unwrap_or(input_for_resolution);
        if !canonical_input.starts_with(&canonical_root) {
            return Response::error(
                &req.id,
                "path_outside_project_root",
                format!(
                    "Callgraph operations require paths inside project_root. Got: {} (project_root: {})",
                    file_path.display(),
                    project_root.display(),
                ),
            );
        }
    }

    let store = match ctx.callgraph_store_for_ops() {
        CallgraphStoreAccess::Ready(store) => store,
        CallgraphStoreAccess::Building => return building_response(&req.id, "callers"),
        CallgraphStoreAccess::Unavailable => {
            return unavailable_response(&req.id, "callers", ctx.is_worktree_bridge())
        }
        CallgraphStoreAccess::Error(error) => {
            return store_error_response(&req.id, "callers", error)
        }
    };

    let started = Instant::now();
    let outcome = callers_result(&store, &file_path, symbol, depth);
    let elapsed_ms = started.elapsed().as_millis();

    match outcome {
        Ok(result) => {
            slog_info!(
                "callers: '{}' in {} → {} sites in {}ms",
                result.symbol,
                file_path.display(),
                result.total_callers,
                elapsed_ms
            );
            let result_json = serde_json::to_value(&result).unwrap_or_default();
            Response::success(&req.id, result_json)
        }
        Err(error) => {
            slog_warn!(
                "callers: '{}' failed after {}ms: {}",
                symbol,
                elapsed_ms,
                error
            );
            store_error_response(&req.id, "callers", error)
        }
    }
}