Skip to main content

pi_agent/tools/
grep.rs

1use async_trait::async_trait;
2use ignore::WalkBuilder;
3use serde_json::{json, Value};
4use std::fs;
5
6use crate::types::{AgentTool, AgentToolResult};
7
8pub struct GrepTool;
9
10#[async_trait]
11impl AgentTool for GrepTool {
12    fn name(&self) -> &str {
13        "grep"
14    }
15    fn description(&self) -> &str {
16        "Search file contents under a directory for a fixed substring. Honors .gitignore by default."
17    }
18    fn parameters(&self) -> Value {
19        json!({
20            "type": "object",
21            "properties": {
22                "pattern": {"type": "string", "description": "Substring to search for"},
23                "path": {"type": "string", "description": "Directory to search (default: cwd)"},
24                "max_matches": {"type": "integer", "default": 200}
25            },
26            "required": ["pattern"]
27        })
28    }
29    async fn execute(&self, _id: &str, args: Value) -> Result<AgentToolResult, String> {
30        let pattern = args
31            .get("pattern")
32            .and_then(|v| v.as_str())
33            .ok_or("missing 'pattern'")?
34            .to_string();
35        let path = args
36            .get("path")
37            .and_then(|v| v.as_str())
38            .unwrap_or(".")
39            .to_string();
40        let max = args
41            .get("max_matches")
42            .and_then(|v| v.as_u64())
43            .unwrap_or(200) as usize;
44
45        let result = tokio::task::spawn_blocking(move || -> Result<String, String> {
46            let mut buf = String::new();
47            let mut hits = 0usize;
48            let walker = WalkBuilder::new(&path).follow_links(false).build();
49            for entry in walker.flatten() {
50                if hits >= max {
51                    buf.push_str(&format!("... (truncated at {max} matches)\n"));
52                    break;
53                }
54                let p = entry.path();
55                if !p.is_file() {
56                    continue;
57                }
58                let text = match fs::read_to_string(p) {
59                    Ok(t) => t,
60                    Err(_) => continue, // skip binary or unreadable
61                };
62                for (i, line) in text.lines().enumerate() {
63                    if line.contains(&pattern) {
64                        buf.push_str(&format!("{}:{}:{}\n", p.display(), i + 1, line));
65                        hits += 1;
66                        if hits >= max {
67                            break;
68                        }
69                    }
70                }
71            }
72            Ok(buf)
73        })
74        .await
75        .map_err(|e| e.to_string())??;
76
77        Ok(AgentToolResult::text(if result.is_empty() {
78            "(no matches)".to_string()
79        } else {
80            result
81        }))
82    }
83}