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_counted;
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 = match truncate_chars_counted(&result, 50_000) {
88 Some((head, dropped)) => {
89 format!("{}...\n[truncated {} chars]", head, dropped)
90 }
91 None => result,
92 };
93
94 let remaining = total_lines.saturating_sub(offset + limit);
95 if remaining > 0 {
96 Ok(ToolResult::success(format!(
97 "{}\n\n[{} more lines in file. Use offset={} to continue.]",
98 truncated,
99 remaining,
100 offset + limit + 1
101 )))
102 } else {
103 Ok(ToolResult::success(truncated))
104 }
105 }
106 Err(e) => Ok(ToolResult::error(format!(
107 "Cannot read '{}': {}",
108 args.path, e
109 ))),
110 }
111 }
112}
113
114pub struct FileWriteTool {
119 workspace: PathBuf,
120}
121
122impl FileWriteTool {
123 pub fn new(workspace: PathBuf) -> Self {
124 Self { workspace }
125 }
126}
127
128#[derive(Deserialize)]
129struct WriteArgs {
130 #[serde(alias = "file_path")]
131 path: String,
132 content: String,
133}
134
135#[async_trait]
136impl Tool for FileWriteTool {
137 fn name(&self) -> &str {
138 "Write"
139 }
140
141 fn spec(&self) -> ToolSpec {
142 ToolSpec {
143 name: "Write".to_string(),
144 description: "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.".to_string(),
145 parameters: serde_json::json!({
146 "type": "object",
147 "properties": {
148 "path": {
149 "type": "string",
150 "description": "Path to the file to write (relative or absolute)"
151 },
152 "content": {
153 "type": "string",
154 "description": "Content to write to the file"
155 }
156 },
157 "required": ["path", "content"]
158 }),
159 }
160 }
161
162 async fn execute(&self, arguments: &str) -> anyhow::Result<ToolResult> {
163 let args: WriteArgs = serde_json::from_str(arguments)?;
164 let full_path = match resolve_workspace_write_path(&self.workspace, &args.path) {
165 Ok(p) => p,
166 Err(e) => return Ok(ToolResult::error(e.to_string())),
167 };
168
169 if let Some(parent) = full_path.parent() {
171 tokio::fs::create_dir_all(parent).await?;
172 }
173
174 match tokio::fs::write(&full_path, &args.content).await {
175 Ok(_) => Ok(ToolResult::success(format!(
176 "Successfully wrote {} bytes to {}",
177 args.content.len(),
178 args.path
179 ))),
180 Err(e) => Ok(ToolResult::error(format!("Failed to write: {}", e))),
181 }
182 }
183}