use llm_coding_tools_core::operations::execute_command;
use llm_coding_tools_core::tool_names;
use llm_coding_tools_core::{ToolContext, ToolError, ToolOutput};
use rig::completion::ToolDefinition;
use rig::tool::Tool;
use schemars::{schema_for, JsonSchema};
use serde::Deserialize;
use std::path::Path;
use std::time::Duration;
const DEFAULT_TIMEOUT_MS: u64 = 120_000;
fn default_timeout_ms() -> u64 {
DEFAULT_TIMEOUT_MS
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct BashArgs {
pub command: String,
pub workdir: Option<String>,
#[serde(default = "default_timeout_ms")]
pub timeout_ms: u64,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct BashTool;
impl BashTool {
#[inline]
pub fn new() -> Self {
Self
}
}
impl Tool for BashTool {
const NAME: &'static str = tool_names::BASH;
type Error = ToolError;
type Args = BashArgs;
type Output = ToolOutput;
async fn definition(&self, _prompt: String) -> ToolDefinition {
ToolDefinition {
name: <Self as Tool>::NAME.to_string(),
description: "Execute a shell command with optional working directory and timeout."
.to_string(),
parameters: serde_json::to_value(schema_for!(BashArgs))
.expect("schema serialization should never fail"),
}
}
async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
let workdir = args.workdir.as_ref().map(Path::new);
let timeout = Duration::from_millis(args.timeout_ms);
let result = execute_command(&args.command, workdir, timeout).await?;
Ok(result.format_output())
}
}
impl ToolContext for BashTool {
const NAME: &'static str = tool_names::BASH;
fn context(&self) -> &'static str {
llm_coding_tools_core::context::BASH
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn executes_echo() {
let tool = BashTool::new();
let args = BashArgs {
command: "echo hello".to_string(),
workdir: None,
timeout_ms: 5000,
};
let result = tool.call(args).await.unwrap();
assert!(result.content.contains("hello"));
}
#[tokio::test]
async fn timeout_returns_error() {
let tool = BashTool::new();
let cmd = if cfg!(target_os = "windows") {
"ping -n 10 127.0.0.1"
} else {
"sleep 10"
};
let args = BashArgs {
command: cmd.to_string(),
workdir: None,
timeout_ms: 100,
};
let result = tool.call(args).await;
assert!(matches!(result, Err(ToolError::Timeout(_))));
}
}