- 1
use std::path::Path; - 2
- 3
use async_trait::async_trait; - 4
use globset::{Glob, GlobSetBuilder}; - 5
use regex::Regex; - 6
use serde_json::Value; - 7
use walkdir::WalkDir; - 8
- 9
use crate::{Tool, ToolContext, ToolOutput}; - 10
- 11
const MAX_MATCH_LINES: usize = 300; - 12
- 13
pub struct GrepTool; - 14
- 15
#[async_trait] - 16
impl Tool for GrepTool { - 17
fn name(&self) -> &str { - 18
"grep" - 19
} - 20
- 21
fn serves(&self) -> &'static [&'static str] { - 22
&["filesystem"] - 23
} - 24
- 25
fn always_loaded(&self) -> bool { - 26
true - 27
} - 28
- 29
fn description(&self) -> &str { - 30
"Search file contents with a regular expression. Returns file:line:text matches. Use include to filter by glob (e.g. \"*.rs\")." - 31
} - 32
- 33
fn schema(&self) -> Value { - 34
serde_json::json!({ - 35
"type": "object", - 36
"properties": { - 37
"pattern": {"type": "string", "description": "Regular expression to search for"}, - 38
"path": {"type": "string", "description": "File or directory to search (default: cwd)"}, - 39
"include": {"type": "string", "description": "Glob filter for filenames, e.g. *.ts"} - 40
}, - 41
"required": ["pattern"] - 42
}) - 43
} - 44
- 45
fn claims(&self, _args: &Value) -> crate::ResourceClaims { - 46
crate::ResourceClaims { - 47
exclusive: false, - 48
read_only: true, - 49
paths: Vec::new(), - 50
} - 51
} - 52
- 53
async fn execute(&self, args: &Value, ctx: &ToolContext) -> ToolOutput { - 54
let Some(pattern) = args.get("pattern").and_then(|p| p.as_str()) else { - 55
return ToolOutput::error("missing required parameter: pattern"); - 56
}; - 57
let re = match Regex::new(pattern) { - 58
Ok(r) => r, - 59
Err(e) => return ToolOutput::error(format!("invalid regex: {e}")), - 60
}; - 61
let base = match args.get("path").and_then(|p| p.as_str()) { - 62
Some(p) => ctx.resolve(Path::new(p)), - 63
None => ctx.cwd.clone(), - 64
}; - 65
let include = args.get("include").and_then(|i| i.as_str()); - 66
let include_set = match include { - 67
Some(glob_str) => { - 68
let g = match Glob::new(glob_str) { - 69
Ok(g) => g, - 70
Err(e) => return ToolOutput::error(format!("invalid include: {e}")), - 71
}; - 72
match GlobSetBuilder::new().add(g).build() { - 73
Ok(gs) => Some(gs), - 74
Err(e) => return ToolOutput::error(format!("invalid include: {e}")), - 75
} - 76
} - 77
None => None, - 78
}; - 79
- 80
if !base.exists() { - 81
return ToolOutput::error(format!("path not found: {}", base.display())); - 82
} - 83
- 84
// File IO + regex over whole trees is blocking; keep it off the - 85
// async workers that carry SSE and TUI delivery. - 86
let cwd = ctx.cwd.clone(); - 87
let walked = - 88
tokio::task::spawn_blocking(move || search_tree(&base, &re, include_set, &cwd)).await; - 89
let out = match walked { - 90
Ok(out) => out, - 91
Err(e) => return ToolOutput::error(format!("grep task failed: {e}")), - 92
}; - 93
- 94
match out { - 95
SearchOutcome::Empty => ToolOutput::ok("no matches"), - 96
SearchOutcome::Hits(text) => ToolOutput::ok(text), - 97
} - 98
} - 99
} - 100
- 101
enum SearchOutcome { - 102
Empty, - 103
Hits(String), - 104
} - 105
- 106
fn search_file_bytes(path: &Path, root: &Path, re: &Regex, out: &mut String) -> usize { - 107
let Ok(bytes) = std::fs::read(path) else { - 108
return 0; - 109
}; - 110
if bytes.iter().take(8192).any(|&b| b == 0) { - 111
return 0; - 112
} - 113
let text = String::from_utf8_lossy(&bytes); - 114
let mut hits = 0usize; - 115
for (i, line) in text.lines().enumerate() { - 116
if re.is_match(line) { - 117
out.push_str(&format!( - 118
"{}:{}:{line}\n", - 119
path.strip_prefix(root).unwrap_or(path).display(), - 120
i + 1 - 121
)); - 122
hits += 1; - 123
} - 124
} - 125
hits - 126
} - 127
- 128
fn search_tree( - 129
base: &Path, - 130
re: &Regex, - 131
include_set: Option<globset::GlobSet>, - 132
cwd: &Path, - 133
) -> SearchOutcome { - 134
let mut out = String::new(); - 135
let mut count = 0usize; - 136
- 137
if base.is_file() { - 138
count = search_file_bytes(base, cwd, re, &mut out); - 139
return if count == 0 { - 140
SearchOutcome::Empty - 141
} else { - 142
SearchOutcome::Hits(out) - 143
}; - 144
} - 145
- 146
'outer: for entry in WalkDir::new(base) - 147
.follow_links(false) - 148
.into_iter() - 149
.filter_entry(|e| !crate::glob::is_ignored(e.path())) - 150
.flatten() - 151
{ - 152
if !entry.file_type().is_file() { - 153
continue; - 154
} - 155
if let Some(gs) = &include_set { - 156
let name_ok = entry - 157
.file_name() - 158
.to_str() - 159
.map(|n| gs.is_match(n)) - 160
.unwrap_or(false); - 161
if !name_ok { - 162
continue; - 163
} - 164
} - 165
count += search_file_bytes(entry.path(), cwd, re, &mut out); - 166
if count >= MAX_MATCH_LINES { - 167
out.push_str("\n[match limit reached]"); - 168
break 'outer; - 169
} - 170
} - 171
- 172
if count == 0 { - 173
SearchOutcome::Empty - 174
} else { - 175
SearchOutcome::Hits(out) - 176
} - 177
} - 178
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.