claude-utils 0.2.0

Cross-platform companion toolkit for Anthropic's Claude Code CLI
Documentation
use serde_json::{json, Value};
use std::path::PathBuf;
use tokio::fs;
use tokio::process::Command;
use tracing::{info, warn};

use crate::mcp::protocol::*;
use crate::turbo::TurboMode;
use crate::Result;

/// Extended MCP tools for Turbo Mode
pub struct TurboTools {
    turbo: Option<std::sync::Arc<TurboMode>>,
}

impl TurboTools {
    pub fn new(turbo: Option<std::sync::Arc<TurboMode>>) -> Self {
        Self { turbo }
    }

    pub fn list_tools(&self) -> Vec<Tool> {
        vec![
            // File operations
            Tool {
                name: "file.read".to_string(),
                description: "Read file contents".to_string(),
                input_schema: json!({
                    "type": "object",
                    "properties": {
                        "path": {
                            "type": "string",
                            "description": "File path to read"
                        }
                    },
                    "required": ["path"]
                }),
            },
            Tool {
                name: "file.write".to_string(),
                description: "Write content to file".to_string(),
                input_schema: json!({
                    "type": "object",
                    "properties": {
                        "path": {
                            "type": "string",
                            "description": "File path to write"
                        },
                        "content": {
                            "type": "string",
                            "description": "Content to write"
                        }
                    },
                    "required": ["path", "content"]
                }),
            },
            Tool {
                name: "file.edit".to_string(),
                description: "Edit file with find/replace".to_string(),
                input_schema: json!({
                    "type": "object",
                    "properties": {
                        "path": {
                            "type": "string",
                            "description": "File path to edit"
                        },
                        "find": {
                            "type": "string",
                            "description": "Text to find"
                        },
                        "replace": {
                            "type": "string", 
                            "description": "Text to replace with"
                        }
                    },
                    "required": ["path", "find", "replace"]
                }),
            },
            // Command execution
            Tool {
                name: "command.execute".to_string(),
                description: "Execute shell command".to_string(),
                input_schema: json!({
                    "type": "object",
                    "properties": {
                        "command": {
                            "type": "string",
                            "description": "Command to execute"
                        },
                        "cwd": {
                            "type": "string",
                            "description": "Working directory (optional)"
                        }
                    },
                    "required": ["command"]
                }),
            },
            // Turbo-specific tools
            Tool {
                name: "turbo.checkpoint".to_string(),
                description: "Create a rollback checkpoint".to_string(),
                input_schema: json!({
                    "type": "object",
                    "properties": {
                        "message": {
                            "type": "string",
                            "description": "Checkpoint description"
                        }
                    },
                    "required": ["message"]
                }),
            },
        ]
    }

    pub async fn handle_tool_call(
        &self,
        tool_name: &str,
        args: Option<Value>,
    ) -> Result<Value> {
        // Check if we're in YOLO mode
        let auto_approve = if let Some(turbo) = &self.turbo {
            turbo.is_yolo().await
        } else {
            false
        };

        if auto_approve {
            info!("🚀 YOLO: Auto-approving {}", tool_name);
        } else {
            // In non-YOLO mode, we could add a permission check here
            warn!("Tool {} called without YOLO mode", tool_name);
        }

        match tool_name {
            "file.read" => self.handle_file_read(args).await,
            "file.write" => self.handle_file_write(args).await,
            "file.edit" => self.handle_file_edit(args).await,
            "command.execute" => self.handle_command_execute(args).await,
            "turbo.checkpoint" => self.handle_turbo_checkpoint(args).await,
            _ => Err(crate::ClaudeUtilsError::McpProtocol(
                format!("Unknown tool: {}", tool_name)
            )),
        }
    }

    async fn handle_file_read(&self, args: Option<Value>) -> Result<Value> {
        let args = args.ok_or_else(|| 
            crate::ClaudeUtilsError::McpProtocol("Missing arguments".into())
        )?;
        
        let path = args["path"].as_str().ok_or_else(||
            crate::ClaudeUtilsError::McpProtocol("Missing path".into())
        )?;

        let content = fs::read_to_string(path).await?;
        
        Ok(json!({
            "content": content,
            "path": path,
            "turbo": true
        }))
    }

