// tools/read.rs — Read file contents use anyhow::{Context, Result}; use serde::Deserialize; pub fn tool() -> super::Tool { super::Tool { name: "read_file", description: "Read the contents of a file. Returns the file contents with line numbers.", parameters_json: r#"{"type":"object","properties":{"file_path":{"type":"string","description":"Absolute path to the file to read"},"offset":{"type":"integer","description":"Line number to start reading from (1-based)"},"limit":{"type":"integer","description":"Maximum number of lines to read"}},"required":["file_path"]}"#, handler: Arc::new(|_a, v| Box::pin(async move { read_file(&v)) }), } } #[derive(Deserialize)] struct Args { file_path: String, #[serde(default = "default_offset")] offset: usize, limit: Option, } fn default_offset() -> usize { 1 } fn read_file(args: &serde_json::Value) -> Result { let a: Args = serde_json::from_value(args.clone()).context("invalid read_file arguments")?; let content = std::fs::read_to_string(&a.file_path) .with_context(|| format!("Failed to read {}", a.file_path))?; let lines: Vec<&str> = content.lines().collect(); let start = a.offset.saturating_sub(1); let end = a.limit.map_or(lines.len(), |l| (start + l).min(lines.len())); if start >= lines.len() { return Ok(format!("(file has {} lines, offset {} is past end)", lines.len(), a.offset)); } let mut out = String::new(); for (i, line) in lines[start..end].iter().enumerate() { out.push_str(&format!("{}\t{}\n", start + i + 1, line)); } Ok(out) }