Skip to main content

codetether_agent/tool/
bash.rs

1//! Bash tool: execute shell commands
2
3use super::{Tool, ToolResult};
4use anyhow::Result;
5use async_trait::async_trait;
6use serde_json::{json, Value};
7use std::process::Stdio;
8use tokio::process::Command;
9use tokio::time::{timeout, Duration};
10
11/// Execute shell commands
12pub struct BashTool {
13    timeout_secs: u64,
14}
15
16impl BashTool {
17    pub fn new() -> Self {
18        Self { timeout_secs: 120 }
19    }
20
21    /// Create a new BashTool with a custom timeout
22    #[allow(dead_code)]
23    pub fn with_timeout(timeout_secs: u64) -> Self {
24        Self { timeout_secs }
25    }
26}
27
28#[async_trait]
29impl Tool for BashTool {
30    fn id(&self) -> &str {
31        "bash"
32    }
33
34    fn name(&self) -> &str {
35        "Bash"
36    }
37
38    fn description(&self) -> &str {
39        "Execute a shell command. Commands run in a bash shell with the current working directory."
40    }
41
42    fn parameters(&self) -> Value {
43        json!({
44            "type": "object",
45            "properties": {
46                "command": {
47                    "type": "string",
48                    "description": "The shell command to execute"
49                },
50                "cwd": {
51                    "type": "string",
52                    "description": "Working directory for the command (optional)"
53                },
54                "timeout": {
55                    "type": "integer",
56                    "description": "Timeout in seconds (default: 120)"
57                }
58            },
59            "required": ["command"]
60        })
61    }
62
63    async fn execute(&self, args: Value) -> Result<ToolResult> {
64        let command = args["command"]
65            .as_str()
66            .ok_or_else(|| anyhow::anyhow!("command is required"))?;
67        let cwd = args["cwd"].as_str();
68        let timeout_secs = args["timeout"]
69            .as_u64()
70            .unwrap_or(self.timeout_secs);
71
72        let mut cmd = Command::new("bash");
73        cmd.arg("-c")
74            .arg(command)
75            .stdout(Stdio::piped())
76            .stderr(Stdio::piped());
77
78        if let Some(dir) = cwd {
79            cmd.current_dir(dir);
80        }
81
82        let result = timeout(Duration::from_secs(timeout_secs), cmd.output()).await;
83
84        match result {
85            Ok(Ok(output)) => {
86                let stdout = String::from_utf8_lossy(&output.stdout);
87                let stderr = String::from_utf8_lossy(&output.stderr);
88                let exit_code = output.status.code().unwrap_or(-1);
89
90                let combined = if stderr.is_empty() {
91                    stdout.to_string()
92                } else if stdout.is_empty() {
93                    stderr.to_string()
94                } else {
95                    format!("{}\n--- stderr ---\n{}", stdout, stderr)
96                };
97
98                let success = output.status.success();
99
100                // Truncate if too long
101                let max_len = 50_000;
102                let (output_str, truncated) = if combined.len() > max_len {
103                    let truncated_output = format!(
104                        "{}...\n[Output truncated, {} bytes total]",
105                        &combined[..max_len],
106                        combined.len()
107                    );
108                    (truncated_output, true)
109                } else {
110                    (combined, false)
111                };
112
113                Ok(ToolResult {
114                    output: output_str,
115                    success,
116                    metadata: [
117                        ("exit_code".to_string(), json!(exit_code)),
118                        ("truncated".to_string(), json!(truncated)),
119                    ]
120                    .into_iter()
121                    .collect(),
122                })
123            }
124            Ok(Err(e)) => Ok(ToolResult::error(format!("Failed to execute command: {}", e))),
125            Err(_) => Ok(ToolResult::error(format!(
126                "Command timed out after {} seconds",
127                timeout_secs
128            ))),
129        }
130    }
131}
132
133impl Default for BashTool {
134    fn default() -> Self {
135        Self::new()
136    }
137}