opi_coding_agent/tool/
grep.rs1use 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 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}