Skip to main content

lean_ctx/tools/
ctx_execute.rs

1use crate::core::sandbox::{self, SandboxResult};
2use crate::core::tokens::count_tokens;
3
4/// Executes a code snippet in a sandboxed environment and returns formatted output.
5pub fn handle(language: &str, code: &str, intent: Option<&str>, timeout: Option<u64>) -> String {
6    let result = sandbox::execute(language, code, timeout);
7    format_result(&result, intent)
8}
9
10/// Reads a file from disk, detects its language, and executes a processing script.
11pub fn handle_file(path: &str, intent: Option<&str>) -> String {
12    let content = match std::fs::read_to_string(path) {
13        Ok(c) => c,
14        Err(e) => return format!("Error reading {path}: {e}"),
15    };
16
17    let language = detect_language_from_extension(path);
18    let code = build_file_processing_script(&language, &content, intent);
19    let result = sandbox::execute(&language, &code, None);
20    format_result(&result, intent)
21}
22
23/// Executes multiple (language, code) pairs in parallel and returns aggregated results.
24pub fn handle_batch(items: &[(String, String)]) -> String {
25    let results = sandbox::batch_execute(items);
26    let mut output = Vec::new();
27
28    for (i, result) in results.iter().enumerate() {
29        let label = format!("[{}/{}] {}", i + 1, results.len(), result.language);
30        if result.exit_code == 0 {
31            let stdout = result.stdout.trim();
32            if stdout.is_empty() {
33                output.push(format!("{label}: (no output) [{} ms]", result.duration_ms));
34            } else {
35                output.push(format!("{label}: {stdout} [{} ms]", result.duration_ms));
36            }
37        } else {
38            let stderr = result.stderr.trim();
39            output.push(format!(
40                "{label}: EXIT {} — {stderr} [{} ms]",
41                result.exit_code, result.duration_ms
42            ));
43        }
44    }
45
46    let total_ms: u64 = results.iter().map(|r| r.duration_ms).sum();
47    output.push(format!("\n{} tasks, {} ms total", results.len(), total_ms));
48    output.join("\n")
49}
50
51fn format_result(result: &SandboxResult, intent: Option<&str>) -> String {
52    let mut parts = Vec::new();
53
54    if result.exit_code == 0 {
55        let stdout = result.stdout.trim();
56        if stdout.is_empty() {
57            parts.push("(no output)".to_string());
58        } else {
59            let raw_tokens = count_tokens(stdout);
60            parts.push(stdout.to_string());
61
62            if let Some(intent_desc) = intent {
63                if raw_tokens > 50 {
64                    parts.push(format!("[intent: {intent_desc}]"));
65                }
66            }
67        }
68    } else {
69        if !result.stdout.is_empty() {
70            parts.push(result.stdout.trim().to_string());
71        }
72        parts.push(format!(
73            "EXIT {} — {}",
74            result.exit_code,
75            result.stderr.trim()
76        ));
77    }
78
79    parts.push(format!("[{} | {} ms]", result.language, result.duration_ms));
80    parts.join("\n")
81}
82
83fn detect_language_from_extension(path: &str) -> String {
84    let ext = path.rsplit('.').next().unwrap_or("");
85    match ext {
86        "js" | "mjs" | "cjs" => "javascript",
87        "ts" | "mts" | "cts" => "typescript",
88        "py" | "json" | "csv" | "log" | "txt" | "xml" | "yaml" | "yml" | "md" | "html" => "python",
89        "rb" => "ruby",
90        "go" => "go",
91        "rs" => "rust",
92        "php" => "php",
93        "pl" => "perl",
94        "r" | "R" => "r",
95        "ex" | "exs" => "elixir",
96        _ => "shell",
97    }
98    .to_string()
99}
100
101fn build_file_processing_script(language: &str, content: &str, intent: Option<&str>) -> String {
102    let escaped = content.replace('\\', "\\\\").replace('\'', "\\'");
103    let intent_str = intent.unwrap_or("summarize the content");
104
105    match language {
106        "python" => {
107            format!(
108                r#"
109import json, re
110from collections import Counter
111
112data = '''{escaped}'''
113
114lines = data.strip().split('\n')
115total_lines = len(lines)
116total_bytes = len(data.encode('utf-8'))
117
118word_count = sum(len(line.split()) for line in lines)
119
120print(f"{{total_lines}} lines, {{total_bytes}} bytes, {{word_count}} words")
121print(f"Intent: {intent_str}")
122
123if total_lines > 10:
124    print(f"First 3: {{lines[:3]}}")
125    print(f"Last 3: {{lines[-3:]}}")
126"#
127            )
128        }
129        _ => {
130            format!(
131                r#"
132data='{escaped}'
133lines=$(echo "$data" | wc -l | tr -d ' ')
134bytes=$(echo "$data" | wc -c | tr -d ' ')
135echo "$lines lines, $bytes bytes"
136echo "Intent: {intent_str}"
137echo "$data" | head -3
138echo "..."
139echo "$data" | tail -3
140"#
141            )
142        }
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn handle_simple_python() {
152        let result = handle("python", "print(2 + 2)", None, None);
153        assert!(result.contains('4'));
154        assert!(result.contains("python"));
155    }
156
157    #[test]
158    fn handle_with_intent() {
159        let result = handle(
160            "python",
161            "print('found 5 errors')",
162            Some("count errors"),
163            None,
164        );
165        assert!(result.contains("found 5 errors"));
166    }
167
168    #[test]
169    fn handle_error_shows_stderr() {
170        let result = handle("python", "raise Exception('boom')", None, None);
171        assert!(result.contains("EXIT"));
172        assert!(result.contains("boom"));
173    }
174
175    #[test]
176    fn detect_language_from_path() {
177        assert_eq!(detect_language_from_extension("test.py"), "python");
178        assert_eq!(detect_language_from_extension("test.js"), "javascript");
179        assert_eq!(detect_language_from_extension("test.rs"), "rust");
180        assert_eq!(detect_language_from_extension("test.csv"), "python");
181        assert_eq!(detect_language_from_extension("test.log"), "python");
182    }
183
184    #[test]
185    #[cfg(not(target_os = "windows"))]
186    fn batch_multiple_tasks() {
187        let items = vec![
188            ("python".to_string(), "print('task1')".to_string()),
189            ("shell".to_string(), "echo task2".to_string()),
190        ];
191        let result = handle_batch(&items);
192        assert!(result.contains("task1"));
193        assert!(result.contains("task2"));
194        assert!(result.contains("2 tasks"));
195    }
196}