procyon 0.3.0

Terminal development harness for Stellar and Soroban smart contracts, driven by a language model
use async_trait::async_trait;
use serde_json::{json, Value};

use super::paths::resolve_in_workspace;
use super::Tool;

pub struct ReadFileTool;

#[async_trait]
impl Tool for ReadFileTool {
    fn name(&self) -> &str {
        "read_file"
    }

    fn capability(&self) -> crate::risk::Capability {
        crate::risk::Capability::ReadOnly
    }

    fn description(&self) -> &str {
        "Read the contents of a file"
    }

    fn input_schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "Path to the file to read"
                }
            },
            "required": ["path"]
        })
    }

    async fn execute(&self, input: Value) -> Result<String, String> {
        let path = input
            .get("path")
            .and_then(|v| v.as_str())
            .ok_or("Missing 'path' parameter")?;

        let resolved = resolve_in_workspace(path)?;
        if !tokio::fs::try_exists(&resolved).await.unwrap_or(false) {
            return Err(format!("File does not exist: {}", path));
        }

        tokio::fs::read_to_string(&resolved)
            .await
            .map_err(|e| format!("Failed to read file: {}", e))
    }
}

pub struct WriteFileTool;

#[async_trait]
impl Tool for WriteFileTool {
    fn name(&self) -> &str {
        "write_file"
    }

    fn capability(&self) -> crate::risk::Capability {
        crate::risk::Capability::Write
    }

    fn description(&self) -> &str {
        "Write content to a file"
    }

    fn input_schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "Path to the file to write"
                },
                "content": {
                    "type": "string",
                    "description": "Content to write to the file"
                }
            },
            "required": ["path", "content"]
        })
    }

    async fn execute(&self, input: Value) -> Result<String, String> {
        let path = input
            .get("path")
            .and_then(|v| v.as_str())
            .ok_or("Missing 'path' parameter")?;

        let content = input
            .get("content")
            .and_then(|v| v.as_str())
            .ok_or("Missing 'content' parameter")?;

        let resolved = resolve_in_workspace(path)?;

        if let Some(parent) = resolved.parent() {
            if !tokio::fs::try_exists(parent).await.unwrap_or(false) {
                tokio::fs::create_dir_all(parent)
                    .await
                    .map_err(|e| format!("Failed to create parent directories: {}", e))?;
            }
        }

        tokio::fs::write(&resolved, content)
            .await
            .map_err(|e| format!("Failed to write file: {}", e))?;

        Ok(format!("File written successfully: {}", path))
    }
}

pub struct EditFileTool;

#[async_trait]
impl Tool for EditFileTool {
    fn name(&self) -> &str {
        "edit_file"
    }

    fn capability(&self) -> crate::risk::Capability {
        crate::risk::Capability::Write
    }

    fn description(&self) -> &str {
        "Edit a file by replacing specific text"
    }

    fn input_schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "Path to the file to edit"
                },
                "old_text": {
                    "type": "string",
                    "description": "Text to find and replace"
                },
                "new_text": {
                    "type": "string",
                    "description": "Text to replace with"
                }
            },
            "required": ["path", "old_text", "new_text"]
        })
    }

    async fn execute(&self, input: Value) -> Result<String, String> {
        let path = input
            .get("path")
            .and_then(|v| v.as_str())
            .ok_or("Missing 'path' parameter")?;

        let old_text = input
            .get("old_text")
            .and_then(|v| v.as_str())
            .ok_or("Missing 'old_text' parameter")?;

        let new_text = input
            .get("new_text")
            .and_then(|v| v.as_str())
            .ok_or("Missing 'new_text' parameter")?;

        let resolved = resolve_in_workspace(path)?;
        if !tokio::fs::try_exists(&resolved).await.unwrap_or(false) {
            return Err(format!("File does not exist: {}", path));
        }

        let content = tokio::fs::read_to_string(&resolved)
            .await
            .map_err(|e| format!("Failed to read file: {}", e))?;

        if !content.contains(old_text) {
            return Err("Text not found in file".to_string());
        }

        let new_content = content.replace(old_text, new_text);
        tokio::fs::write(&resolved, new_content)
            .await
            .map_err(|e| format!("Failed to write file: {}", e))?;

        Ok(format!("File edited successfully: {}", path))
    }
}