| 240 | } |
| 241 | |
| 242 | async fn patch_file(&self, input: &serde_json::Value) -> AppResult<String> { |
| 243 | let path_str = input["path"] |
| 244 | .as_str() |
| 245 | .ok_or_else(|| AppError::Validation("Missing 'path' field".to_string()))?; |
| 246 | let old_string = input["old_string"] |
| 247 | .as_str() |
| 248 | .ok_or_else(|| AppError::Validation("Missing 'old_string' field".to_string()))?; |
| 249 | let new_string = input["new_string"] |
| 250 | .as_str() |
| 251 | .ok_or_else(|| AppError::Validation("Missing 'new_string' field".to_string()))?; |
| 252 | |
| 253 | let safe_path = self.validate_path(path_str)?; |
| 254 | |
| 255 | let content = tokio::fs::read_to_string(&safe_path) |
| 256 | .await |
| 257 | .map_err(AppError::from)?; |
| 258 | |
| 259 | let occurrences = content.matches(old_string).count(); |
| 260 | if occurrences == 0 { |
| 261 | return Err(AppError::Validation(format!( |
| 262 | "old_string not found in {}", |
| 263 | path_str |
| 264 | ))); |
| 265 | } |
| 266 | if occurrences > 1 { |
| 267 | return Err(AppError::Validation(format!( |
| 268 | "old_string found {} times in {} — must be unique. Add more context lines to disambiguate.", |
| 269 | occurrences, path_str |
| 270 | ))); |
| 271 | } |
| 272 | |
| 273 | let new_content = content.replacen(old_string, new_string, 1); |
| 274 | tokio::fs::write(&safe_path, &new_content) |
| 275 | .await |
| 276 | .map_err(AppError::from)?; |
| 277 | |
| 278 | let old_lines = old_string.lines().count(); |
| 279 | let new_lines = new_string.lines().count(); |
| 280 | Ok(format!( |
| 281 | "Patched {}: replaced {} lines with {} lines", |
| 282 | path_str, old_lines, new_lines |
| 283 | )) |
| 284 | } |
| 285 | |
| 286 | async fn list_dir(&self, input: &serde_json::Value) -> AppResult<String> { |
| 287 | let Some(path_str) = input["path"].as_str() else { |