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