// tools/read.rs — Read file contents use anyhow::{Context, Result}; use serde::Deserialize; use serde_json::json; use super::ToolDef; #[derive(Deserialize)] struct Args { file_path: String, #[serde(default = "default_offset")] offset: usize, limit: Option, } fn default_offset() -> usize { 1 } pub fn tool() -> super::Tool { super::Tool { def: definition(), handler: |_a, v| Box::pin(async move { read_file(&v) }) } } fn definition() -> ToolDef { ToolDef::new( "read_file", "Read the contents of a file. Returns the file contents with line numbers.", json!({ "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). Optional." }, "limit": { "type": "integer", "description": "Maximum number of lines to read. Optional." } }, "required": ["file_path"] }), ) } fn read_file(args: &serde_json::Value) -> Result { let args: Args = serde_json::from_value(args.clone()) .context("invalid read_file arguments")?; let content = std::fs::read_to_string(&args.file_path) .with_context(|| format!("Failed to read {}", args.file_path))?; let lines: Vec<&str> = content.lines().collect(); let offset = args.offset.max(1) - 1; let limit = args.limit.unwrap_or(lines.len()); let mut output = String::new(); for (i, line) in lines.iter().skip(offset).take(limit).enumerate() { output.push_str(&format!("{:>6}\t{}\n", offset + i + 1, line)); } if output.is_empty() { output = "(empty file)\n".to_string(); } Ok(output) }