1use async_trait::async_trait;
4use serde::Deserialize;
5use std::path::PathBuf;
6
7use super::sandbox::{resolve_workspace_existing_path, resolve_workspace_write_path};
8use super::traits::*;
9use crate::text::truncate_chars;
10
11pub struct FileReadTool {
16 workspace: PathBuf,
17}
18
19impl FileReadTool {
20 pub fn new(workspace: PathBuf) -> Self {
21 Self { workspace }
22 }
23}
24
25#[derive(Deserialize)]
26struct ReadArgs {
27 #[serde(alias = "file_path")]
28 path: String,
29 offset: Option<usize>,
31 limit: Option<usize>,
33}
34
35#[async_trait]
36impl Tool for FileReadTool {
37 fn name(&self) -> &str {
38 "Read"
39 }
40
41 fn spec(&self) -> ToolSpec {
42 ToolSpec {
43 name: "Read".to_string(),
44 description: "Read the contents of a file. Use offset/limit for large files."
45 .to_string(),
46 parameters: serde_json::json!({
47 "type": "object",
48 "properties": {
49 "path": {
50 "type": "string",
51 "description": "Path to the file to read (relative or absolute)"
52 },
53 "offset": {
54 "type": "integer",
55 "description": "Line number to start reading from (1-indexed)"
56 },
57 "limit": {
58 "type": "integer",
59 "description": "Maximum number of lines to read"
60 }
61 },
62 "required": ["path"]
63 }),
64 }
65 }
66
67 async fn execute(&self, arguments: &str) -> anyhow::Result<ToolResult> {
68 let args: ReadArgs = serde_json::from_str(arguments)?;
69 let full_path = match resolve_workspace_existing_path(&self.workspace, &args.path) {
70 Ok(p) => p,
71 Err(e) => return Ok(ToolResult::error(e.to_string())),
72 };
73
74 match tokio::fs::read_to_string(&full_path).await {
75 Ok(content) => {
76 let lines: Vec<&str> = content.lines().collect();
77 let total_lines = lines.len();
78
79 let offset = args.offset.unwrap_or(1).max(1) - 1; let limit = args.limit.unwrap_or(2000);
81
82 let selected: Vec<&str> = lines.iter().skip(offset).take(limit).copied().collect();
83
84 let result = selected.join("\n");
85
86 let truncated = if result.len() > 50_000 {
88 format!(
89 "{}...\n[truncated at 50KB]",
90 truncate_chars(&result, 50_000)
91 )
92 } else {
93 result
94 };
95
96 let remaining = total_lines.saturating_sub(offset + limit);
97 if remaining > 0 {
98 Ok(ToolResult::success(format!(
99 "{}\n\n[{} more lines in file. Use offset={} to continue.]",
100 truncated,
101 remaining,
102 offset + limit + 1
103 )))
104 } else {
105 Ok(ToolResult::success(truncated))
106 }
107 }
108 Err(e) => Ok(ToolResult::error(format!(
109 "Cannot read '{}': {}",
110 args.path, e
111 ))),
112 }
113 }
114}
115
116pub struct FileWriteTool {
121 workspace: PathBuf,
122}
123
124impl FileWriteTool {
125 pub fn new(workspace: PathBuf) -> Self {
126 Self { workspace }
127 }
128}
129
130#[derive(Deserialize)]
131struct WriteArgs {
132 #[serde(alias = "file_path")]
133 path: String,
134 content: String,
135}
136
137#[async_trait]
138impl Tool for FileWriteTool {
139 fn name(&self) -> &str {
140 "Write"
141 }
142
143 fn spec(&self) -> ToolSpec {
144 ToolSpec {
145 name: "Write".to_string(),
146 description: "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.".to_string(),
147 parameters: serde_json::json!({
148 "type": "object",
149 "properties": {
150 "path": {
151 "type": "string",
152 "description": "Path to the file to write (relative or absolute)"
153 },
154 "content": {
155 "type": "string",
156 "description": "Content to write to the file"
157 }
158 },
159 "required": ["path", "content"]
160 }),
161 }
162 }
163
164 async fn execute(&self, arguments: &str) -> anyhow::Result<ToolResult> {
165 let args: WriteArgs = serde_json::from_str(arguments)?;
166 let full_path = match resolve_workspace_write_path(&self.workspace, &args.path) {
167 Ok(p) => p,
168 Err(e) => return Ok(ToolResult::error(e.to_string())),
169 };
170
171 if let Some(parent) = full_path.parent() {
173 tokio::fs::create_dir_all(parent).await?;
174 }
175
176 match tokio::fs::write(&full_path, &args.content).await {
177 Ok(_) => Ok(ToolResult::success(format!(
178 "Successfully wrote {} bytes to {}",
179 args.content.len(),
180 args.path
181 ))),
182 Err(e) => Ok(ToolResult::error(format!("Failed to write: {}", e))),
183 }
184 }
185}