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