Skip to main content

dot/tools/
shell.rs

1use anyhow::{Context, Result};
2use serde_json::Value;
3use std::process::Command;
4
5use super::Tool;
6
7const MAX_OUTPUT_CHARS: usize = 10_000;
8
9pub struct RunCommandTool;
10
11impl Tool for RunCommandTool {
12    fn name(&self) -> &str {
13        "run_command"
14    }
15
16    fn description(&self) -> &str {
17        "Execute a shell command and return its output. Use this to run build commands, tests, git operations, etc."
18    }
19
20    fn input_schema(&self) -> Value {
21        serde_json::json!({
22            "type": "object",
23            "properties": {
24                "command": {
25                    "type": "string",
26                    "description": "The shell command to execute"
27                },
28                "working_directory": {
29                    "type": "string",
30                    "description": "Optional working directory for the command"
31                }
32            },
33            "required": ["command"]
34        })
35    }
36
37    fn execute(&self, input: Value) -> Result<String> {
38        let command = input["command"]
39            .as_str()
40            .context("Missing required parameter 'command'")?;
41        let working_dir = input["working_directory"].as_str();
42
43        tracing::debug!("run_command: {}", command);
44
45        let mut cmd = Command::new("/bin/sh");
46        cmd.arg("-c").arg(command);
47
48        if let Some(dir) = working_dir {
49            cmd.current_dir(dir);
50        }
51
52        let output = cmd
53            .output()
54            .with_context(|| format!("Failed to execute command: {}", command))?;
55
56        let stdout = String::from_utf8_lossy(&output.stdout);
57        let stderr = String::from_utf8_lossy(&output.stderr);
58        let exit_code = output.status.code().unwrap_or(-1);
59
60        let mut combined = format!("Exit code: {}\n", exit_code);
61
62        if !stdout.is_empty() {
63            combined.push_str(&stdout);
64        }
65
66        if !stderr.is_empty() {
67            if !stdout.is_empty() {
68                combined.push('\n');
69            }
70            combined.push_str("STDERR:\n");
71            combined.push_str(&stderr);
72        }
73
74        if combined.len() > MAX_OUTPUT_CHARS {
75            combined.truncate(MAX_OUTPUT_CHARS);
76            combined.push_str(&format!(
77                "\n... (output truncated at {} chars)",
78                MAX_OUTPUT_CHARS
79            ));
80        }
81
82        Ok(combined)
83    }
84}