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))?;
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();
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');
}
}
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');
}
}
if !result.is_empty() {
result.push('\n');
}
result.push_str(&format!("Exit code: {}", output.status.code().unwrap_or(-1)));
Ok(result)
}
}