1package tools
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "os"
8 "path/filepath"
9 "strings"
10
11 "github.com/cloudwego/eino/components/tool"
12 "github.com/cloudwego/eino/schema"
13)
14
15type lsTool struct {
16 workingDir string
17}
18
19const (
20 LSToolName = "ls"
21
22 MaxFiles = 1000
23 TruncatedMessage = "There are more than 1000 files in the repository. Use the LS tool (passing a specific path), Bash tool, and other tools to explore nested directories. The first 1000 files and directories are included below:\n\n"
24)
25
26type LSParams struct {
27 Path string `json:"path"`
28 Ignore []string `json:"ignore"`
29}
30
31func (b *lsTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
32 return &schema.ToolInfo{
33 Name: LSToolName,
34 Desc: "Lists files and directories in a given path. The path parameter must be an absolute path, not a relative path. You can optionally provide an array of glob patterns to ignore with the ignore parameter. You should generally prefer the Glob and Grep tools, if you know which directories to search.",
35 ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
36 "path": {
37 Type: "string",
38 Desc: "The absolute path to the directory to list (must be absolute, not relative)",
39 Required: true,
40 },
41 "ignore": {
42 Type: "array",
43 ElemInfo: &schema.ParameterInfo{
44 Type: schema.String,
45 Desc: "List of glob patterns to ignore",
46 },
47 },
48 }),
49 }, nil
50}
51
52func (b *lsTool) InvokableRun(ctx context.Context, args string, opts ...tool.Option) (string, error) {
53 var params LSParams
54 if err := json.Unmarshal([]byte(args), ¶ms); err != nil {
55 return "", err
56 }
57
58 if !filepath.IsAbs(params.Path) {
59 return fmt.Sprintf("path must be absolute, got: %s", params.Path), nil
60 }
61
62 files, err := b.listDirectory(params.Path)
63 if err != nil {
64 return fmt.Sprintf("error listing directory: %s", err), nil
65 }
66
67 tree := createFileTree(files)
68 output := printTree(tree, params.Path)
69
70 if len(files) >= MaxFiles {
71 output = TruncatedMessage + output
72 }
73
74 return output, nil
75}
76
77func (b *lsTool) listDirectory(initialPath string) ([]string, error) {
78 var results []string
79
80 err := filepath.Walk(initialPath, func(path string, info os.FileInfo, err error) error {
81 if err != nil {
82 return nil // Skip files we don't have permission to access
83 }
84
85 if shouldSkip(path) {
86 if info.IsDir() {
87 return filepath.SkipDir
88 }
89 return nil
90 }
91
92 if path != initialPath {
93 if info.IsDir() {
94 path = path + string(filepath.Separator)
95 }
96
97 relPath, err := filepath.Rel(b.workingDir, path)
98 if err == nil {
99 results = append(results, relPath)
100 } else {
101 results = append(results, path)
102 }
103 }
104
105 if len(results) >= MaxFiles {
106 return fmt.Errorf("max files reached")
107 }
108
109 return nil
110 })
111
112 if err != nil && err.Error() != "max files reached" {
113 return nil, err
114 }
115
116 return results, nil
117}
118
119func shouldSkip(path string) bool {
120 base := filepath.Base(path)
121
122 if base != "." && strings.HasPrefix(base, ".") {
123 return true
124 }
125
126 if strings.Contains(path, filepath.Join("__pycache__", "")) {
127 return true
128 }
129
130 return false
131}
132
133type TreeNode struct {
134 Name string `json:"name"`
135 Path string `json:"path"`
136 Type string `json:"type"` // "file" or "directory"
137 Children []TreeNode `json:"children,omitempty"`
138}
139
140func createFileTree(sortedPaths []string) []TreeNode {
141 root := []TreeNode{}
142
143 for _, path := range sortedPaths {
144 parts := strings.Split(path, string(filepath.Separator))
145 currentLevel := &root
146 currentPath := ""
147
148 for i, part := range parts {
149 if part == "" {
150 continue
151 }
152
153 if currentPath == "" {
154 currentPath = part
155 } else {
156 currentPath = filepath.Join(currentPath, part)
157 }
158
159 isLastPart := i == len(parts)-1
160 isDir := !isLastPart || strings.HasSuffix(path, string(filepath.Separator))
161
162 found := false
163 for i := range *currentLevel {
164 if (*currentLevel)[i].Name == part {
165 found = true
166 if (*currentLevel)[i].Children != nil {
167 currentLevel = &(*currentLevel)[i].Children
168 }
169 break
170 }
171 }
172
173 if !found {
174 nodeType := "file"
175 if isDir {
176 nodeType = "directory"
177 }
178
179 newNode := TreeNode{
180 Name: part,
181 Path: currentPath,
182 Type: nodeType,
183 }
184
185 if isDir {
186 newNode.Children = []TreeNode{}
187 *currentLevel = append(*currentLevel, newNode)
188 currentLevel = &(*currentLevel)[len(*currentLevel)-1].Children
189 } else {
190 *currentLevel = append(*currentLevel, newNode)
191 }
192 }
193 }
194 }
195
196 return root
197}
198
199func printTree(tree []TreeNode, rootPath string) string {
200 var result strings.Builder
201
202 result.WriteString(fmt.Sprintf("- %s%s\n", rootPath, string(filepath.Separator)))
203
204 printTreeRecursive(&result, tree, 0, " ")
205
206 return result.String()
207}
208
209func printTreeRecursive(builder *strings.Builder, tree []TreeNode, level int, prefix string) {
210 for _, node := range tree {
211 linePrefix := prefix + "- "
212
213 nodeName := node.Name
214 if node.Type == "directory" {
215 nodeName += string(filepath.Separator)
216 }
217 fmt.Fprintf(builder, "%s%s\n", linePrefix, nodeName)
218
219 if node.Type == "directory" && len(node.Children) > 0 {
220 printTreeRecursive(builder, node.Children, level+1, prefix+" ")
221 }
222 }
223}
224
225func NewLsTool(workingDir string) tool.InvokableTool {
226 return &lsTool{
227 workingDir,
228 }
229}