@@ -10,11 +10,11 @@ import (
13 "github.com/charmbracelet/crush/internal/csync"
14 "github.com/charmbracelet/crush/internal/csync"
14 "github.com/charmbracelet/crush/internal/diff"
15 "github.com/charmbracelet/crush/internal/diff"
15 "github.com/charmbracelet/crush/internal/fsext"
16 "github.com/charmbracelet/crush/internal/fsext"
16 "github.com/charmbracelet/crush/internal/history"
17 "github.com/charmbracelet/crush/internal/history"
17− "github.com/charmbracelet/fantasy/ai"
19 "github.com/charmbracelet/crush/internal/lsp"
19 "github.com/charmbracelet/crush/internal/lsp"
20 "github.com/charmbracelet/crush/internal/permission"
20 "github.com/charmbracelet/crush/internal/permission"
@@ -52,20 +52,20 @@ type editContext struct {
52 workingDir string
52 workingDir string
55−func NewEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) ai.AgentTool {
55+func NewEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) fantasy.AgentTool {
56− return ai.NewAgentTool(
56+ return fantasy.NewAgentTool(
57 EditToolName,
57 EditToolName,
58 string(editDescription),
58 string(editDescription),
59− func(ctx context.Context, params EditParams, call ai.ToolCall) (ai.ToolResponse, error) {
59+ func(ctx context.Context, params EditParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
60 if params.FilePath == "" {
60 if params.FilePath == "" {
61− return ai.NewTextErrorResponse("file_path is required"), nil
61+ return fantasy.NewTextErrorResponse("file_path is required"), nil
64 if !filepath.IsAbs(params.FilePath) {
64 if !filepath.IsAbs(params.FilePath) {
65 params.FilePath = filepath.Join(workingDir, params.FilePath)
65 params.FilePath = filepath.Join(workingDir, params.FilePath)
68− var response ai.ToolResponse
68+ var response fantasy.ToolResponse
69 var err error
69 var err error
71 editCtx := editContext{ctx, permissions, files, workingDir}
71 editCtx := editContext{ctx, permissions, files, workingDir}
@@ -103,25 +103,25 @@ func NewEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
106−func createNewFile(edit editContext, filePath, content string, call ai.ToolCall) (ai.ToolResponse, error) {
106+func createNewFile(edit editContext, filePath, content string, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
107 fileInfo, err := os.Stat(filePath)
107 fileInfo, err := os.Stat(filePath)
108 if err == nil {
108 if err == nil {
109 if fileInfo.IsDir() {
109 if fileInfo.IsDir() {
110− return ai.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
110+ return fantasy.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
112− return ai.NewTextErrorResponse(fmt.Sprintf("file already exists: %s", filePath)), nil
112+ return fantasy.NewTextErrorResponse(fmt.Sprintf("file already exists: %s", filePath)), nil
113 } else if !os.IsNotExist(err) {
113 } else if !os.IsNotExist(err) {
114− return ai.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
114+ return fantasy.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
117 dir := filepath.Dir(filePath)
117 dir := filepath.Dir(filePath)
118 if err = os.MkdirAll(dir, 0o755); err != nil {
118 if err = os.MkdirAll(dir, 0o755); err != nil {
119− return ai.ToolResponse{}, fmt.Errorf("failed to create parent directories: %w", err)
119+ return fantasy.ToolResponse{}, fmt.Errorf("failed to create parent directories: %w", err)
122 sessionID := GetSessionFromContext(edit.ctx)
122 sessionID := GetSessionFromContext(edit.ctx)
123 if sessionID == "" {
123 if sessionID == "" {
124− return ai.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
124+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
127 _, additions, removals := diff.GenerateDiff(
127 _, additions, removals := diff.GenerateDiff(
@@ -145,19 +145,19 @@ func createNewFile(edit editContext, filePath, content string, call ai.ToolCall)
148− return ai.ToolResponse{}, permission.ErrorPermissionDenied
148+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
151 err = os.WriteFile(filePath, []byte(content), 0o644)
151 err = os.WriteFile(filePath, []byte(content), 0o644)
152 if err != nil {
152 if err != nil {
153− return ai.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
153+ return fantasy.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
156 // File can't be in the history so we create a new file history
156 // File can't be in the history so we create a new file history
157 _, err = edit.files.Create(edit.ctx, sessionID, filePath, "")
157 _, err = edit.files.Create(edit.ctx, sessionID, filePath, "")
158 if err != nil {
158 if err != nil {
159 // Log error but don't fail the operation
159 // Log error but don't fail the operation
160− return ai.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
160+ return fantasy.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
163 // Add the new content to the file history
163 // Add the new content to the file history
@@ -170,8 +170,8 @@ func createNewFile(edit editContext, filePath, content string, call ai.ToolCall)
170 recordFileWrite(filePath)
170 recordFileWrite(filePath)
171 recordFileRead(filePath)
171 recordFileRead(filePath)
173− return ai.WithResponseMetadata(
173+ return fantasy.WithResponseMetadata(
174− ai.NewTextResponse("File created: "+filePath),
174+ fantasy.NewTextResponse("File created: "+filePath),
175 EditResponseMetadata{
175 EditResponseMetadata{
176 OldContent: "",
176 OldContent: "",
177 NewContent: content,
177 NewContent: content,
@@ -181,27 +181,27 @@ func createNewFile(edit editContext, filePath, content string, call ai.ToolCall)
184−func deleteContent(edit editContext, filePath, oldString string, replaceAll bool, call ai.ToolCall) (ai.ToolResponse, error) {
184+func deleteContent(edit editContext, filePath, oldString string, replaceAll bool, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
185 fileInfo, err := os.Stat(filePath)
185 fileInfo, err := os.Stat(filePath)
186 if err != nil {
186 if err != nil {
187 if os.IsNotExist(err) {
187 if os.IsNotExist(err) {
188− return ai.NewTextErrorResponse(fmt.Sprintf("file not found: %s", filePath)), nil
188+ return fantasy.NewTextErrorResponse(fmt.Sprintf("file not found: %s", filePath)), nil
190− return ai.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
190+ return fantasy.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
193 if fileInfo.IsDir() {
193 if fileInfo.IsDir() {
194− return ai.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
194+ return fantasy.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
197 if getLastReadTime(filePath).IsZero() {
197 if getLastReadTime(filePath).IsZero() {
198− return ai.NewTextErrorResponse("you must read the file before editing it. Use the View tool first"), nil
198+ return fantasy.NewTextErrorResponse("you must read the file before editing it. Use the View tool first"), nil
201 modTime := fileInfo.ModTime()
201 modTime := fileInfo.ModTime()
202 lastRead := getLastReadTime(filePath)
202 lastRead := getLastReadTime(filePath)
203 if modTime.After(lastRead) {
203 if modTime.After(lastRead) {
204− return ai.NewTextErrorResponse(
204+ return fantasy.NewTextErrorResponse(
205 fmt.Sprintf("file %s has been modified since it was last read (mod time: %s, last read: %s)",
205 fmt.Sprintf("file %s has been modified since it was last read (mod time: %s, last read: %s)",
206 filePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339),
206 filePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339),
@@ -209,7 +209,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
210 content, err := os.ReadFile(filePath)
210 content, err := os.ReadFile(filePath)
211 if err != nil {
211 if err != nil {
212− return ai.ToolResponse{}, fmt.Errorf("failed to read file: %w", err)
212+ return fantasy.ToolResponse{}, fmt.Errorf("failed to read file: %w", err)
215 oldContent, isCrlf := fsext.ToUnixLineEndings(string(content))
215 oldContent, isCrlf := fsext.ToUnixLineEndings(string(content))
@@ -221,17 +221,17 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
221 newContent = strings.ReplaceAll(oldContent, oldString, "")
221 newContent = strings.ReplaceAll(oldContent, oldString, "")
222 deletionCount = strings.Count(oldContent, oldString)
222 deletionCount = strings.Count(oldContent, oldString)
223 if deletionCount == 0 {
223 if deletionCount == 0 {
224− return ai.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
224+ return fantasy.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
227 index := strings.Index(oldContent, oldString)
227 index := strings.Index(oldContent, oldString)
228 if index == -1 {
228 if index == -1 {
229− return ai.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
229+ return fantasy.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
232 lastIndex := strings.LastIndex(oldContent, oldString)
232 lastIndex := strings.LastIndex(oldContent, oldString)
233 if index != lastIndex {
233 if index != lastIndex {
234− return ai.NewTextErrorResponse("old_string appears multiple times in the file. Please provide more context to ensure a unique match, or set replace_all to true"), nil
234+ return fantasy.NewTextErrorResponse("old_string appears multiple times in the file. Please provide more context to ensure a unique match, or set replace_all to true"), nil
237 newContent = oldContent[:index] + oldContent[index+len(oldString):]
237 newContent = oldContent[:index] + oldContent[index+len(oldString):]
@@ -241,7 +241,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
241 sessionID := GetSessionFromContext(edit.ctx)
241 sessionID := GetSessionFromContext(edit.ctx)
243 if sessionID == "" {
243 if sessionID == "" {
244− return ai.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
244+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
247 _, additions, removals := diff.GenerateDiff(
247 _, additions, removals := diff.GenerateDiff(
@@ -266,7 +266,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
269− return ai.ToolResponse{}, permission.ErrorPermissionDenied
269+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
272 if isCrlf {
272 if isCrlf {
@@ -275,7 +275,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
276 err = os.WriteFile(filePath, []byte(newContent), 0o644)
276 err = os.WriteFile(filePath, []byte(newContent), 0o644)
277 if err != nil {
277 if err != nil {
278− return ai.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
278+ return fantasy.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
281 // Check if file exists in history
281 // Check if file exists in history
@@ -284,7 +284,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
284 _, err = edit.files.Create(edit.ctx, sessionID, filePath, oldContent)
284 _, err = edit.files.Create(edit.ctx, sessionID, filePath, oldContent)
285 if err != nil {
285 if err != nil {
286 // Log error but don't fail the operation
286 // Log error but don't fail the operation
287− return ai.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
287+ return fantasy.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
290 if file.Content != oldContent {
290 if file.Content != oldContent {
@@ -303,8 +303,8 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
303 recordFileWrite(filePath)
303 recordFileWrite(filePath)
304 recordFileRead(filePath)
304 recordFileRead(filePath)
306− return ai.WithResponseMetadata(
306+ return fantasy.WithResponseMetadata(
307− ai.NewTextResponse("Content deleted from file: "+filePath),
307+ fantasy.NewTextResponse("Content deleted from file: "+filePath),
308 EditResponseMetadata{
308 EditResponseMetadata{
309 OldContent: oldContent,
309 OldContent: oldContent,
310 NewContent: newContent,
310 NewContent: newContent,
@@ -314,27 +314,27 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
317−func replaceContent(edit editContext, filePath, oldString, newString string, replaceAll bool, call ai.ToolCall) (ai.ToolResponse, error) {
317+func replaceContent(edit editContext, filePath, oldString, newString string, replaceAll bool, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
318 fileInfo, err := os.Stat(filePath)
318 fileInfo, err := os.Stat(filePath)
319 if err != nil {
319 if err != nil {
320 if os.IsNotExist(err) {
320 if os.IsNotExist(err) {
321− return ai.NewTextErrorResponse(fmt.Sprintf("file not found: %s", filePath)), nil
321+ return fantasy.NewTextErrorResponse(fmt.Sprintf("file not found: %s", filePath)), nil
323− return ai.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
323+ return fantasy.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
326 if fileInfo.IsDir() {
326 if fileInfo.IsDir() {
327− return ai.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
327+ return fantasy.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
330 if getLastReadTime(filePath).IsZero() {
330 if getLastReadTime(filePath).IsZero() {
331− return ai.NewTextErrorResponse("you must read the file before editing it. Use the View tool first"), nil
331+ return fantasy.NewTextErrorResponse("you must read the file before editing it. Use the View tool first"), nil
334 modTime := fileInfo.ModTime()
334 modTime := fileInfo.ModTime()
335 lastRead := getLastReadTime(filePath)
335 lastRead := getLastReadTime(filePath)
336 if modTime.After(lastRead) {
336 if modTime.After(lastRead) {
337− return ai.NewTextErrorResponse(
337+ return fantasy.NewTextErrorResponse(
338 fmt.Sprintf("file %s has been modified since it was last read (mod time: %s, last read: %s)",
338 fmt.Sprintf("file %s has been modified since it was last read (mod time: %s, last read: %s)",
339 filePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339),
339 filePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339),
@@ -342,7 +342,7 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
343 content, err := os.ReadFile(filePath)
343 content, err := os.ReadFile(filePath)
344 if err != nil {
344 if err != nil {
345− return ai.ToolResponse{}, fmt.Errorf("failed to read file: %w", err)
345+ return fantasy.ToolResponse{}, fmt.Errorf("failed to read file: %w", err)
348 oldContent, isCrlf := fsext.ToUnixLineEndings(string(content))
348 oldContent, isCrlf := fsext.ToUnixLineEndings(string(content))
@@ -354,17 +354,17 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
354 newContent = strings.ReplaceAll(oldContent, oldString, newString)
354 newContent = strings.ReplaceAll(oldContent, oldString, newString)
355 replacementCount = strings.Count(oldContent, oldString)
355 replacementCount = strings.Count(oldContent, oldString)
356 if replacementCount == 0 {
356 if replacementCount == 0 {
357− return ai.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
357+ return fantasy.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
360 index := strings.Index(oldContent, oldString)
360 index := strings.Index(oldContent, oldString)
361 if index == -1 {
361 if index == -1 {
362− return ai.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
362+ return fantasy.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
365 lastIndex := strings.LastIndex(oldContent, oldString)
365 lastIndex := strings.LastIndex(oldContent, oldString)
366 if index != lastIndex {
366 if index != lastIndex {
367− return ai.NewTextErrorResponse("old_string appears multiple times in the file. Please provide more context to ensure a unique match, or set replace_all to true"), nil
367+ return fantasy.NewTextErrorResponse("old_string appears multiple times in the file. Please provide more context to ensure a unique match, or set replace_all to true"), nil
370 newContent = oldContent[:index] + newString + oldContent[index+len(oldString):]
370 newContent = oldContent[:index] + newString + oldContent[index+len(oldString):]
@@ -372,12 +372,12 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
374 if oldContent == newContent {
374 if oldContent == newContent {
375− return ai.NewTextErrorResponse("new content is the same as old content. No changes made."), nil
375+ return fantasy.NewTextErrorResponse("new content is the same as old content. No changes made."), nil
377 sessionID := GetSessionFromContext(edit.ctx)
377 sessionID := GetSessionFromContext(edit.ctx)
379 if sessionID == "" {
379 if sessionID == "" {
380− return ai.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
380+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
382 _, additions, removals := diff.GenerateDiff(
382 _, additions, removals := diff.GenerateDiff(
383 oldContent,
383 oldContent,
@@ -401,7 +401,7 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
404− return ai.ToolResponse{}, permission.ErrorPermissionDenied
404+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
407 if isCrlf {
407 if isCrlf {
@@ -410,7 +410,7 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
411 err = os.WriteFile(filePath, []byte(newContent), 0o644)
411 err = os.WriteFile(filePath, []byte(newContent), 0o644)
412 if err != nil {
412 if err != nil {
413− return ai.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
413+ return fantasy.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
416 // Check if file exists in history
416 // Check if file exists in history
@@ -419,7 +419,7 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
419 _, err = edit.files.Create(edit.ctx, sessionID, filePath, oldContent)
419 _, err = edit.files.Create(edit.ctx, sessionID, filePath, oldContent)
420 if err != nil {
420 if err != nil {
421 // Log error but don't fail the operation
421 // Log error but don't fail the operation
422− return ai.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
422+ return fantasy.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
425 if file.Content != oldContent {
425 if file.Content != oldContent {
@@ -438,8 +438,8 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
438 recordFileWrite(filePath)
438 recordFileWrite(filePath)
439 recordFileRead(filePath)
439 recordFileRead(filePath)
441− return ai.WithResponseMetadata(
441+ return fantasy.WithResponseMetadata(
442− ai.NewTextResponse("Content replaced in file: "+filePath),
442+ fantasy.NewTextResponse("Content replaced in file: "+filePath),
443 EditResponseMetadata{
443 EditResponseMetadata{
444 OldContent: oldContent,
444 OldContent: oldContent,
445 NewContent: newContent,
445 NewContent: newContent,