1use crate::core::{Tool, ToolArgs, ToolError, ToolResult};
4use crate::state::ToolState;
5use anyhow::Result;
6use std::fs;
7use std::path::{Path, PathBuf};
8use std::sync::{Arc, Mutex};
9use walkdir::WalkDir;
10
11use crate::search::ConfigurableFilter;
12
13mod count_tokens;
14
15pub use count_tokens::CountTokensTool;
16
17pub struct ClassifyTaskTool {
19 name: String,
20}
21
22impl ClassifyTaskTool {
23 pub fn new() -> Self {
24 Self {
25 name: "classify_task".to_string(),
26 }
27 }
28}
29
30impl Tool for ClassifyTaskTool {
31 fn name(&self) -> &str {
32 &self.name
33 }
34
35 fn description(&self) -> &str {
36 "Classify a task request into one of the supported categories: bug_fix, feature, maintenance, or query"
37 }
38
39 fn signature(&self) -> &str {
40 "classify_task <task_type>"
41 }
42
43 fn validate_args(&self, args: &ToolArgs) -> Result<(), ToolError> {
44 if args.is_empty() {
45 return Err(ToolError::InvalidArgs {
46 message: "Usage: classify_task <task_type>. Valid types: bug_fix, feature, maintenance, query".to_string(),
47 });
48 }
49
50 let task_type = args.get_arg(0).unwrap();
51 match task_type.as_str() {
52 "bug_fix" | "feature" | "maintenance" | "query" => Ok(()),
53 _ => Err(ToolError::InvalidArgs {
54 message: format!(
55 "Invalid task type: {}. Valid types: bug_fix, feature, maintenance, query",
56 task_type
57 ),
58 }),
59 }
60 }
61
62 fn execute(&mut self, args: &ToolArgs, state: &Arc<Mutex<ToolState>>) -> Result<ToolResult> {
63 let task_type = args.get_arg(0).unwrap();
64
65 {
67 let mut state_guard = state
68 .lock()
69 .map_err(|e| anyhow::anyhow!("Failed to lock state: {}", e))?;
70 state_guard.push_history(format!("Classified task as: {}", task_type));
71 }
72
73 Ok(ToolResult::success_with_data(
74 format!("Task classified as: {}", task_type),
75 serde_json::json!({
76 "task_type": task_type,
77 "action": "classify_task"
78 }),
79 ))
80 }
81
82 fn get_parameters_schema(&self) -> serde_json::Value {
83 serde_json::json!({
84 "type": "object",
85 "properties": {
86 "task_type": {
87 "type": "string",
88 "description": "The task type classification",
89 "enum": ["bug_fix", "feature", "maintenance", "query"]
90 }
91 },
92 "required": ["task_type"]
93 })
94 }
95}
96
97pub struct FilemapTool {
98 name: String,
99}
100
101impl FilemapTool {
102 pub fn new() -> Self {
103 Self {
104 name: "filemap".to_string(),
105 }
106 }
107
108 #[allow(dead_code)]
110 fn should_include_file(path: &Path) -> bool {
111 if let Some(name) = path.file_name() {
112 let name_str = name.to_string_lossy();
113
114 if name_str.starts_with('.') {
116 return false;
117 }
118
119 if matches!(
121 name_str.as_ref(),
122 "target"
123 | "node_modules"
124 | "__pycache__"
125 | "dist"
126 | "build"
127 | ".git"
128 | ".svn"
129 | ".hg"
130 | "venv"
131 | "env"
132 | ".venv"
133 ) {
134 return false;
135 }
136
137 if let Some(ext) = path.extension() {
139 let ext_str = ext.to_string_lossy().to_lowercase();
140 if matches!(
141 ext_str.as_str(),
142 "exe"
143 | "dll"
144 | "so"
145 | "dylib"
146 | "a"
147 | "o"
148 | "pyc"
149 | "png"
150 | "jpg"
151 | "jpeg"
152 | "gif"
153 | "bmp"
154 | "ico"
155 | "mp3"
156 | "mp4"
157 | "avi"
158 | "mov"
159 | "wav"
160 | "pdf"
161 | "zip"
162 | "tar"
163 | "gz"
164 | "rar"
165 | "7z"
166 ) {
167 return false;
168 }
169 }
170 }
171
172 true
173 }
174
175 fn generate_tree(path: &Path, max_depth: usize) -> Result<String> {
177 let mut result = String::new();
178
179 if path.is_file() {
180 return Self::show_file_content(path);
182 }
183
184 result.push_str(&format!("📁 {}\n", path.display()));
185
186 let filter = ConfigurableFilter::new(None);
188
189 let mut entries: Vec<_> = WalkDir::new(path)
192 .max_depth(max_depth)
193 .into_iter()
194 .filter_entry(|e| filter.should_include_path(e.path()))
195 .filter_map(|e| e.ok())
196 .filter(|e| e.path() != path)
197 .collect();
198
199 entries.sort_by(|a, b| a.path().cmp(b.path()));
204
205 let mut file_count = 0;
206 let mut dir_count = 0;
207
208 for entry in entries.iter().take(100) {
209 let depth = entry.depth();
211 let indent = " ".repeat(depth);
212 let name = entry.file_name().to_string_lossy();
213
214 if entry.file_type().is_dir() {
215 result.push_str(&format!("{}📁 {}/\n", indent, name));
216 dir_count += 1;
217 } else {
218 let size = entry.metadata().map(|m| m.len()).unwrap_or(0);
219 result.push_str(&format!("{}📄 {} ({} bytes)\n", indent, name, size));
220 file_count += 1;
221 }
222 }
223
224 if entries.len() > 100 {
225 result.push_str(&format!("... and {} more items\n", entries.len() - 100));
226 }
227
228 result.push_str(&format!(
229 "\nSummary: {} directories, {} files\n",
230 dir_count, file_count
231 ));
232
233 Ok(result)
234 }
235
236 fn show_file_content(path: &Path) -> Result<String> {
238 let content =
239 fs::read_to_string(path).map_err(|e| anyhow::anyhow!("Failed to read file: {}", e))?;
240
241 let lines: Vec<&str> = content.lines().collect();
242 let mut result = String::new();
243
244 result.push_str(&format!("📄 {} ({} lines)\n", path.display(), lines.len()));
245
246 if let Some(ext) = path.extension() {
248 if ext == "py" {
249 return Self::show_python_file_content(path, &lines);
250 }
251 }
252
253 let mut current_line = 0;
255 while current_line < lines.len() {
256 let line = lines[current_line];
257
258 if Self::is_block_start(line) {
260 let block_end = Self::find_block_end(&lines, current_line);
262
263 if block_end - current_line > 5 {
265 result.push_str(&format!("{:4} | {}\n", current_line + 1, line));
266 result.push_str(&format!(
267 " | ... eliding lines {}-{} ...\n",
268 current_line + 2,
269 block_end
270 ));
271 current_line = block_end;
272 } else {
273 for i in current_line..=block_end {
275 if i < lines.len() {
276 result.push_str(&format!("{:4} | {}\n", i + 1, lines[i]));
277 }
278 }
279 current_line = block_end + 1;
280 }
281 } else {
282 result.push_str(&format!("{:4} | {}\n", current_line + 1, line));
283 current_line += 1;
284 }
285
286 if result.len() > 50000 {
288 result.push_str("... (output truncated) ...\n");
289 break;
290 }
291 }
292
293 Ok(result)
294 }
295
296 fn show_python_file_content(path: &Path, lines: &[&str]) -> Result<String> {
298 let mut result = String::new();
299 result.push_str(&format!("📄 {} ({} lines)\n", path.display(), lines.len()));
300
301 let mut current_line = 0;
302 while current_line < lines.len() {
303 let line = lines[current_line];
304 let trimmed = line.trim_start();
305
306 if trimmed.starts_with("def ") || trimmed.starts_with("class ") {
308 let indent_level = line.len() - line.trim_start().len();
309
310 let mut end_line = current_line + 1;
312 let mut found_body = false;
313
314 while end_line < lines.len() {
315 let next_line = lines[end_line];
316 let next_trimmed = next_line.trim();
317
318 if next_trimmed.is_empty() || next_trimmed.starts_with('#') {
320 end_line += 1;
321 continue;
322 }
323
324 let next_indent = next_line.len() - next_line.trim_start().len();
325
326 if next_indent <= indent_level && found_body {
328 break;
329 }
330
331 if next_indent > indent_level {
332 found_body = true;
333 }
334
335 end_line += 1;
336 }
337
338 result.push_str(&format!("{:4} | {}\n", current_line + 1, line));
340
341 if end_line - current_line > 5 {
343 result.push_str(&format!(
344 " | ... eliding lines {}-{} ...\n",
345 current_line + 2,
346 end_line
347 ));
348 } else {
349 for i in (current_line + 1)..end_line {
351 if i < lines.len() {
352 result.push_str(&format!("{:4} | {}\n", i + 1, lines[i]));
353 }
354 }
355 }
356
357 current_line = end_line;
358 } else {
359 result.push_str(&format!("{:4} | {}\n", current_line + 1, line));
360 current_line += 1;
361 }
362
363 if result.len() > 50000 {
365 result.push_str("... (output truncated) ...\n");
366 break;
367 }
368 }
369
370 Ok(result)
371 }
372
373 fn is_block_start(line: &str) -> bool {
375 let trimmed = line.trim_start();
376 trimmed.starts_with("def ") ||
377 trimmed.starts_with("class ") ||
378 trimmed.starts_with("fn ") || trimmed.starts_with("function ") || trimmed.starts_with("impl ") || (trimmed.starts_with("if ") && line.trim_end().ends_with(":")) || (trimmed.starts_with("for ") && line.trim_end().ends_with(":")) || (trimmed.starts_with("while ") && line.trim_end().ends_with(":")) }
385
386 fn find_block_end(lines: &[&str], start: usize) -> usize {
388 if start >= lines.len() {
389 return start;
390 }
391
392 let start_line = lines[start];
393 let indent_level = start_line.len() - start_line.trim_start().len();
394
395 let mut end_line = start + 1;
396 let mut found_body = false;
397
398 while end_line < lines.len() {
399 let line = lines[end_line];
400 let trimmed = line.trim();
401
402 if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with('#') {
404 end_line += 1;
405 continue;
406 }
407
408 let line_indent = line.len() - line.trim_start().len();
409
410 if line_indent <= indent_level && found_body {
412 break;
413 }
414
415 if line_indent > indent_level {
416 found_body = true;
417 }
418
419 end_line += 1;
420 }
421
422 end_line.saturating_sub(1)
423 }
424}
425
426impl Tool for FilemapTool {
427 fn name(&self) -> &str {
428 &self.name
429 }
430
431 fn description(&self) -> &str {
432 "Generate a file structure map or show file contents with abbreviated view"
433 }
434
435 fn signature(&self) -> &str {
436 "filemap <file_path>"
437 }
438
439 fn validate_args(&self, args: &ToolArgs) -> Result<(), ToolError> {
440 if args.is_empty() {
441 return Err(ToolError::InvalidArgs {
442 message: "Usage: filemap <file_path>".to_string(),
443 });
444 }
445 Ok(())
446 }
447
448 fn execute(&mut self, args: &ToolArgs, state: &Arc<Mutex<ToolState>>) -> Result<ToolResult> {
449 let file_path = args.get_arg(0).unwrap();
450 let path_buf = PathBuf::from(file_path);
451
452 if !path_buf.exists() {
454 return Ok(ToolResult::error(format!("Path not found: {}", file_path)));
455 }
456
457 let content = Self::generate_tree(&path_buf, 3)?; {
462 let mut state_guard = state
463 .lock()
464 .map_err(|e| anyhow::anyhow!("Failed to lock state: {}", e))?;
465 state_guard.push_history(format!("Generated filemap for: {}", file_path));
466 }
467
468 Ok(ToolResult::success_with_data(
469 content,
470 serde_json::json!({
471 "path": file_path,
472 "type": if path_buf.is_file() { "file" } else { "directory" },
473 "action": "filemap"
474 }),
475 ))
476 }
477
478 fn get_parameters_schema(&self) -> serde_json::Value {
479 serde_json::json!({
480 "type": "object",
481 "properties": {
482 "file_path": {
483 "type": "string",
484 "description": "The path to the file or directory to map"
485 }
486 },
487 "required": ["file_path"]
488 })
489 }
490}
491
492pub struct SubmitTool {
493 name: String,
494}
495
496impl SubmitTool {
497 pub fn new() -> Self {
498 Self {
499 name: "submit".to_string(),
500 }
501 }
502}
503
504impl Tool for SubmitTool {
505 fn name(&self) -> &str {
506 &self.name
507 }
508
509 fn description(&self) -> &str {
510 "Submit your completed task or solution"
511 }
512
513 fn signature(&self) -> &str {
514 "submit"
515 }
516
517 fn validate_args(&self, _args: &ToolArgs) -> Result<(), ToolError> {
518 Ok(()) }
520
521 fn execute(&mut self, _args: &ToolArgs, state: &Arc<Mutex<ToolState>>) -> Result<ToolResult> {
522 {
524 let mut state_guard = state
525 .lock()
526 .map_err(|e| anyhow::anyhow!("Failed to lock state: {}", e))?;
527 state_guard.push_history("Task submitted".to_string());
528 }
529
530 Ok(ToolResult::success_with_data(
531 "Task has been submitted successfully".to_string(),
532 serde_json::json!({
533 "action": "submit",
534 "status": "completed"
535 }),
536 ))
537 }
538
539 fn get_parameters_schema(&self) -> serde_json::Value {
540 serde_json::json!({
541 "type": "object",
542 "properties": {},
543 "required": []
544 })
545 }
546}