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