Skip to main content

deepseek/agent/builtin_tools/
grep.rs

1use async_trait::async_trait;
2use serde_json::{json, Value};
3
4use crate::agent::tool::{Tool, ToolDefinition};
5
6const MAX_MATCHES: usize = 200;
7
8
9pub struct GrepTool;
10
11#[async_trait]
12impl Tool for GrepTool {
13    fn name(&self) -> &str {
14        "Grep"
15    }
16
17    fn read_only_hint(&self) -> bool {
18        true
19    }
20
21    fn definition(&self) -> ToolDefinition {
22        ToolDefinition {
23            name: self.name().to_string(),
24            description: "Search files for a regex pattern. Output is `path:line:match`, \
25                          one per line, capped at 200 matches."
26                .into(),
27            parameters: json!({
28                "type": "object",
29                "properties": {
30                    "pattern": { "type": "string", "description": "Rust regex." },
31                    "path":    { "type": "string", "description": "Root directory (default: cwd)." },
32                    "include": { "type": "string", "description": "Optional glob filter, e.g. `*.rs`." }
33                },
34                "required": ["pattern"]
35            }),
36        }
37    }
38
39    async fn call_json(&self, args: Value) -> Result<String, String> {
40        let pattern = args
41            .get("pattern")
42            .and_then(Value::as_str)
43            .ok_or_else(|| "Grep: missing string `pattern`".to_string())?
44            .to_string();
45        let root = args
46            .get("path")
47            .and_then(Value::as_str)
48            .map(String::from)
49            .unwrap_or_else(|| ".".into());
50        let include = args
51            .get("include")
52            .and_then(Value::as_str)
53            .map(String::from);
54
55        let result = tokio::task::spawn_blocking(move || -> Result<String, String> {
56            let re = regex::Regex::new(&pattern)
57                .map_err(|e| format!("Grep: bad regex `{pattern}`: {e}"))?;
58            let include_matcher = include
59                .map(|p| {
60                    globset::Glob::new(&p)
61                        .map(|g| g.compile_matcher())
62                        .map_err(|e| format!("Grep: bad include `{p}`: {e}"))
63                })
64                .transpose()?;
65
66            let mut out = String::new();
67            let mut matches = 0usize;
68            for entry in walkdir::WalkDir::new(&root)
69                .into_iter()
70                .filter_map(Result::ok)
71            {
72                if !entry.file_type().is_file() {
73                    continue;
74                }
75                let path = entry.path();
76                if let Some(m) = &include_matcher {
77                    let name = path.file_name().unwrap_or_default();
78                    if !m.is_match(name) {
79                        continue;
80                    }
81                }
82                let body = match std::fs::read_to_string(path) {
83                    Ok(s) => s,
84                    Err(_) => continue, // skip binary/unreadable
85                };
86                for (lineno, line) in body.lines().enumerate() {
87                    if re.is_match(line) {
88                        out.push_str(&format!(
89                            "{}:{}:{}\n",
90                            path.display(),
91                            lineno + 1,
92                            line
93                        ));
94                        matches += 1;
95                        if matches >= MAX_MATCHES {
96                            return Ok(out);
97                        }
98                    }
99                }
100            }
101            Ok(out)
102        })
103        .await
104        .map_err(|e| format!("Grep: task join error: {e}"))??;
105
106        Ok(result)
107    }
108}