@@ -9,6 +9,7 @@ import (
11 "github.com/charmbracelet/crush/internal/fsext"
11 "github.com/charmbracelet/crush/internal/fsext"
12+ "github.com/charmbracelet/crush/internal/permission"
14type LSParams struct {
15type LSParams struct {
@@ -16,6 +17,11 @@ type LSParams struct {
16 Ignore []string `json:"ignore"`
17 Ignore []string `json:"ignore"`
20+type LSPermissionsParams struct {
21+ Path string `json:"path"`
22+ Ignore []string `json:"ignore"`
19type TreeNode struct {
25type TreeNode struct {
20 Name string `json:"name"`
26 Name string `json:"name"`
21 Path string `json:"path"`
27 Path string `json:"path"`
@@ -29,7 +35,8 @@ type LSResponseMetadata struct {
31type lsTool struct {
37type lsTool struct {
32− workingDir string
38+ workingDir string
39+ permissions permission.Service
@@ -71,9 +78,10 @@ TIPS:
71- Combine with other tools for more effective exploration`
78- Combine with other tools for more effective exploration`
74−func NewLsTool(workingDir string) BaseTool {
81+func NewLsTool(permissions permission.Service, workingDir string) BaseTool {
75 return &lsTool{
82 return &lsTool{
76− workingDir: workingDir,
83+ workingDir: workingDir,
84+ permissions: permissions,
@@ -113,24 +121,61 @@ func (l *lsTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
113 searchPath = l.workingDir
121 searchPath = l.workingDir
125+ searchPath, err = fsext.Expand(searchPath)
127+ return ToolResponse{}, fmt.Errorf("error expanding path: %w", err)
116 if !filepath.IsAbs(searchPath) {
130 if !filepath.IsAbs(searchPath) {
117 searchPath = filepath.Join(l.workingDir, searchPath)
131 searchPath = filepath.Join(l.workingDir, searchPath)
120− if _, err := os.Stat(searchPath); os.IsNotExist(err) {
134+ // Check if directory is outside working directory and request permission if needed
121− return NewTextErrorResponse(fmt.Sprintf("path does not exist: %s", searchPath)), nil
135+ absWorkingDir, err := filepath.Abs(l.workingDir)
137+ return ToolResponse{}, fmt.Errorf("error resolving working directory: %w", err)
124− files, truncated, err := fsext.ListDirectory(searchPath, params.Ignore, MaxLSFiles)
140+ absSearchPath, err := filepath.Abs(searchPath)
125 if err != nil {
141 if err != nil {
126− return ToolResponse{}, fmt.Errorf("error listing directory: %w", err)
142+ return ToolResponse{}, fmt.Errorf("error resolving search path: %w", err)
129− tree := createFileTree(files)
145+ relPath, err := filepath.Rel(absWorkingDir, absSearchPath)
130− output := printTree(tree, searchPath)
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")
132− if truncated {
153+ granted := l.permissions.Request(
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)
154+ permission.CreatePermissionRequest{
155+ SessionID: sessionID,
156+ Path: absSearchPath,
157+ ToolCallID: call.ID,
158+ ToolName: LSToolName,
160+ Description: fmt.Sprintf("List directory outside working directory: %s", absSearchPath),
161+ Params: LSPermissionsParams(params),
166+ return ToolResponse{}, permission.ErrorPermissionDenied
170+ output, err := ListDirectoryTree(searchPath, params.Ignore)
172+ return ToolResponse{}, err
175+ // Get file count for metadata
176+ files, truncated, err := fsext.ListDirectory(searchPath, params.Ignore, MaxLSFiles)
178+ return ToolResponse{}, fmt.Errorf("error listing directory for metadata: %w", err)
136 return WithResponseMetadata(
181 return WithResponseMetadata(
@@ -142,6 +187,26 @@ func (l *lsTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
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)
195+ files, truncated, err := fsext.ListDirectory(searchPath, ignore, MaxLSFiles)
197+ return "", fmt.Errorf("error listing directory: %w", err)
200+ tree := createFileTree(files)
201+ output := printTree(tree, searchPath)
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)
145func createFileTree(sortedPaths []string) []*TreeNode {
210func createFileTree(sortedPaths []string) []*TreeNode {
146 root := []*TreeNode{}
211 root := []*TreeNode{}
147 pathMap := make(map[string]*TreeNode)
212 pathMap := make(map[string]*TreeNode)