    async fn handle_file_write(&self, args: Option<Value>) -> Result<Value> {
        let args = args.ok_or_else(|| 
            crate::ClaudeUtilsError::McpProtocol("Missing arguments".into())
        )?;
        
        let path = args["path"].as_str().ok_or_else(||
            crate::ClaudeUtilsError::McpProtocol("Missing path".into())
        )?;
        
        let content = args["content"].as_str().ok_or_else(||
            crate::ClaudeUtilsError::McpProtocol("Missing content".into())
        )?;

        // Record operation for rollback if turbo mode is enabled
        if let Some(turbo) = &self.turbo {
            use crate::turbo::rollback::{Operation, OperationType};
            
            // Check if file exists for backup
            let original_content = fs::read_to_string(path).await.ok();
            
            let op = Operation {
                op_type: if original_content.is_some() { 
                    OperationType::FileEdit 
                } else { 
                    OperationType::FileCreate 
                },
                target: path.to_string(),
                backup_path: None,
                original_content,
            };
            
            turbo.rollback_manager.record_operation(op).await?;
        }

        fs::write(path, content).await?;
        
        Ok(json!({
            "success": true,
            "path": path,
            "turbo": true
        }))
    }

    async fn handle_file_edit(&self, args: Option<Value>) -> Result<Value> {
        let args = args.ok_or_else(|| 
            crate::ClaudeUtilsError::McpProtocol("Missing arguments".into())
        )?;
        
        let path = args["path"].as_str().ok_or_else(||
            crate::ClaudeUtilsError::McpProtocol("Missing path".into())
        )?;
        
        let find = args["find"].as_str().ok_or_else(||
            crate::ClaudeUtilsError::McpProtocol("Missing find".into())
        )?;
        
        let replace = args["replace"].as_str().ok_or_else(||
            crate::ClaudeUtilsError::McpProtocol("Missing replace".into())
        )?;

        let content = fs::read_to_string(path).await?;
        
        // Record for rollback
        if let Some(turbo) = &self.turbo {
            use crate::turbo::rollback::{Operation, OperationType};
            
            let op = Operation {
                op_type: OperationType::FileEdit,
                target: path.to_string(),
                backup_path: None,
                original_content: Some(content.clone()),
            };
            
            turbo.rollback_manager.record_operation(op).await?;
        }

        let new_content = content.replace(find, replace);
        fs::write(path, &new_content).await?;
        
        Ok(json!({
            "success": true,
            "path": path,
            "replacements": content.matches(find).count(),
            "turbo": true
        }))
    }

    async fn handle_command_execute(&self, args: Option<Value>) -> Result<Value> {
        let args = args.ok_or_else(|| 
            crate::ClaudeUtilsError::McpProtocol("Missing arguments".into())
        )?;
        
        let command = args["command"].as_str().ok_or_else(||
            crate::ClaudeUtilsError::McpProtocol("Missing command".into())
        )?;
        
        let cwd = args["cwd"].as_str().map(PathBuf::from);

        let mut cmd = Command::new("sh");
        cmd.arg("-c").arg(command);
        
        if let Some(cwd) = cwd {
            cmd.current_dir(cwd);
        }

        let output = cmd.output().await?;
        
        Ok(json!({
            "stdout": String::from_utf8_lossy(&output.stdout),
            "stderr": String::from_utf8_lossy(&output.stderr),
            "success": output.status.success(),
            "code": output.status.code(),
            "turbo": true
        }))
    }

    async fn handle_turbo_checkpoint(&self, args: Option<Value>) -> Result<Value> {
        let args = args.ok_or_else(|| 
            crate::ClaudeUtilsError::McpProtocol("Missing arguments".into())
        )?;
        
        let message = args["message"].as_str().ok_or_else(||
            crate::ClaudeUtilsError::McpProtocol("Missing message".into())
        )?;

        if let Some(turbo) = &self.turbo {
            let checkpoint_id = turbo.rollback_manager
                .create_checkpoint(message.to_string())
                .await?;
            
            Ok(json!({
                "checkpoint_id": checkpoint_id,
                "message": message
            }))
        } else {
            Err(crate::ClaudeUtilsError::Turbo(
                "Turbo mode not enabled".into()
            ))
        }
    }
}