toast-api 0.1.7

An unofficial CLI client and API server for Claude/Deepseek
Documentation
//! Bash tool implementation for executing shell commands

use super::{Tool, ToolInfo};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::process::Stdio;
use tokio::process::Command;
use tokio::time::{timeout, Duration};

#[derive(Debug, Clone)]
pub struct BashTool {
    timeout_secs: u64,
}

#[derive(Debug, Deserialize, Serialize)]
struct BashParams {
    command: String,
}

impl Default for BashTool {
    fn default() -> Self {
        Self::new()
    }
}

impl BashTool {
    pub fn new() -> Self {
        Self { timeout_secs: 120 }
    }
}

#[async_trait]
impl Tool for BashTool {
    fn info(&self) -> ToolInfo {
        ToolInfo {
            name: "bash".to_string(),
            description: r#"Run commands in a bash shell
* State is persistent across command calls within the same session
* You don't have access to the internet via this tool
* Long-running commands will timeout after 120 seconds
* For background processes, use '&' (e.g., 'sleep 10 &')
* To inspect specific line ranges, use 'sed -n 10,25p /path/to/file'"#.to_string(),
            input_schema: serde_json::json!({
                "type": "object",
                "properties": {
                    "command": {
                        "type": "string",
                        "description": "The bash command to run"
                    }
                },
                "required": ["command"]
            }),
        }
    }

    async fn execute(&self, params: serde_json::Value) -> Result<String> {
        let bash_params: BashParams = serde_json::from_value(params)
            .map_err(|e| anyhow!("Invalid parameters: {}", e))?;

        // Execute command with timeout
        let output = timeout(
            Duration::from_secs(self.timeout_secs),
            Command::new("bash")
                .arg("-c")
                .arg(&bash_params.command)
                .stdout(Stdio::piped())
                .stderr(Stdio::piped())
                .output()
        )
        .await
        .map_err(|_| anyhow!("Command timed out after {} seconds", self.timeout_secs))??;

        let mut result = String::new();

        // Add stdout if present
        if !output.stdout.is_empty() {
            result.push_str("=== STDOUT ===\n");
            result.push_str(&String::from_utf8_lossy(&output.stdout));
            if !result.ends_with('\n') {
                result.push('\n');
            }
        }

        // Add stderr if present
        if !output.stderr.is_empty() {
            if !result.is_empty() {
                result.push('\n');
            }
            result.push_str("=== STDERR ===\n");
            result.push_str(&String::from_utf8_lossy(&output.stderr));
            if !result.ends_with('\n') {
                result.push('\n');
            }
        }

        // Add exit code
        if !result.is_empty() {
            result.push('\n');
        }
        result.push_str(&format!("Exit code: {}", output.status.code().unwrap_or(-1)));

        Ok(result)
    }
}