ls.go

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