1use super::{Tool, ToolInfo};
4use anyhow::{anyhow, Result};
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7use std::process::Stdio;
8use tokio::process::Command;
9use tokio::time::{timeout, Duration};
10
11#[derive(Debug, Clone)]
12pub struct BashTool {
13 timeout_secs: u64,
14}
15
16#[derive(Debug, Deserialize, Serialize)]
17struct BashParams {
18 command: String,
19}
20
21impl Default for BashTool {
22 fn default() -> Self {
23 Self::new()
24 }
25}
26
27impl BashTool {
28 pub fn new() -> Self {
29 Self { timeout_secs: 120 }
30 }
31}
32
33#[async_trait]
34impl Tool for BashTool {
35 fn info(&self) -> ToolInfo {
36 ToolInfo {
37 name: "bash".to_string(),
38 description: r#"Run commands in a bash shell
39* State is persistent across command calls within the same session
40* You don't have access to the internet via this tool
41* Long-running commands will timeout after 120 seconds
42* For background processes, use '&' (e.g., 'sleep 10 &')
43* To inspect specific line ranges, use 'sed -n 10,25p /path/to/file'"#.to_string(),
44 input_schema: serde_json::json!({
45 "type": "object",
46 "properties": {
47 "command": {
48 "type": "string",
49 "description": "The bash command to run"
50 }
51 },
52 "required": ["command"]
53 }),
54 }
55 }
56
57 async fn execute(&self, params: serde_json::Value) -> Result<String> {
58 let bash_params: BashParams = serde_json::from_value(params)
59 .map_err(|e| anyhow!("Invalid parameters: {}", e))?;
60
61 let output = timeout(
63 Duration::from_secs(self.timeout_secs),
64 Command::new("bash")
65 .arg("-c")
66 .arg(&bash_params.command)
67 .stdout(Stdio::piped())
68 .stderr(Stdio::piped())
69 .output()
70 )
71 .await
72 .map_err(|_| anyhow!("Command timed out after {} seconds", self.timeout_secs))??;
73
74 let mut result = String::new();
75
76 if !output.stdout.is_empty() {
78 result.push_str("=== STDOUT ===\n");
79 result.push_str(&String::from_utf8_lossy(&output.stdout));
80 if !result.ends_with('\n') {
81 result.push('\n');
82 }
83 }
84
85 if !output.stderr.is_empty() {
87 if !result.is_empty() {
88 result.push('\n');
89 }
90 result.push_str("=== STDERR ===\n");
91 result.push_str(&String::from_utf8_lossy(&output.stderr));
92 if !result.ends_with('\n') {
93 result.push('\n');
94 }
95 }
96
97 if !result.is_empty() {
99 result.push('\n');
100 }
101 result.push_str(&format!("Exit code: {}", output.status.code().unwrap_or(-1)));
102
103 Ok(result)
104 }
105}