Skip to main content

claude_rust_tools/infrastructure/
repl_tool.rs

1use claude_rust_errors::AppResult;
2use claude_rust_types::{InterruptBehavior, PermissionLevel, Tool};
3use serde_json::{Value, json};
4use tokio::process::Command;
5
6/// Tool to execute code snippets in a REPL subprocess (Python or Node.js).
7pub struct REPLTool;
8
9impl REPLTool {
10    pub fn new() -> Self {
11        Self
12    }
13}
14
15#[async_trait::async_trait]
16impl Tool for REPLTool {
17    fn name(&self) -> &str {
18        "repl"
19    }
20
21    fn description(&self) -> &str {
22        "Execute code in a Python or Node.js subprocess and return the output."
23    }
24
25    fn input_schema(&self) -> Value {
26        json!({
27            "type": "object",
28            "properties": {
29                "language": {
30                    "type": "string",
31                    "description": "Programming language: \"python\" or \"node\"",
32                    "enum": ["python", "node"]
33                },
34                "code": {
35                    "type": "string",
36                    "description": "The code to execute"
37                }
38            },
39            "required": ["language", "code"]
40        })
41    }
42
43    fn permission_level(&self) -> PermissionLevel {
44        PermissionLevel::Dangerous
45    }
46
47    fn interrupt_behavior(&self) -> InterruptBehavior {
48        InterruptBehavior::Cancel
49    }
50
51    async fn execute(&self, input: Value) -> AppResult<String> {
52        let language = input
53            .get("language")
54            .and_then(|v| v.as_str())
55            .ok_or_else(|| claude_rust_errors::AppError::Tool("missing 'language' field".into()))?;
56
57        let code = input
58            .get("code")
59            .and_then(|v| v.as_str())
60            .ok_or_else(|| claude_rust_errors::AppError::Tool("missing 'code' field".into()))?;
61
62        let (program, flag) = match language {
63            "python" => ("python3", "-c"),
64            "node" => ("node", "-e"),
65            other => return Err(claude_rust_errors::AppError::Tool(
66                format!("unsupported language: {other}. Use 'python' or 'node'.")
67            )),
68        };
69
70        tracing::info!(language, code_len = code.len(), "executing REPL");
71
72        let output = Command::new(program)
73            .arg(flag)
74            .arg(code)
75            .output()
76            .await
77            .map_err(|e| claude_rust_errors::AppError::Tool(
78                format!("failed to spawn {program}: {e}")
79            ))?;
80
81        let stdout = String::from_utf8_lossy(&output.stdout);
82        let stderr = String::from_utf8_lossy(&output.stderr);
83
84        let mut result = String::new();
85        if !stdout.is_empty() {
86            result.push_str(&stdout);
87        }
88        if !stderr.is_empty() {
89            if !result.is_empty() {
90                result.push('\n');
91            }
92            result.push_str("STDERR:\n");
93            result.push_str(&stderr);
94        }
95        if result.is_empty() {
96            result.push_str("(no output)");
97        }
98
99        if result.len() > 100_000 {
100            result.truncate(100_000);
101            result.push_str("\n... (truncated)");
102        }
103
104        Ok(result)
105    }
106}