ls.go

  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), &params); 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}