@@ -9,6 +9,7 @@ import (
99 "strings"
1010
1111 "github.com/charmbracelet/crush/internal/fsext"
12+ "github.com/charmbracelet/crush/internal/permission"
1213)
1314
1415type LSParams struct {
@@ -16,6 +17,11 @@ type LSParams struct {
1617 Ignore []string `json:"ignore"`
1718}
1819
20+type LSPermissionsParams struct {
21+ Path string `json:"path"`
22+ Ignore []string `json:"ignore"`
23+}
24+
1925type TreeNode struct {
2026 Name string `json:"name"`
2127 Path string `json:"path"`
@@ -29,7 +35,8 @@ type LSResponseMetadata struct {
2935}
3036
3137type lsTool struct {
32− workingDir string
38+ workingDir string
39+ permissions permission.Service
3340}
3441
3542const (
@@ -71,9 +78,10 @@ TIPS:
7178- Combine with other tools for more effective exploration`
7279)
7380
74−func NewLsTool(workingDir string) BaseTool {
81+func NewLsTool(permissions permission.Service, workingDir string) BaseTool {
7582 return &lsTool{
76− workingDir: workingDir,
83+ workingDir: workingDir,
84+ permissions: permissions,
7785 }
7886}
7987
@@ -113,24 +121,61 @@ func (l *lsTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
113121 searchPath = l.workingDir
114122 }
115123
124+ var err error
125+ searchPath, err = fsext.Expand(searchPath)
126+ if err != nil {
127+ return ToolResponse{}, fmt.Errorf("error expanding path: %w", err)
128+ }
129+
116130 if !filepath.IsAbs(searchPath) {
117131 searchPath = filepath.Join(l.workingDir, searchPath)
118132 }
119133
120− if _, err := os.Stat(searchPath); os.IsNotExist(err) {
121− return NewTextErrorResponse(fmt.Sprintf("path does not exist: %s", searchPath)), nil
134+ // Check if directory is outside working directory and request permission if needed
135+ absWorkingDir, err := filepath.Abs(l.workingDir)
136+ if err != nil {
137+ return ToolResponse{}, fmt.Errorf("error resolving working directory: %w", err)
122138 }
123139
124− files, truncated, err := fsext.ListDirectory(searchPath, params.Ignore, MaxLSFiles)
140+ absSearchPath, err := filepath.Abs(searchPath)
125141 if err != nil {
126− return ToolResponse{}, fmt.Errorf("error listing directory: %w", err)
142+ return ToolResponse{}, fmt.Errorf("error resolving search path: %w", err)
127143 }
128144
129− tree := createFileTree(files)
130− output := printTree(tree, searchPath)
145+ relPath, err := filepath.Rel(absWorkingDir, absSearchPath)
146+ if err != nil || strings.HasPrefix(relPath, "..") {
147+ // Directory is outside working directory, request permission
148+ sessionID, messageID := GetContextValues(ctx)
149+ if sessionID == "" || messageID == "" {
150+ return ToolResponse{}, fmt.Errorf("session ID and message ID are required for accessing directories outside working directory")
151+ }
131152
132− if truncated {
133− 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)
153+ granted := l.permissions.Request(
154+ permission.CreatePermissionRequest{
155+ SessionID: sessionID,
156+ Path: absSearchPath,
157+ ToolCallID: call.ID,
158+ ToolName: LSToolName,
159+ Action: "list",
160+ Description: fmt.Sprintf("List directory outside working directory: %s", absSearchPath),
161+ Params: LSPermissionsParams(params),
162+ },
163+ )
164+
165+ if !granted {
166+ return ToolResponse{}, permission.ErrorPermissionDenied
167+ }
168+ }
169+
170+ output, err := ListDirectoryTree(searchPath, params.Ignore)
171+ if err != nil {
172+ return ToolResponse{}, err
173+ }
174+
175+ // Get file count for metadata
176+ files, truncated, err := fsext.ListDirectory(searchPath, params.Ignore, MaxLSFiles)
177+ if err != nil {
178+ return ToolResponse{}, fmt.Errorf("error listing directory for metadata: %w", err)
134179 }
135180
136181 return WithResponseMetadata(
@@ -142,6 +187,26 @@ func (l *lsTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
142187 ), nil
143188}
144189
190+func ListDirectoryTree(searchPath string, ignore []string) (string, error) {
191+ if _, err := os.Stat(searchPath); os.IsNotExist(err) {
192+ return "", fmt.Errorf("path does not exist: %s", searchPath)
193+ }
194+
195+ files, truncated, err := fsext.ListDirectory(searchPath, ignore, MaxLSFiles)
196+ if err != nil {
197+ return "", fmt.Errorf("error listing directory: %w", err)
198+ }
199+
200+ tree := createFileTree(files)
201+ output := printTree(tree, searchPath)
202+
203+ if truncated {
204+ 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)
205+ }
206+
207+ return output, nil
208+}
209+
145210func createFileTree(sortedPaths []string) []*TreeNode {
146211 root := []*TreeNode{}
147212 pathMap := make(map[string]*TreeNode)