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