use super::state::ToolState;
use crate::core::{Tool, ToolArgs, ToolError, ToolResult};
use anyhow::Result;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use walkdir::WalkDir;
use super::search::ConfigurableFilter;
mod count_tokens;
pub use count_tokens::CountTokensTool;
pub struct ClassifyTaskTool {
name: String,
}
impl ClassifyTaskTool {
pub fn new() -> Self {
Self {
name: "classify_task".to_string(),
}
}
}
impl Tool for ClassifyTaskTool {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
"Classify a task request into one of the supported categories: bug_fix, feature, maintenance, or query"
}
fn signature(&self) -> &str {
"classify_task <task_type>"
}
fn validate_args(&self, args: &ToolArgs) -> Result<(), ToolError> {
if args.is_empty() {
return Err(ToolError::InvalidArgs {
message: "Usage: classify_task <task_type>. Valid types: bug_fix, feature, maintenance, query".to_string(),
});
}
let task_type = args.get_arg(0).unwrap();
match task_type.as_str() {
"bug_fix" | "feature" | "maintenance" | "query" => Ok(()),
_ => Err(ToolError::InvalidArgs {
message: format!(
"Invalid task type: {}. Valid types: bug_fix, feature, maintenance, query",
task_type
),
}),
}
}
fn execute(&mut self, args: &ToolArgs, state: &Arc<Mutex<ToolState>>) -> Result<ToolResult> {
let task_type = args.get_arg(0).unwrap();
{
let mut state_guard = state
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock state: {}", e))?;
state_guard.push_history(format!("Classified task as: {}", task_type));
}
Ok(ToolResult::success_with_data(
format!("Task classified as: {}", task_type),
serde_json::json!({
"task_type": task_type,
"action": "classify_task"
}),
))
}
fn get_parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"task_type": {
"type": "string",
"description": "The task type classification",
"enum": ["bug_fix", "feature", "maintenance", "query"]
}
},
"required": ["task_type"]
})
}
}
pub struct FilemapTool {
name: String,
}
impl FilemapTool {
pub fn new() -> Self {
Self {
name: "filemap".to_string(),
}
}
#[allow(dead_code)]
fn should_include_file(path: &Path) -> bool {
if let Some(name) = path.file_name() {
let name_str = name.to_string_lossy();
if name_str.starts_with('.') {
return false;
}
if matches!(
name_str.as_ref(),
"target"
| "node_modules"
| "__pycache__"
| "dist"
| "build"
| ".git"
| ".svn"
| ".hg"
| "venv"
| "env"
| ".venv"
) {
return false;
}
if let Some(ext) = path.extension() {
let ext_str = ext.to_string_lossy().to_lowercase();
if matches!(
ext_str.as_str(),
"exe"
| "dll"
| "so"
| "dylib"
| "a"
| "o"
| "pyc"
| "png"
| "jpg"
| "jpeg"
| "gif"
| "bmp"
| "ico"
| "mp3"
| "mp4"
| "avi"
| "mov"
| "wav"
| "pdf"
| "zip"
| "tar"
| "gz"
| "rar"
| "7z"
) {
return false;
}
}
}
true
}
fn generate_tree(path: &Path, max_depth: usize) -> Result<String> {
let mut result = String::new();
if path.is_file() {
return Self::show_file_content(path);
}
result.push_str(&format!("📁 {}\n", path.display()));
let filter = ConfigurableFilter::new(None);
let mut entries: Vec<_> = WalkDir::new(path)
.max_depth(max_depth)
.into_iter()
.filter_entry(|e| filter.should_include_path(e.path()))
.filter_map(|e| e.ok())
.filter(|e| e.path() != path)
.collect();
entries.sort_by(|a, b| a.path().cmp(b.path()));
let mut file_count = 0;
let mut dir_count = 0;
for entry in entries.iter().take(100) {
let depth = entry.depth();
let indent = " ".repeat(depth);
let name = entry.file_name().to_string_lossy();
if entry.file_type().is_dir() {
result.push_str(&format!("{}📁 {}/\n", indent, name));
dir_count += 1;
} else {
let size = entry.metadata().map(|m| m.len()).unwrap_or(0);
result.push_str(&format!("{}📄 {} ({} bytes)\n", indent, name, size));
file_count += 1;
}
}
if entries.len() > 100 {
result.push_str(&format!("... and {} more items\n", entries.len() - 100));
}
result.push_str(&format!(
"\nSummary: {} directories, {} files\n",
dir_count, file_count
));
Ok(result)
}
fn show_file_content(path: &Path) -> Result<String> {
let content =
fs::read_to_string(path).map_err(|e| anyhow::anyhow!("Failed to read file: {}", e))?;
let lines: Vec<&str> = content.lines().collect();
let mut result = String::new();
result.push_str(&format!("📄 {} ({} lines)\n", path.display(), lines.len()));
if let Some(ext) = path.extension() {
if ext == "py" {
return Self::show_python_file_content(path, &lines);
}
}
let mut current_line = 0;
while current_line < lines.len() {
let line = lines[current_line];
if Self::is_block_start(line) {
let block_end = Self::find_block_end(&lines, current_line);
if block_end - current_line > 5 {
result.push_str(&format!("{:4} | {}\n", current_line + 1, line));
result.push_str(&format!(
" | ... eliding lines {}-{} ...\n",
current_line + 2,
block_end
));
current_line = block_end;
} else {
for i in current_line..=block_end {
if i < lines.len() {
result.push_str(&format!("{:4} | {}\n", i + 1, lines[i]));
}
}
current_line = block_end + 1;
}
} else {
result.push_str(&format!("{:4} | {}\n", current_line + 1, line));
current_line += 1;
}
if result.len() > 50000 {
result.push_str("... (output truncated) ...\n");
break;
}
}
Ok(result)
}
fn show_python_file_content(path: &Path, lines: &[&str]) -> Result<String> {
let mut result = String::new();
result.push_str(&format!("📄 {} ({} lines)\n", path.display(), lines.len()));
let mut current_line = 0;
while current_line < lines.len() {
let line = lines[current_line];
let trimmed = line.trim_start();
if trimmed.starts_with("def ") || trimmed.starts_with("class ") {
let indent_level = line.len() - line.trim_start().len();
let mut end_line = current_line + 1;
let mut found_body = false;
while end_line < lines.len() {
let next_line = lines[end_line];
let next_trimmed = next_line.trim();
if next_trimmed.is_empty() || next_trimmed.starts_with('#') {
end_line += 1;
continue;
}
let next_indent = next_line.len() - next_line.trim_start().len();
if next_indent <= indent_level && found_body {
break;
}
if next_indent > indent_level {
found_body = true;
}
end_line += 1;
}
result.push_str(&format!("{:4} | {}\n", current_line + 1, line));
if end_line - current_line > 5 {
result.push_str(&format!(
" | ... eliding lines {}-{} ...\n",
current_line + 2,
end_line
));
} else {
for i in (current_line + 1)..end_line {
if i < lines.len() {
result.push_str(&format!("{:4} | {}\n", i + 1, lines[i]));
}
}
}
current_line = end_line;
} else {
result.push_str(&format!("{:4} | {}\n", current_line + 1, line));
current_line += 1;
}
if result.len() > 50000 {
result.push_str("... (output truncated) ...\n");
break;
}
}
Ok(result)
}
fn is_block_start(line: &str) -> bool {
let trimmed = line.trim_start();
trimmed.starts_with("def ") ||
trimmed.starts_with("class ") ||
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(":")) }
fn find_block_end(lines: &[&str], start: usize) -> usize {
if start >= lines.len() {
return start;
}
let start_line = lines[start];
let indent_level = start_line.len() - start_line.trim_start().len();
let mut end_line = start + 1;
let mut found_body = false;
while end_line < lines.len() {
let line = lines[end_line];
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with('#') {
end_line += 1;
continue;
}
let line_indent = line.len() - line.trim_start().len();
if line_indent <= indent_level && found_body {
break;
}
if line_indent > indent_level {
found_body = true;
}
end_line += 1;
}
end_line.saturating_sub(1)
}
}
impl Tool for FilemapTool {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
"Generate a file structure map or show file contents with abbreviated view"
}
fn signature(&self) -> &str {
"filemap <file_path>"
}
fn validate_args(&self, args: &ToolArgs) -> Result<(), ToolError> {
if args.is_empty() {
return Err(ToolError::InvalidArgs {
message: "Usage: filemap <file_path>".to_string(),
});
}
Ok(())
}
fn execute(&mut self, args: &ToolArgs, state: &Arc<Mutex<ToolState>>) -> Result<ToolResult> {
let file_path = args.get_arg(0).unwrap();
let path_buf = PathBuf::from(file_path);
if !path_buf.exists() {
return Ok(ToolResult::error(format!("Path not found: {}", file_path)));
}
let content = Self::generate_tree(&path_buf, 3)?;
{
let mut state_guard = state
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock state: {}", e))?;
state_guard.push_history(format!("Generated filemap for: {}", file_path));
}
Ok(ToolResult::success_with_data(
content,
serde_json::json!({
"path": file_path,
"type": if path_buf.is_file() { "file" } else { "directory" },
"action": "filemap"
}),
))
}
fn get_parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "The path to the file or directory to map"
}
},
"required": ["file_path"]
})
}
}
pub struct SubmitTool {
name: String,
}
impl SubmitTool {
pub fn new() -> Self {
Self {
name: "submit".to_string(),
}
}
}
impl Tool for SubmitTool {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
"Submit your completed task or solution"
}
fn signature(&self) -> &str {
"submit"
}
fn validate_args(&self, _args: &ToolArgs) -> Result<(), ToolError> {
Ok(()) }
fn execute(&mut self, _args: &ToolArgs, state: &Arc<Mutex<ToolState>>) -> Result<ToolResult> {
{
let mut state_guard = state
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock state: {}", e))?;
state_guard.push_history("Task submitted".to_string());
}
Ok(ToolResult::success_with_data(
"Task has been submitted successfully".to_string(),
serde_json::json!({
"action": "submit",
"status": "completed"
}),
))
}
fn get_parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {},
"required": []
})
}
}