返回 DeepSeek-Reasonix
ls.go
根目录 / internal / tool / builtin / ls.go
1 package builtin
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "os"
8 "path/filepath"
9 "strings"
10
11 "reasonix/internal/tool"
12 )
13
14 func init() { tool.RegisterBuiltin(listDir{}) }
15
16 // listDir lists a directory. workDir, when non-empty, is the directory a
17 // relative path resolves against (see resolveIn). paths resolves session-scoped
18 // read aliases for external folder refs. forbidRoots lists directories the tool
19 // may not list or recurse into.
20 type listDir struct {
21 workDir string
22 paths *PathResolver
23 forbidRoots []string
24 }
25
26 func (listDir) Name() string { return "ls" }
27
28 func (listDir) Description() string {
29 return "List the entries of a directory. Directories are shown with a trailing slash; files show their byte size. Set recursive=true to list all nested files depth-first (skips .git/node_modules)."
30 }
31
32 func (listDir) Schema() json.RawMessage {
33 return json.RawMessage(`{"type":"object","properties":{"path":{"type":"string","description":"Directory path (default \".\")"},"recursive":{"type":"boolean","description":"When true, recursively list all nested files (default false)"}}}`)
34 }
35
36 func (listDir) ReadOnly() bool { return true }
37
38 // SnipHint keeps a long head and short tail like grep/glob: the first entries
39 // matter most, the tail confirms scope.
40 func (listDir) SnipHint() tool.SnipHint {
41 return tool.SnipHint{Head: 80, Tail: 8, HeadChars: 10000, TailChars: 1000}
42 }
43
44 func (l listDir) Execute(ctx context.Context, args json.RawMessage) (string, error) {
45 p := struct {
46 Path string `json:"path"`
47 Recursive bool `json:"recursive"`
48 }{Path: "."}
49 if len(args) > 0 {
50 if err := json.Unmarshal(args, &p); err != nil {
51 return "", fmt.Errorf("invalid args: %w", err)
52 }
53 }
54 if p.Path == "" {
55 p.Path = "."
56 }
57 rp := resolveReadablePath(l.workDir, p.Path, l.paths)
58 p.Path = rp.Path
59 if confineRead(l.forbidRoots, p.Path) {
60 return "(empty directory)", nil
61 }
62
63 // Recursive mode: walk the whole tree depth-first.
64 if p.Recursive {
65 return l.listRecursive(p.Path, rp)
66 }
67
68 entries, err := os.ReadDir(p.Path)
69 if err != nil {
70 if rp.External {
71 return "", fmt.Errorf("ls %s: %s", rp.DisplayPath, rp.ErrorText(err))
72 }
73 return "", fmt.Errorf("ls %s: %w", rp.DisplayPath, err)
74 }
75
76 var b strings.Builder
77 for _, e := range entries {
78 if e.IsDir() {
79 fmt.Fprintf(&b, "%s/\n", e.Name())
80 continue
81 }
82 size := int64(-1)
83 if info, err := e.Info(); err == nil {
84 size = info.Size()
85 }
86 fmt.Fprintf(&b, "%s\t%d\n", e.Name(), size)
87 }
88 if b.Len() == 0 {
89 return "(empty directory)", nil
90 }
91 return b.String(), nil
92 }
93
94 // listRecursive walks a directory tree depth-first, skipping noise dirs.
95 // Depth is capped to guard against symlink loops.
96 func (l listDir) listRecursive(root string, rp ResolvedPath) (string, error) {
97 var b strings.Builder
98 err := filepath.WalkDir(root, func(p string, d os.DirEntry, wErr error) error {
99 if wErr != nil {
100 return wErr
101 }
102 if p == root {
103 return nil
104 }
105 if d.IsDir() {
106 switch d.Name() {
107 case ".git", "node_modules", ".DS_Store", "__pycache__", ".idea", ".vscode":
108 return filepath.SkipDir
109 }
110 if skipForbidDir(p, l.forbidRoots) {
111 return filepath.SkipDir
112 }
113 }
114 rel, rErr := filepath.Rel(root, p)
115 if rErr != nil {
116 rel = p
117 }
118 // Guard against excessive depth.
119 if strings.Count(rel, string(os.PathSeparator)) > 50 {
120 if d.IsDir() {
121 return filepath.SkipDir
122 }
123 return nil
124 }
125 rel = filepath.ToSlash(rel)
126 if d.IsDir() {
127 rel += "/"
128 } else if info, iErr := d.Info(); iErr == nil {
129 rel += fmt.Sprintf("\t%d", info.Size())
130 }
131 b.WriteString(rel + "\n")
132 return nil
133 })
134 if err != nil {
135 if rp.External {
136 return "", fmt.Errorf("ls -R %s: %s", rp.DisplayPath, rp.ErrorText(err))
137 }
138 return "", fmt.Errorf("ls -R %s: %w", rp.DisplayPath, err)
139 }
140 if b.Len() == 0 {
141 return "(empty directory tree)", nil
142 }
143 return b.String(), nil
144 }
145
145 lines GO