- 1
use std::path::Path; - 2
- 3
use async_trait::async_trait; - 4
use globset::{GlobBuilder, GlobSetBuilder}; - 5
use serde_json::Value; - 6
use walkdir::WalkDir; - 7
- 8
use crate::{Tool, ToolContext, ToolOutput}; - 9
- 10
const MAX_MATCHES: usize = 500; - 11
- 12
pub struct GlobTool; - 13
- 14
#[async_trait] - 15
impl Tool for GlobTool { - 16
fn name(&self) -> &str { - 17
"glob" - 18
} - 19
- 20
fn serves(&self) -> &'static [&'static str] { - 21
&["filesystem"] - 22
} - 23
- 24
fn always_loaded(&self) -> bool { - 25
true - 26
} - 27
- 28
fn description(&self) -> &str { - 29
"Find files matching a glob pattern (supports **). Returns matching paths relative to the search directory." - 30
} - 31
- 32
fn schema(&self) -> Value { - 33
serde_json::json!({ - 34
"type": "object", - 35
"properties": { - 36
"pattern": {"type": "string", "description": "Glob pattern, e.g. src/**/*.rs"}, - 37
"path": {"type": "string", "description": "Directory to search (default: cwd)"} - 38
}, - 39
"required": ["pattern"] - 40
}) - 41
} - 42
- 43
fn claims(&self, _args: &Value) -> crate::ResourceClaims { - 44
crate::ResourceClaims { - 45
exclusive: false, - 46
read_only: true, - 47
paths: Vec::new(), - 48
} - 49
} - 50
- 51
async fn execute(&self, args: &Value, ctx: &ToolContext) -> ToolOutput { - 52
let Some(pattern) = args.get("pattern").and_then(|p| p.as_str()) else { - 53
return ToolOutput::error("missing required parameter: pattern"); - 54
}; - 55
let base = match args.get("path").and_then(|p| p.as_str()) { - 56
Some(p) => ctx.resolve(Path::new(p)), - 57
None => ctx.cwd.clone(), - 58
}; - 59
if !base.is_dir() { - 60
return ToolOutput::error(format!("not a directory: {}", base.display())); - 61
} - 62
- 63
let glob = match GlobBuilder::new(pattern).literal_separator(true).build() { - 64
Ok(g) => g, - 65
Err(e) => return ToolOutput::error(format!("invalid pattern: {e}")), - 66
}; - 67
let gs = match GlobSetBuilder::new().add(glob).build() { - 68
Ok(gs) => gs, - 69
Err(e) => return ToolOutput::error(format!("invalid pattern: {e}")), - 70
}; - 71
- 72
// Directory walks are blocking; keep them off the async workers. - 73
let walk = tokio::task::spawn_blocking(move || { - 74
let mut matches = Vec::new(); - 75
for entry in WalkDir::new(&base) - 76
.follow_links(false) - 77
.into_iter() - 78
.filter_entry(|e| !is_ignored(e.path())) - 79
.flatten() - 80
{ - 81
if !entry.file_type().is_file() { - 82
continue; - 83
} - 84
let rel = entry - 85
.path() - 86
.strip_prefix(&base) - 87
.map(|p| p.to_path_buf()) - 88
.unwrap_or_else(|_| entry.path().to_path_buf()); - 89
if gs.is_match(&rel) || gs.is_match(entry.path()) { - 90
matches.push(rel.display().to_string()); - 91
if matches.len() >= MAX_MATCHES { - 92
break; - 93
} - 94
} - 95
} - 96
matches - 97
}) - 98
.await; - 99
let mut matches = match walk { - 100
Ok(m) => m, - 101
Err(e) => return ToolOutput::error(format!("glob walk failed: {e}")), - 102
}; - 103
- 104
if matches.is_empty() { - 105
return ToolOutput::ok("no files matched"); - 106
} - 107
matches.sort(); - 108
ToolOutput::ok(matches.join("\n")) - 109
} - 110
} - 111
- 112
pub fn is_ignored(path: &Path) -> bool { - 113
path.file_name() - 114
.and_then(|f| f.to_str()) - 115
// `.vak` holds tool-internal state, including every other Agent's - 116
// isolated workspace nested under `.vak/agents/<id>/workspace` (see - 117
// vak_config::paths::agent_workspace) — walking into it here would - 118
// let a glob/grep from one Agent's session read another Agent's - 119
// supposedly isolated files. `fs_tree`'s directory-browser walk - 120
// already excludes it for the same reason. - 121
.map(|f| f == ".git" || f == "node_modules" || f == "target" || f == ".vak") - 122
.unwrap_or(false) - 123
} - 124
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.