ls.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. package tools
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "github.com/kujtimiihoxha/termai/internal/config"
  10. )
  11. type lsTool struct{}
  12. const (
  13. LSToolName = "ls"
  14. MaxLSFiles = 1000
  15. )
  16. type LSParams struct {
  17. Path string `json:"path"`
  18. Ignore []string `json:"ignore"`
  19. }
  20. type TreeNode struct {
  21. Name string `json:"name"`
  22. Path string `json:"path"`
  23. Type string `json:"type"` // "file" or "directory"
  24. Children []*TreeNode `json:"children,omitempty"`
  25. }
  26. func (l *lsTool) Info() ToolInfo {
  27. return ToolInfo{
  28. Name: LSToolName,
  29. Description: lsDescription(),
  30. Parameters: map[string]any{
  31. "path": map[string]any{
  32. "type": "string",
  33. "description": "The path to the directory to list (defaults to current working directory)",
  34. },
  35. "ignore": map[string]any{
  36. "type": "array",
  37. "description": "List of glob patterns to ignore",
  38. "items": map[string]any{
  39. "type": "string",
  40. },
  41. },
  42. },
  43. Required: []string{"path"},
  44. }
  45. }
  46. // Run implements Tool.
  47. func (l *lsTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
  48. var params LSParams
  49. if err := json.Unmarshal([]byte(call.Input), &params); err != nil {
  50. return NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil
  51. }
  52. // If path is empty, use current working directory
  53. searchPath := params.Path
  54. if searchPath == "" {
  55. searchPath = config.WorkingDirectory()
  56. }
  57. // Ensure the path is absolute
  58. if !filepath.IsAbs(searchPath) {
  59. searchPath = filepath.Join(config.WorkingDirectory(), searchPath)
  60. }
  61. // Check if the path exists
  62. if _, err := os.Stat(searchPath); os.IsNotExist(err) {
  63. return NewTextErrorResponse(fmt.Sprintf("path does not exist: %s", searchPath)), nil
  64. }
  65. files, truncated, err := listDirectory(searchPath, params.Ignore, MaxLSFiles)
  66. if err != nil {
  67. return NewTextErrorResponse(fmt.Sprintf("error listing directory: %s", err)), nil
  68. }
  69. tree := createFileTree(files)
  70. output := printTree(tree, searchPath)
  71. if truncated {
  72. output = fmt.Sprintf("There are more than %d files in the directory. Use a more specific path or use the Glob tool to find specific files. The first %d files and directories are included below:\n\n%s", MaxLSFiles, MaxLSFiles, output)
  73. }
  74. return NewTextResponse(output), nil
  75. }
  76. func listDirectory(initialPath string, ignorePatterns []string, limit int) ([]string, bool, error) {
  77. var results []string
  78. truncated := false
  79. err := filepath.Walk(initialPath, func(path string, info os.FileInfo, err error) error {
  80. if err != nil {
  81. return nil // Skip files we don't have permission to access
  82. }
  83. if shouldSkip(path, ignorePatterns) {
  84. if info.IsDir() {
  85. return filepath.SkipDir
  86. }
  87. return nil
  88. }
  89. if path != initialPath {
  90. if info.IsDir() {
  91. path = path + string(filepath.Separator)
  92. }
  93. results = append(results, path)
  94. }
  95. if len(results) >= limit {
  96. truncated = true
  97. return filepath.SkipAll
  98. }
  99. return nil
  100. })
  101. if err != nil {
  102. return nil, truncated, err
  103. }
  104. return results, truncated, nil
  105. }
  106. func shouldSkip(path string, ignorePatterns []string) bool {
  107. base := filepath.Base(path)
  108. // Skip hidden files and directories
  109. if base != "." && strings.HasPrefix(base, ".") {
  110. return true
  111. }
  112. // Skip common directories and files
  113. commonIgnored := []string{
  114. "__pycache__",
  115. "node_modules",
  116. "dist",
  117. "build",
  118. "target",
  119. "vendor",
  120. "bin",
  121. "obj",
  122. ".git",
  123. ".idea",
  124. ".vscode",
  125. ".DS_Store",
  126. "*.pyc",
  127. "*.pyo",
  128. "*.pyd",
  129. "*.so",
  130. "*.dll",
  131. "*.exe",
  132. }
  133. // Skip __pycache__ directories
  134. if strings.Contains(path, filepath.Join("__pycache__", "")) {
  135. return true
  136. }
  137. // Check against common ignored patterns
  138. for _, ignored := range commonIgnored {
  139. if strings.HasSuffix(ignored, "/") {
  140. // Directory pattern
  141. if strings.Contains(path, filepath.Join(ignored[:len(ignored)-1], "")) {
  142. return true
  143. }
  144. } else if strings.HasPrefix(ignored, "*.") {
  145. // File extension pattern
  146. if strings.HasSuffix(base, ignored[1:]) {
  147. return true
  148. }
  149. } else {
  150. // Exact match
  151. if base == ignored {
  152. return true
  153. }
  154. }
  155. }
  156. // Check against ignore patterns
  157. for _, pattern := range ignorePatterns {
  158. matched, err := filepath.Match(pattern, base)
  159. if err == nil && matched {
  160. return true
  161. }
  162. }
  163. return false
  164. }
  165. func createFileTree(sortedPaths []string) []*TreeNode {
  166. root := []*TreeNode{}
  167. pathMap := make(map[string]*TreeNode)
  168. for _, path := range sortedPaths {
  169. parts := strings.Split(path, string(filepath.Separator))
  170. currentPath := ""
  171. var parentPath string
  172. var cleanParts []string
  173. for _, part := range parts {
  174. if part != "" {
  175. cleanParts = append(cleanParts, part)
  176. }
  177. }
  178. parts = cleanParts
  179. if len(parts) == 0 {
  180. continue
  181. }
  182. for i, part := range parts {
  183. if currentPath == "" {
  184. currentPath = part
  185. } else {
  186. currentPath = filepath.Join(currentPath, part)
  187. }
  188. if _, exists := pathMap[currentPath]; exists {
  189. parentPath = currentPath
  190. continue
  191. }
  192. isLastPart := i == len(parts)-1
  193. isDir := !isLastPart || strings.HasSuffix(path, string(filepath.Separator))
  194. nodeType := "file"
  195. if isDir {
  196. nodeType = "directory"
  197. }
  198. newNode := &TreeNode{
  199. Name: part,
  200. Path: currentPath,
  201. Type: nodeType,
  202. Children: []*TreeNode{},
  203. }
  204. pathMap[currentPath] = newNode
  205. if i > 0 && parentPath != "" {
  206. if parent, ok := pathMap[parentPath]; ok {
  207. parent.Children = append(parent.Children, newNode)
  208. }
  209. } else {
  210. root = append(root, newNode)
  211. }
  212. parentPath = currentPath
  213. }
  214. }
  215. return root
  216. }
  217. func printTree(tree []*TreeNode, rootPath string) string {
  218. var result strings.Builder
  219. result.WriteString(fmt.Sprintf("- %s%s\n", rootPath, string(filepath.Separator)))
  220. for _, node := range tree {
  221. printNode(&result, node, 1)
  222. }
  223. return result.String()
  224. }
  225. func printNode(builder *strings.Builder, node *TreeNode, level int) {
  226. indent := strings.Repeat(" ", level)
  227. nodeName := node.Name
  228. if node.Type == "directory" {
  229. nodeName += string(filepath.Separator)
  230. }
  231. fmt.Fprintf(builder, "%s- %s\n", indent, nodeName)
  232. if node.Type == "directory" && len(node.Children) > 0 {
  233. for _, child := range node.Children {
  234. printNode(builder, child, level+1)
  235. }
  236. }
  237. }
  238. func lsDescription() string {
  239. return `Directory listing tool that shows files and subdirectories in a tree structure, helping you explore and understand the project organization.
  240. WHEN TO USE THIS TOOL:
  241. - Use when you need to explore the structure of a directory
  242. - Helpful for understanding the organization of a project
  243. - Good first step when getting familiar with a new codebase
  244. HOW TO USE:
  245. - Provide a path to list (defaults to current working directory)
  246. - Optionally specify glob patterns to ignore
  247. - Results are displayed in a tree structure
  248. FEATURES:
  249. - Displays a hierarchical view of files and directories
  250. - Automatically skips hidden files/directories (starting with '.')
  251. - Skips common system directories like __pycache__
  252. - Can filter out files matching specific patterns
  253. LIMITATIONS:
  254. - Results are limited to 1000 files
  255. - Very large directories will be truncated
  256. - Does not show file sizes or permissions
  257. - Cannot recursively list all directories in a large project
  258. TIPS:
  259. - Use Glob tool for finding files by name patterns instead of browsing
  260. - Use Grep tool for searching file contents
  261. - Combine with other tools for more effective exploration`
  262. }
  263. func NewLsTool() BaseTool {
  264. return &lsTool{}
  265. }