use agent_contracts::backend::{
capability::{
search::{GlobRequest, GrepMode, GrepRequest, GrepResult},
OperationSearch,
},
BackendPath, OperationError,
};
use async_trait::async_trait;
use glob::Pattern;
use std::path::Path;
use std::sync::Arc;
use super::backend::{shell_quote, E2bBackendState};
use super::exec::E2bExec;
pub(crate) struct E2bSearch {
exec: E2bExec,
}
impl E2bSearch {
pub(crate) fn new(state: Arc<E2bBackendState>) -> Self {
Self {
exec: E2bExec::new(state),
}
}
}
#[async_trait]
impl OperationSearch for E2bSearch {
async fn glob(&self, request: GlobRequest) -> Result<Vec<BackendPath>, OperationError> {
let base_dir = request
.base_dir
.unwrap_or_else(|| self.exec.state().workspace_root.clone());
let pattern = Pattern::new(request.pattern.as_str()).map_err(|error| {
OperationError::InvalidPath {
message: format!("invalid glob pattern: {error}"),
}
})?;
let script = format!(
"if [ -d {base} ]; then find {base} -mindepth 1 -print; fi",
base = shell_quote(base_dir.0.as_str()),
);
let output = self.exec.run_shell_script(script.as_str(), None).await?;
if output.exit_code != Some(0) {
return Err(OperationError::ExecutionFailed {
message: String::from_utf8_lossy(output.stderr.as_slice()).to_string(),
});
}
let mut paths = String::from_utf8_lossy(output.stdout.as_slice())
.lines()
.filter(|line| !line.is_empty())
.filter_map(|line| {
let path = Path::new(line);
let relative = path
.strip_prefix(base_dir.0.as_str())
.ok()
.and_then(|value| value.to_str())
.unwrap_or("")
.trim_start_matches('/');
if pattern.matches(relative) || pattern.matches_path(path) {
Some(BackendPath(line.to_string()))
} else {
None
}
})
.collect::<Vec<_>>();
paths.sort_by(|a, b| a.0.cmp(&b.0));
if let Some(limit) = request.limit {
paths.truncate(limit);
}
Ok(paths)
}
async fn grep(&self, request: GrepRequest) -> Result<GrepResult, OperationError> {
let mut cmd = vec!["grep -r -H".to_string()];
match &request.mode {
GrepMode::FilesWithMatches => {
cmd.push("-l".to_string());
}
GrepMode::Content => {}
GrepMode::Count => {
cmd.push("-c".to_string());
}
}
if let Some(include) = &request.include {
cmd.push(format!("--include={}", shell_quote(include)));
}
cmd.push("--".to_string());
cmd.push(shell_quote(&request.query));
cmd.push(shell_quote(request.base_dir.0.as_str()));
let output = self
.exec
.run_shell_script(cmd.join(" ").as_str(), None)
.await?;
let exit_code = output.exit_code.unwrap_or(-1);
if exit_code != 0 && exit_code != 1 {
return Err(OperationError::ExecutionFailed {
message: String::from_utf8_lossy(output.stderr.as_slice()).to_string(),
});
}
let stdout_text = String::from_utf8_lossy(output.stdout.as_slice());
let mut entries = stdout_text
.lines()
.filter(|line| !line.is_empty())
.map(|line| line.to_string())
.collect::<Vec<_>>();
if let Some(limit) = request.head_limit {
entries.truncate(limit);
}
Ok(GrepResult { entries })
}
}