claude_rust_tools/infrastructure/
repl_tool.rs1use claude_rust_errors::AppResult;
2use claude_rust_types::{InterruptBehavior, PermissionLevel, Tool};
3use serde_json::{Value, json};
4use tokio::process::Command;
5
6pub 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}