steer_tools/tools/
replace.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use std::path::Path;
4use steer_macros::tool;
5use tokio::fs;
6
7use crate::result::{EditResult, ReplaceResult};
8use crate::tools::{LS_TOOL_NAME, VIEW_TOOL_NAME};
9use crate::{ExecutionContext, ToolError};
10
11#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
12pub struct ReplaceParams {
13    /// The absolute path to the file to write (must be absolute, not relative)
14    pub file_path: String,
15    /// The content to write to the file
16    pub content: String,
17}
18
19tool! {
20    ReplaceTool {
21        params: ReplaceParams,
22        output: ReplaceResult,
23        variant: Edit,
24        description: format!(r#"Writes a file to the local filesystem.
25
26Before using this tool:
27
281. Use the {} tool to understand the file's contents and context
29
302. Directory Verification (only applicable when creating new files):
31 - Use the {} tool to verify the parent directory exists and is the correct location"#, VIEW_TOOL_NAME, LS_TOOL_NAME),
32        name: "write_file",
33        require_approval: true
34    }
35
36    async fn run(
37        _tool: &ReplaceTool,
38        params: ReplaceParams,
39        context: &ExecutionContext,
40    ) -> Result<ReplaceResult, ToolError> {
41        // Validate absolute path
42        if !params.file_path.starts_with('/') && !params.file_path.starts_with("\\") {
43            return Err(ToolError::invalid_params(
44                REPLACE_TOOL_NAME,
45                "file_path must be an absolute path".to_string(),
46            ));
47        }
48
49        // Convert to absolute path relative to working directory
50        let abs_path = if Path::new(&params.file_path).is_absolute() {
51            params.file_path.clone()
52        } else {
53            context
54                .working_directory
55                .join(&params.file_path)
56                .to_string_lossy()
57                .to_string()
58        };
59
60        let path = Path::new(&abs_path);
61
62        if context.is_cancelled() {
63            return Err(ToolError::Cancelled(REPLACE_TOOL_NAME.to_string()));
64        }
65
66        // Ensure parent directory exists
67        if let Some(parent) = path.parent() {
68            if !parent.exists() {
69                fs::create_dir_all(parent).await.map_err(|e| {
70                    ToolError::io(
71                        REPLACE_TOOL_NAME,
72                        format!("Failed to create parent directory: {e}"),
73                    )
74                })?;
75            }
76        }
77
78        // Check if file already exists
79        let file_existed = path.exists();
80
81        // Write the file
82        fs::write(path, &params.content).await.map_err(|e| {
83            ToolError::io(
84                REPLACE_TOOL_NAME,
85                format!("Failed to write file {abs_path}: {e}"),
86            )
87        })?;
88
89        Ok(ReplaceResult(EditResult {
90            file_path: abs_path,
91            changes_made: 1,
92            file_created: !file_existed,
93            old_content: None,
94            new_content: Some(params.content),
95        }))
96    }
97}