magi-code 0.64.0

Repository-aware CLI coding agent for terminal work
Documentation
mod engine;
mod matcher;
mod reader;

use super::{
    ToolResult, ToolResultDisplay, ToolRuntime,
    args::GrepArgs,
    contract::{metadata_key as meta, tool_name},
};
use crate::agent::cancellation::AgentCancellation;
use serde_json::json;
use std::path::Path;

impl ToolRuntime {
    pub(super) fn grep(
        &self,
        args: GrepArgs,
        cancellation: &AgentCancellation,
    ) -> anyhow::Result<ToolResult> {
        let path_arg = args.path.clone().unwrap_or_else(|| ".".to_string());
        let path = self.resolve_existing_path(
            &path_arg,
            super::fs::ExistingPathPolicy::grep(self.grep_absolute_paths),
        )?;
        cancellation.check()?;
        let scan = engine::run(engine::GrepRequest {
            cwd: &self.cwd_canonical,
            walker: &self.workspace_walker,
            pattern: &args.pattern,
            root: &path,
            raw_limit: args.limit,
            context: args.context.unwrap_or(0),
            cancellation,
        })?;

        Ok(ToolResult {
            tool_name: tool_name::GREP.to_string(),
            success: !scan.timed_out,
            content: scan.matches.join("\n"),
            metadata: json!({
                (meta::ENGINE): "native",
                (meta::MATCHES_RETURNED): scan.matches.len(),
                (meta::TRUNCATED): scan.truncated,
                (meta::TIMED_OUT): scan.timed_out,
                (meta::FILES_SCANNED): scan.files_scanned,
                (meta::BYTES_SCANNED): scan.bytes_scanned,
                (meta::LIMIT_REASON): scan.limit_reason,
            }),
            display: ToolResultDisplay::default(),
        })
    }
}

pub(super) fn push_grep_match(
    matches: &mut Vec<String>,
    cwd: &Path,
    file: &Path,
    line_no: usize,
    line: &str,
) {
    matches.push(format!(
        "{}:{}:{}",
        file.strip_prefix(cwd).unwrap_or(file).display(),
        line_no,
        line.trim_end_matches(['\r', '\n'])
    ));
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        agent::cancellation::AgentCancellation,
        tools::{ToolRuntime, args::ListFilesArgs},
    };
    use std::fs;

    fn grep(
        runtime: &ToolRuntime,
        pattern: &str,
        limit: Option<usize>,
        context: Option<usize>,
    ) -> ToolResult {
        runtime
            .grep(
                GrepArgs {
                    pattern: pattern.to_string(),
                    path: Some(".".to_string()),
                    limit,
                    context,
                },
                &AgentCancellation::default(),
            )
            .unwrap()
    }

    #[test]
    fn native_grep_excludes_gitignored_files_without_contaminating_list_files_cache() {
        let temp = tempfile::TempDir::new().unwrap();
        fs::create_dir(temp.path().join(".git")).unwrap();
        fs::write(temp.path().join(".gitignore"), "ignored.txt\n").unwrap();
        fs::write(temp.path().join("ignored.txt"), "needle").unwrap();
        fs::write(temp.path().join("visible.txt"), "needle").unwrap();
        let runtime = ToolRuntime::new(temp.path()).unwrap();

        let result = grep(&runtime, "needle", Some(10), None);
        let list_result = runtime
            .list_files(ListFilesArgs {
                path: ".".to_string(),
                include_directories: false,
            })
            .unwrap();

        assert!(result.content.contains("visible.txt:1:needle"));
        assert!(!result.content.contains("ignored.txt"));
        assert!(list_result.content.contains("ignored.txt"));
    }

    #[test]
    fn native_metadata_and_output_shape_are_preserved() {
        let temp = tempfile::TempDir::new().unwrap();
        fs::write(temp.path().join("a.txt"), "hay\nneedle\n").unwrap();
        let runtime = ToolRuntime::new(temp.path()).unwrap();

        let result = grep(&runtime, "needle", Some(5), None);

        assert_eq!(result.tool_name, "grep");
        assert_eq!(result.content, "a.txt:2:needle");
        assert_eq!(result.metadata["engine"], "native");
        assert!(result.metadata.get("exit_code").is_none());
        assert!(result.metadata.get("stderr").is_none());
        assert!(result.metadata.get("cleanup_warning").is_none());
    }

    #[test]
    fn native_context_lines_use_same_content_shape() {
        let temp = tempfile::TempDir::new().unwrap();
        fs::write(temp.path().join("a.txt"), "before\nneedle\nafter\n").unwrap();
        let runtime = ToolRuntime::new(temp.path()).unwrap();

        let result = grep(&runtime, "needle", Some(5), Some(1));

        assert_eq!(
            result.content,
            "a.txt:1:before\na.txt:2:needle\na.txt:3:after"
        );
    }

    #[test]
    fn default_limit_caps_parallel_results_at_100() {
        let temp = tempfile::TempDir::new().unwrap();
        for index in 0..120 {
            fs::write(temp.path().join(format!("{index:03}.txt")), "needle\n").unwrap();
        }
        let runtime = ToolRuntime::new(temp.path()).unwrap();

        let result = grep(&runtime, "needle", None, None);

        assert_eq!(result.content.lines().count(), 100);
        assert_eq!(result.metadata["truncated"], true);
        assert_eq!(result.metadata["limit_reason"], "matches");
    }

    #[test]
    fn line_anchors_are_line_oriented() {
        let temp = tempfile::TempDir::new().unwrap();
        fs::write(temp.path().join("a.txt"), "xneedle\nneedle\nneedleX\n").unwrap();
        let runtime = ToolRuntime::new(temp.path()).unwrap();

        let result = grep(&runtime, "^needle$", Some(10), None);

        assert_eq!(result.content, "a.txt:2:needle");
    }
}