dot-ai 0.6.1

A minimal AI agent that lives in your terminal
Documentation
use anyhow::{Context, Result};
use serde_json::Value;
use std::process::Command;

use super::Tool;

const MAX_OUTPUT_CHARS: usize = 10_000;

pub struct RunCommandTool;

impl Tool for RunCommandTool {
    fn name(&self) -> &str {
        "run_command"
    }

    fn description(&self) -> &str {
        "Execute a shell command and return its output. Use this to run build commands, tests, git operations, etc."
    }

    fn input_schema(&self) -> Value {
        serde_json::json!({
            "type": "object",
            "properties": {
                "command": {
                    "type": "string",
                    "description": "The shell command to execute"
                },
                "working_directory": {
                    "type": "string",
                    "description": "Optional working directory for the command"
                }
            },
            "required": ["command"]
        })
    }

    fn execute(&self, input: Value) -> Result<String> {
        let command = input["command"]
            .as_str()
            .context("Missing required parameter 'command'")?;
        let working_dir = input["working_directory"].as_str();

        tracing::debug!("run_command: {}", command);

        let mut cmd = Command::new("/bin/sh");
        cmd.arg("-c").arg(command);

        if let Some(dir) = working_dir {
            cmd.current_dir(dir);
        }

        let output = cmd
            .output()
            .with_context(|| format!("Failed to execute command: {}", command))?;

        let stdout = String::from_utf8_lossy(&output.stdout);
        let stderr = String::from_utf8_lossy(&output.stderr);
        let exit_code = output.status.code().unwrap_or(-1);

        let mut combined = format!("Exit code: {}\n", exit_code);

        if !stdout.is_empty() {
            combined.push_str(&stdout);
        }

        if !stderr.is_empty() {
            if !stdout.is_empty() {
                combined.push('\n');
            }
            combined.push_str("STDERR:\n");
            combined.push_str(&stderr);
        }

        if combined.len() > MAX_OUTPUT_CHARS {
            combined.truncate(MAX_OUTPUT_CHARS);
            combined.push_str(&format!(
                "\n... (output truncated at {} chars)",
                MAX_OUTPUT_CHARS
            ));
        }

        Ok(combined)
    }
}