use crate::common::Result;
use serde::Deserialize;
use tokio::fs;
#[derive(Debug, Deserialize)]
pub struct FileReadInput {
pub path: String,
#[serde(default)]
pub offset: Option<usize>,
#[serde(default)]
pub limit: Option<usize>,
}
pub fn definition() -> serde_json::Value {
serde_json::json!({
"type": "function",
"function": {
"name": "file_read",
"description": "Read a file's contents. Returns content with line numbers.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Absolute or relative path to the file"
},
"offset": {
"type": "integer",
"description": "Start reading from this line number (1-based)"
},
"limit": {
"type": "integer",
"description": "Maximum number of lines to read"
}
},
"required": ["path"]
}
}
})
}
pub async fn execute(input: FileReadInput, working_dir: &str) -> Result<String> {
let path = {
let candidate = if input.path.starts_with('/') {
std::path::PathBuf::from(&input.path)
} else {
std::path::Path::new(working_dir).join(&input.path)
};
crate::agent::approval::normalize_path_lexical(&candidate)
.to_string_lossy()
.into_owned()
};
let content = fs::read_to_string(&path)
.await
.map_err(crate::common::AgentError::Io)?;
let lines: Vec<&str> = content.lines().collect();
let total = lines.len();
let offset = input.offset.unwrap_or(1).saturating_sub(1).min(total);
let limit = input.limit.unwrap_or(2000);
let end = (offset + limit).min(total);
let selected = &lines[offset..end];
let mut output = String::new();
for (i, line) in selected.iter().enumerate() {
let line_num = offset + i + 1;
output.push_str(&format!("{:>6}\t{}\n", line_num, line));
}
if end < total {
output.push_str(&format!("\n[... {} more lines not shown]\n", total - end));
}
Ok(output)
}