lean_ctx/tools/
ctx_execute.rs1use crate::core::sandbox::{self, SandboxResult};
2use crate::core::tokens::count_tokens;
3
4pub 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
10pub fn handle_file(path: &str, intent: Option<&str>, project_root: Option<&str>) -> String {
15 let jail_root = match project_root {
16 Some(r) => std::path::PathBuf::from(r),
17 None => std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
18 };
19 let candidate = std::path::Path::new(path);
20 let jailed = match crate::core::pathjail::jail_path(candidate, &jail_root) {
21 Ok(p) => p,
22 Err(e) => return format!("Path rejected: {e}"),
23 };
24 let path_str = jailed.to_string_lossy();
25
26 let cap = crate::core::limits::max_read_bytes();
27 let meta = match std::fs::metadata(&*jailed) {
28 Ok(m) => m,
29 Err(e) => return format!("Error reading {path_str}: {e}"),
30 };
31 if meta.len() > cap as u64 {
32 return format!(
33 "File too large ({} bytes, limit {cap} bytes). Use a line-range read instead.",
34 meta.len()
35 );
36 }
37 let content = match std::fs::read_to_string(&*jailed) {
38 Ok(c) => c,
39 Err(e) => return format!("Error reading {path_str}: {e}"),
40 };
41
42 let language = detect_language_from_extension(path);
43 let code = build_file_processing_script(&language, &content, intent);
44 let result = sandbox::execute(&language, &code, None);
45 format_result(&result, intent)
46}
47
48pub fn handle_batch(items: &[(String, String)]) -> String {
50 let results = sandbox::batch_execute(items);
51 let mut output = Vec::new();
52
53 for (i, result) in results.iter().enumerate() {
54 let label = format!("[{}/{}] {}", i + 1, results.len(), result.language);
55 if result.exit_code == 0 {
56 let stdout = result.stdout.trim();
57 if stdout.is_empty() {
58 output.push(format!("{label}: (no output) [{} ms]", result.duration_ms));
59 } else {
60 output.push(format!("{label}: {stdout} [{} ms]", result.duration_ms));
61 }
62 } else {
63 let stderr = result.stderr.trim();
64 output.push(format!(
65 "{label}: EXIT {} — {stderr} [{} ms]",
66 result.exit_code, result.duration_ms
67 ));
68 }
69 }
70
71 let total_ms: u64 = results.iter().map(|r| r.duration_ms).sum();
72 output.push(format!("\n{} tasks, {} ms total", results.len(), total_ms));
73 output.join("\n")
74}
75
76fn format_result(result: &SandboxResult, intent: Option<&str>) -> String {
77 let mut parts = Vec::new();
78
79 if result.exit_code == 0 {
80 let stdout = result.stdout.trim();
81 if stdout.is_empty() {
82 parts.push("(no output)".to_string());
83 } else {
84 let raw_tokens = count_tokens(stdout);
85 parts.push(stdout.to_string());
86
87 if let Some(intent_desc) = intent {
88 if raw_tokens > 50 {
89 parts.push(format!("[intent: {intent_desc}]"));
90 }
91 }
92 }
93 } else {
94 if !result.stdout.is_empty() {
95 parts.push(result.stdout.trim().to_string());
96 }
97 parts.push(format!(
98 "EXIT {} — {}",
99 result.exit_code,
100 result.stderr.trim()
101 ));
102 }
103
104 parts.push(format!("[{} | {} ms]", result.language, result.duration_ms));
105 parts.join("\n")
106}
107
108fn detect_language_from_extension(path: &str) -> String {
109 let ext = path.rsplit('.').next().unwrap_or("");
110 match ext {
111 "js" | "mjs" | "cjs" => "javascript",
112 "ts" | "mts" | "cts" => "typescript",
113 "py" | "json" | "csv" | "log" | "txt" | "xml" | "yaml" | "yml" | "md" | "html" => "python",
114 "rb" => "ruby",
115 "go" => "go",
116 "rs" => "rust",
117 "php" => "php",
118 "pl" => "perl",
119 "r" | "R" => "r",
120 "ex" | "exs" => "elixir",
121 _ => "shell",
122 }
123 .to_string()
124}
125
126fn sanitize_intent(raw: &str) -> String {
127 raw.chars()
128 .filter(|c| c.is_alphanumeric() || *c == ' ' || *c == '-' || *c == '_' || *c == '.')
129 .take(200)
130 .collect()
131}
132
133fn build_file_processing_script(language: &str, content: &str, intent: Option<&str>) -> String {
134 let Ok(tmp) = tempfile::Builder::new()
135 .prefix("lean-ctx-exec-")
136 .suffix(".dat")
137 .tempfile()
138 else {
139 return "echo 'lean-ctx: failed to create temp file'".to_string();
140 };
141 let _ = std::fs::write(tmp.path(), content);
142 let tmp_path = tmp.path().to_string_lossy().to_string();
143 let _keep = tmp.into_temp_path();
144 let intent_str = sanitize_intent(intent.unwrap_or("summarize the content"));
145
146 match language {
147 "python" => {
148 format!(
149 r#"
150import os
151
152with open(r"{tmp_path}", "r", encoding="utf-8") as f:
153 data = f.read()
154os.remove(r"{tmp_path}")
155
156lines = data.strip().split('\n')
157total_lines = len(lines)
158total_bytes = len(data.encode('utf-8'))
159
160word_count = sum(len(line.split()) for line in lines)
161
162print(f"{{total_lines}} lines, {{total_bytes}} bytes, {{word_count}} words")
163print("Intent: {intent_str}")
164
165if total_lines > 10:
166 print(f"First 3: {{lines[:3]}}")
167 print(f"Last 3: {{lines[-3:]}}")
168"#
169 )
170 }
171 _ => {
172 format!(
173 r#"
174data=$(cat "{tmp_path}")
175rm -f "{tmp_path}"
176lines=$(echo "$data" | wc -l | tr -d ' ')
177bytes=$(echo "$data" | wc -c | tr -d ' ')
178echo "$lines lines, $bytes bytes"
179echo 'Intent: {intent_str}'
180echo "$data" | head -3
181echo "..."
182echo "$data" | tail -3
183"#
184 )
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn handle_simple_python() {
195 let result = handle("python", "print(2 + 2)", None, None);
196 assert!(result.contains('4'));
197 assert!(result.contains("python"));
198 }
199
200 #[test]
201 fn handle_with_intent() {
202 let result = handle(
203 "python",
204 "print('found 5 errors')",
205 Some("count errors"),
206 None,
207 );
208 assert!(result.contains("found 5 errors"));
209 }
210
211 #[test]
212 fn handle_error_shows_stderr() {
213 let result = handle("python", "raise Exception('boom')", None, None);
214 assert!(result.contains("EXIT"));
215 assert!(result.contains("boom"));
216 }
217
218 #[test]
219 fn detect_language_from_path() {
220 assert_eq!(detect_language_from_extension("test.py"), "python");
221 assert_eq!(detect_language_from_extension("test.js"), "javascript");
222 assert_eq!(detect_language_from_extension("test.rs"), "rust");
223 assert_eq!(detect_language_from_extension("test.csv"), "python");
224 assert_eq!(detect_language_from_extension("test.log"), "python");
225 }
226
227 #[test]
228 #[cfg(not(target_os = "windows"))]
229 fn batch_multiple_tasks() {
230 let items = vec![
231 ("python".to_string(), "print('task1')".to_string()),
232 ("shell".to_string(), "echo task2".to_string()),
233 ];
234 let result = handle_batch(&items);
235 assert!(result.contains("task1"));
236 assert!(result.contains("task2"));
237 assert!(result.contains("2 tasks"));
238 }
239}