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) Name() string {
78 return LSToolName
79}
80
81func (l *lsTool) Info() ToolInfo {
82 return ToolInfo{
83 Name: LSToolName,
84 Description: lsDescription,
85 Parameters: map[string]any{
86 "path": map[string]any{
87 "type": "string",
88 "description": "The path to the directory to list (defaults to current working directory)",
89 },
90 "ignore": map[string]any{
91 "type": "array",
92 "description": "List of glob patterns to ignore",
93 "items": map[string]any{
94 "type": "string",
95 },
96 },
97 },
98 Required: []string{"path"},
99 }
100}
101
102func (l *lsTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
103 var params LSParams
104 if err := json.Unmarshal([]byte(call.Input), ¶ms); err != nil {
105 return NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil
106 }
107
108 searchPath := params.Path
109 if searchPath == "" {
110 searchPath = config.WorkingDirectory()
111 }
112
113 if !filepath.IsAbs(searchPath) {
114 searchPath = filepath.Join(config.WorkingDirectory(), searchPath)
115 }
116
117 if _, err := os.Stat(searchPath); os.IsNotExist(err) {
118 return NewTextErrorResponse(fmt.Sprintf("path does not exist: %s", searchPath)), nil
119 }
120
121 files, truncated, err := fsext.ListDirectory(searchPath, params.Ignore, MaxLSFiles)
122 if err != nil {
123 return ToolResponse{}, fmt.Errorf("error listing directory: %w", err)
124 }
125
126 tree := createFileTree(files)
127 output := printTree(tree, searchPath)
128
129 if truncated {
130 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)
131 }
132
133 return WithResponseMetadata(
134 NewTextResponse(output),
135 LSResponseMetadata{
136 NumberOfFiles: len(files),
137 Truncated: truncated,
138 },
139 ), nil
140}
141
142func createFileTree(sortedPaths []string) []*TreeNode {
143 root := []*TreeNode{}
144 pathMap := make(map[string]*TreeNode)
145
146 for _, path := range sortedPaths {
147 parts := strings.Split(path, string(filepath.Separator))
148 currentPath := ""
149 var parentPath string
150
151 var cleanParts []string
152 for _, part := range parts {
153 if part != "" {
154 cleanParts = append(cleanParts, part)
155 }
156 }
157 parts = cleanParts
158
159 if len(parts) == 0 {
160 continue
161 }
162
163 for i, part := range parts {
164 if currentPath == "" {
165 currentPath = part
166 } else {
167 currentPath = filepath.Join(currentPath, part)
168 }
169
170 if _, exists := pathMap[currentPath]; exists {
171 parentPath = currentPath
172 continue
173 }
174
175 isLastPart := i == len(parts)-1
176 isDir := !isLastPart || strings.HasSuffix(path, string(filepath.Separator))
177 nodeType := "file"
178 if isDir {
179 nodeType = "directory"
180 }
181 newNode := &TreeNode{
182 Name: part,
183 Path: currentPath,
184 Type: nodeType,
185 Children: []*TreeNode{},
186 }
187
188 pathMap[currentPath] = newNode
189
190 if i > 0 && parentPath != "" {
191 if parent, ok := pathMap[parentPath]; ok {
192 parent.Children = append(parent.Children, newNode)
193 }
194 } else {
195 root = append(root, newNode)
196 }
197
198 parentPath = currentPath
199 }
200 }
201
202 return root
203}
204
205func printTree(tree []*TreeNode, rootPath string) string {
206 var result strings.Builder
207
208 result.WriteString(fmt.Sprintf("- %s%s\n", rootPath, string(filepath.Separator)))
209
210 for _, node := range tree {
211 printNode(&result, node, 1)
212 }
213
214 return result.String()
215}
216
217func printNode(builder *strings.Builder, node *TreeNode, level int) {
218 indent := strings.Repeat(" ", level)
219
220 nodeName := node.Name
221 if node.Type == "directory" {
222 nodeName = nodeName + string(filepath.Separator)
223 }
224
225 fmt.Fprintf(builder, "%s- %s\n", indent, nodeName)
226
227 if node.Type == "directory" && len(node.Children) > 0 {
228 for _, child := range node.Children {
229 printNode(builder, child, level+1)
230 }
231 }
232}