| 1 | //! Project mapping tool for understanding codebase structure. |
| 2 | |
| 3 | use crate::utils::{is_key_file, project_tree, summarize_project}; |
| 4 | use anyhow::Result; |
| 5 | use async_trait::async_trait; |
| 6 | use serde::Serialize; |
| 7 | use serde_json::{Value, json}; |
| 8 | |
| 9 | use super::spec::{ |
| 10 | ApprovalRequirement, ToolCapability, ToolContext, ToolError, ToolResult, ToolSpec, optional_u64, |
| 11 | }; |
| 12 | |
| 13 | pub struct ProjectMapTool; |
| 14 | |
| 15 | #[derive(Debug, Serialize)] |
| 16 | struct ProjectMap { |
| 17 | tree: String, |
| 18 | summary: String, |
| 19 | key_files: Vec<String>, |
| 20 | } |
| 21 | |
| 22 | #[async_trait] |
| 23 | impl ToolSpec for ProjectMapTool { |
| 24 | fn name(&self) -> &'static str { |
| 25 | "project_map" |
| 26 | } |
| 27 | |
| 28 | fn description(&self) -> &'static str { |
| 29 | "Get a high-level map of the project structure, including key files and a tree view." |
| 30 | } |
| 31 | |
| 32 | fn input_schema(&self) -> Value { |
| 33 | json!({ |
| 34 | "type": "object", |
| 35 | "properties": { |
| 36 | "max_depth": { |
| 37 | "type": "integer", |
| 38 | "description": "Maximum depth for the tree view (default: 3)." |
| 39 | } |
| 40 | } |
| 41 | }) |
| 42 | } |
| 43 | |
| 44 | fn capabilities(&self) -> Vec<ToolCapability> { |
| 45 | vec![ToolCapability::ReadOnly, ToolCapability::Sandboxable] |
| 46 | } |
| 47 | |
| 48 | fn approval_requirement(&self) -> ApprovalRequirement { |
| 49 | ApprovalRequirement::Auto |
| 50 | } |
| 51 | |
| 52 | async fn execute(&self, input: Value, context: &ToolContext) -> Result<ToolResult, ToolError> { |
| 53 | let max_depth = optional_u64(&input, "max_depth", 3)? as usize; |
| 54 | let map = generate_project_map(&context.workspace, max_depth, context.follow_symlinks)?; |
| 55 | ToolResult::json(&map).map_err(|e| ToolError::execution_failed(e.to_string())) |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | fn generate_project_map( |
| 60 | root: &std::path::Path, |
| 61 | max_depth: usize, |
| 62 | follow_symlinks: bool, |
| 63 | ) -> Result<ProjectMap, ToolError> { |
| 64 | let tree = project_tree(root, max_depth, follow_symlinks); |
| 65 | let summary = summarize_project(root); |
| 66 | |
| 67 | // For key_files, we can just do a quick scan since summarize_project doesn't return them directly anymore |
| 68 | let mut key_files = Vec::new(); |
| 69 | let mut builder = ignore::WalkBuilder::new(root); |
| 70 | builder |
| 71 | .hidden(false) |
| 72 | .follow_links(follow_symlinks) |
| 73 | .max_depth(Some(2)); |
| 74 | let walker = builder.build(); |
| 75 | |
| 76 | for entry in walker.flatten() { |
| 77 | if entry.file_type().is_some_and(|ft| ft.is_symlink()) && !follow_symlinks { |
| 78 | continue; |
| 79 | } |
| 80 | if is_key_file(entry.path()) |
| 81 | && let Ok(rel) = entry.path().strip_prefix(root) |
| 82 | { |
| 83 | key_files.push(rel.to_string_lossy().to_string()); |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | Ok(ProjectMap { |
| 88 | tree, |
| 89 | summary, |
| 90 | key_files, |
| 91 | }) |
| 92 | } |
| 93 |