Skip to main content

opi_coding_agent/tool/
grep.rs

1use std::future::Future;
2use std::path::PathBuf;
3use std::pin::Pin;
4
5use opi_agent::tool::{ExecutionMode, Tool, ToolError, ToolResult};
6use opi_ai::message::{OutputContent, ToolDef};
7use schemars::JsonSchema;
8use serde::Deserialize;
9use tokio_util::sync::CancellationToken;
10
11#[derive(Debug, Deserialize, JsonSchema)]
12pub struct GrepArgs {
13    /// Regex pattern to search for.
14    pub pattern: String,
15}
16
17pub struct GrepTool {
18    workspace_root: PathBuf,
19    schema: serde_json::Value,
20}
21
22impl GrepTool {
23    pub fn new(workspace_root: PathBuf) -> Self {
24        let schema = schemars::schema_for!(GrepArgs);
25        Self {
26            workspace_root,
27            schema: serde_json::to_value(&schema).unwrap_or_default(),
28        }
29    }
30}
31
32impl Tool for GrepTool {
33    fn definition(&self) -> ToolDef {
34        ToolDef {
35            name: "grep".into(),
36            description: "Gitignore-aware regex search over file contents.".into(),
37            input_schema: self.schema.clone(),
38        }
39    }
40
41    fn execute(
42        &self,
43        _call_id: &str,
44        arguments: serde_json::Value,
45        _signal: CancellationToken,
46        _on_update: Option<opi_agent::tool::UpdateCallback>,
47    ) -> Pin<Box<dyn Future<Output = Result<ToolResult, ToolError>> + Send>> {
48        let args: GrepArgs = match serde_json::from_value(arguments) {
49            Ok(a) => a,
50            Err(e) => {
51                return Box::pin(async move {
52                    Ok(ToolResult {
53                        content: vec![OutputContent::Text {
54                            text: format!("invalid arguments: {e}"),
55                        }],
56                        details: None,
57                        is_error: true,
58                        terminate: false,
59                    })
60                });
61            }
62        };
63        let workspace_root = self.workspace_root.clone();
64        let pattern = args.pattern;
65        Box::pin(async move {
66            let re = match regex::Regex::new(&pattern) {
67                Ok(r) => r,
68                Err(e) => {
69                    return Ok(ToolResult {
70                        content: vec![OutputContent::Text {
71                            text: format!("invalid regex pattern: {e}"),
72                        }],
73                        details: None,
74                        is_error: true,
75                        terminate: false,
76                    });
77                }
78            };
79
80            let mut matches = Vec::new();
81            let mut builder = ignore::WalkBuilder::new(&workspace_root);
82            builder
83                .hidden(false)
84                .git_ignore(true)
85                .git_global(false)
86                .git_exclude(false)
87                .add_custom_ignore_filename(".gitignore");
88            let walker = builder.build();
89
90            for entry in walker.flatten() {
91                if entry.file_type().is_some_and(|ft| ft.is_file()) {
92                    let path = entry.path();
93                    let content = match std::fs::read_to_string(path) {
94                        Ok(c) => c,
95                        Err(_) => continue,
96                    };
97                    for line in content.lines() {
98                        if re.is_match(line) {
99                            let relative = path.strip_prefix(&workspace_root).unwrap_or(path);
100                            matches.push(format!("{}: {}", relative.display(), line));
101                        }
102                    }
103                }
104            }
105
106            let text = matches.join("\n");
107            let details = serde_json::json!({
108                "workspace_root": workspace_root.to_string_lossy(),
109                "pattern": pattern,
110                "match_count": matches.len(),
111            });
112
113            Ok(ToolResult {
114                content: vec![OutputContent::Text { text }],
115                details: Some(details),
116                is_error: false,
117                terminate: false,
118            })
119        })
120    }
121
122    fn execution_mode(&self) -> ExecutionMode {
123        ExecutionMode::Parallel
124    }
125}