ls.go 6.9 KB

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