refactor: centralize tool types in proto pkg and unmarshal permission params

Ayman Bagabas created

Change summary

internal/llm/tools/bash.go        |  23 +--
internal/llm/tools/diagnostics.go |   5 
internal/llm/tools/download.go    |  18 -
internal/llm/tools/edit.go        |  27 +---
internal/llm/tools/fetch.go       |  18 -
internal/llm/tools/glob.go        |  16 -
internal/llm/tools/grep.go        |  18 -
internal/llm/tools/ls.go          |  30 +---
internal/llm/tools/multiedit.go   |  32 +---
internal/llm/tools/sourcegraph.go |  19 +-
internal/llm/tools/tools.go       |  21 --
internal/llm/tools/view.go        |  24 +--
internal/llm/tools/write.go       |  24 +--
internal/proto/message.go         |   4 
internal/proto/permission.go      | 144 ++++++++++++++++++++++
internal/proto/tools.go           | 216 +++++++++++++++++++++++++++++++++
16 files changed, 445 insertions(+), 194 deletions(-)

Detailed changes

internal/llm/tools/bash.go 🔗

@@ -12,25 +12,16 @@ import (
 
 	"github.com/charmbracelet/crush/internal/config"
 	"github.com/charmbracelet/crush/internal/permission"
+	"github.com/charmbracelet/crush/internal/proto"
 	"github.com/charmbracelet/crush/internal/shell"
 )
 
-type BashParams struct {
-	Command string `json:"command"`
-	Timeout int    `json:"timeout"`
-}
-
-type BashPermissionsParams struct {
-	Command string `json:"command"`
-	Timeout int    `json:"timeout"`
-}
+type (
+	BashParams            = proto.BashParams
+	BashPermissionsParams = proto.BashPermissionsParams
+	BashResponseMetadata  = proto.BashResponseMetadata
+)
 
-type BashResponseMetadata struct {
-	StartTime        int64  `json:"start_time"`
-	EndTime          int64  `json:"end_time"`
-	Output           string `json:"output"`
-	WorkingDirectory string `json:"working_directory"`
-}
 type bashTool struct {
 	permissions permission.Service
 	workingDir  string
@@ -38,7 +29,7 @@ type bashTool struct {
 }
 
 const (
-	BashToolName = "bash"
+	BashToolName = proto.BashToolName
 
 	DefaultTimeout  = 1 * 60 * 1000  // 1 minutes in milliseconds
 	MaxTimeout      = 10 * 60 * 1000 // 10 minutes in milliseconds

internal/llm/tools/diagnostics.go 🔗

@@ -12,12 +12,11 @@ import (
 
 	"github.com/charmbracelet/crush/internal/csync"
 	"github.com/charmbracelet/crush/internal/lsp"
+	"github.com/charmbracelet/crush/internal/proto"
 	"github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
 )
 
-type DiagnosticsParams struct {
-	FilePath string `json:"file_path"`
-}
+type DiagnosticsParams = proto.DiagnosticsParams
 
 type diagnosticsTool struct {
 	lspClients *csync.Map[string, *lsp.Client]

internal/llm/tools/download.go 🔗

@@ -13,19 +13,13 @@ import (
 	"time"
 
 	"github.com/charmbracelet/crush/internal/permission"
+	"github.com/charmbracelet/crush/internal/proto"
 )
 
-type DownloadParams struct {
-	URL      string `json:"url"`
-	FilePath string `json:"file_path"`
-	Timeout  int    `json:"timeout,omitempty"`
-}
-
-type DownloadPermissionsParams struct {
-	URL      string `json:"url"`
-	FilePath string `json:"file_path"`
-	Timeout  int    `json:"timeout,omitempty"`
-}
+type (
+	DownloadParams            = proto.DownloadParams
+	DownloadPermissionsParams = proto.DownloadPermissionsParams
+)
 
 type downloadTool struct {
 	client      *http.Client
@@ -33,7 +27,7 @@ type downloadTool struct {
 	workingDir  string
 }
 
-const DownloadToolName = "download"
+const DownloadToolName = proto.DownloadToolName
 
 //go:embed download.md
 var downloadDescription []byte

internal/llm/tools/edit.go 🔗

@@ -15,30 +15,17 @@ import (
 	"github.com/charmbracelet/crush/internal/diff"
 	"github.com/charmbracelet/crush/internal/fsext"
 	"github.com/charmbracelet/crush/internal/history"
+	"github.com/charmbracelet/crush/internal/proto"
 
 	"github.com/charmbracelet/crush/internal/lsp"
 	"github.com/charmbracelet/crush/internal/permission"
 )
 
-type EditParams struct {
-	FilePath   string `json:"file_path"`
-	OldString  string `json:"old_string"`
-	NewString  string `json:"new_string"`
-	ReplaceAll bool   `json:"replace_all,omitempty"`
-}
-
-type EditPermissionsParams struct {
-	FilePath   string `json:"file_path"`
-	OldContent string `json:"old_content,omitempty"`
-	NewContent string `json:"new_content,omitempty"`
-}
-
-type EditResponseMetadata struct {
-	Additions  int    `json:"additions"`
-	Removals   int    `json:"removals"`
-	OldContent string `json:"old_content,omitempty"`
-	NewContent string `json:"new_content,omitempty"`
-}
+type (
+	EditParams            = proto.EditParams
+	EditPermissionsParams = proto.EditPermissionsParams
+	EditResponseMetadata  = proto.EditResponseMetadata
+)
 
 type editTool struct {
 	lspClients  *csync.Map[string, *lsp.Client]
@@ -47,7 +34,7 @@ type editTool struct {
 	workingDir  string
 }
 
-const EditToolName = "edit"
+const EditToolName = proto.EditToolName
 
 //go:embed edit.md
 var editDescription []byte

internal/llm/tools/fetch.go 🔗

@@ -14,19 +14,13 @@ import (
 	md "github.com/JohannesKaufmann/html-to-markdown"
 	"github.com/PuerkitoBio/goquery"
 	"github.com/charmbracelet/crush/internal/permission"
+	"github.com/charmbracelet/crush/internal/proto"
 )
 
-type FetchParams struct {
-	URL     string `json:"url"`
-	Format  string `json:"format"`
-	Timeout int    `json:"timeout,omitempty"`
-}
-
-type FetchPermissionsParams struct {
-	URL     string `json:"url"`
-	Format  string `json:"format"`
-	Timeout int    `json:"timeout,omitempty"`
-}
+type (
+	FetchParams            = proto.FetchParams
+	FetchPermissionsParams = proto.FetchPermissionsParams
+)
 
 type fetchTool struct {
 	client      *http.Client
@@ -34,7 +28,7 @@ type fetchTool struct {
 	workingDir  string
 }
 
-const FetchToolName = "fetch"
+const FetchToolName = proto.FetchToolName
 
 //go:embed fetch.md
 var fetchDescription []byte

internal/llm/tools/glob.go 🔗

@@ -13,22 +13,18 @@ import (
 	"strings"
 
 	"github.com/charmbracelet/crush/internal/fsext"
+	"github.com/charmbracelet/crush/internal/proto"
 )
 
-const GlobToolName = "glob"
+const GlobToolName = proto.GlobToolName
 
 //go:embed glob.md
 var globDescription []byte
 
-type GlobParams struct {
-	Pattern string `json:"pattern"`
-	Path    string `json:"path"`
-}
-
-type GlobResponseMetadata struct {
-	NumberOfFiles int  `json:"number_of_files"`
-	Truncated     bool `json:"truncated"`
-}
+type (
+	GlobParams           = proto.GlobParams
+	GlobResponseMetadata = proto.GlobResponseMetadata
+)
 
 type globTool struct {
 	workingDir string

internal/llm/tools/grep.go 🔗

@@ -18,6 +18,7 @@ import (
 	"time"
 
 	"github.com/charmbracelet/crush/internal/fsext"
+	"github.com/charmbracelet/crush/internal/proto"
 )
 
 // regexCache provides thread-safe caching of compiled regex patterns
@@ -70,13 +71,6 @@ var (
 	globBraceRegex = regexp.MustCompile(`\{([^}]+)\}`)
 )
 
-type GrepParams struct {
-	Pattern     string `json:"pattern"`
-	Path        string `json:"path"`
-	Include     string `json:"include"`
-	LiteralText bool   `json:"literal_text"`
-}
-
 type grepMatch struct {
 	path     string
 	modTime  time.Time
@@ -84,16 +78,16 @@ type grepMatch struct {
 	lineText string
 }
 
-type GrepResponseMetadata struct {
-	NumberOfMatches int  `json:"number_of_matches"`
-	Truncated       bool `json:"truncated"`
-}
+type (
+	GrepParams           = proto.GrepParams
+	GrepResponseMetadata = proto.GrepResponseMetadata
+)
 
 type grepTool struct {
 	workingDir string
 }
 
-const GrepToolName = "grep"
+const GrepToolName = proto.GrepToolName
 
 //go:embed grep.md
 var grepDescription []byte

internal/llm/tools/ls.go 🔗

@@ -11,29 +11,15 @@ import (
 
 	"github.com/charmbracelet/crush/internal/fsext"
 	"github.com/charmbracelet/crush/internal/permission"
+	"github.com/charmbracelet/crush/internal/proto"
 )
 
-type LSParams struct {
-	Path   string   `json:"path"`
-	Ignore []string `json:"ignore"`
-}
-
-type LSPermissionsParams struct {
-	Path   string   `json:"path"`
-	Ignore []string `json:"ignore"`
-}
-
-type TreeNode struct {
-	Name     string      `json:"name"`
-	Path     string      `json:"path"`
-	Type     string      `json:"type"` // "file" or "directory"
-	Children []*TreeNode `json:"children,omitempty"`
-}
-
-type LSResponseMetadata struct {
-	NumberOfFiles int  `json:"number_of_files"`
-	Truncated     bool `json:"truncated"`
-}
+type (
+	LSParams            = proto.LSParams
+	LSPermissionsParams = proto.LSPermissionsParams
+	LSResponseMetadata  = proto.LSResponseMetadata
+	TreeNode            = proto.TreeNode
+)
 
 type lsTool struct {
 	workingDir  string
@@ -41,7 +27,7 @@ type lsTool struct {
 }
 
 const (
-	LSToolName = "ls"
+	LSToolName = proto.LSToolName
 	MaxLSFiles = 1000
 )
 

internal/llm/tools/multiedit.go 🔗

@@ -17,32 +17,16 @@ import (
 	"github.com/charmbracelet/crush/internal/history"
 	"github.com/charmbracelet/crush/internal/lsp"
 	"github.com/charmbracelet/crush/internal/permission"
+	"github.com/charmbracelet/crush/internal/proto"
 )
 
-type MultiEditOperation struct {
-	OldString  string `json:"old_string"`
-	NewString  string `json:"new_string"`
-	ReplaceAll bool   `json:"replace_all,omitempty"`
-}
-
-type MultiEditParams struct {
-	FilePath string               `json:"file_path"`
-	Edits    []MultiEditOperation `json:"edits"`
-}
+type (
+	MultiEditOperation = proto.MultiEditOperation
+	MultiEditParams    = proto.MultiEditParams
 
-type MultiEditPermissionsParams struct {
-	FilePath   string `json:"file_path"`
-	OldContent string `json:"old_content,omitempty"`
-	NewContent string `json:"new_content,omitempty"`
-}
-
-type MultiEditResponseMetadata struct {
-	Additions    int    `json:"additions"`
-	Removals     int    `json:"removals"`
-	OldContent   string `json:"old_content,omitempty"`
-	NewContent   string `json:"new_content,omitempty"`
-	EditsApplied int    `json:"edits_applied"`
-}
+	MultiEditPermissionsParams = proto.MultiEditPermissionsParams
+	MultiEditResponseMetadata  = proto.MultiEditResponseMetadata
+)
 
 type multiEditTool struct {
 	lspClients  *csync.Map[string, *lsp.Client]
@@ -51,7 +35,7 @@ type multiEditTool struct {
 	workingDir  string
 }
 
-const MultiEditToolName = "multiedit"
+const MultiEditToolName = proto.MultiEditToolName
 
 //go:embed multiedit.md
 var multieditDescription []byte

internal/llm/tools/sourcegraph.go 🔗

@@ -10,25 +10,20 @@ import (
 	"net/http"
 	"strings"
 	"time"
-)
 
-type SourcegraphParams struct {
-	Query         string `json:"query"`
-	Count         int    `json:"count,omitempty"`
-	ContextWindow int    `json:"context_window,omitempty"`
-	Timeout       int    `json:"timeout,omitempty"`
-}
+	"github.com/charmbracelet/crush/internal/proto"
+)
 
-type SourcegraphResponseMetadata struct {
-	NumberOfMatches int  `json:"number_of_matches"`
-	Truncated       bool `json:"truncated"`
-}
+type (
+	SourcegraphParams           = proto.SourcegraphParams
+	SourcegraphResponseMetadata = proto.SourcegraphResponseMetadata
+)
 
 type sourcegraphTool struct {
 	client *http.Client
 }
 
-const SourcegraphToolName = "sourcegraph"
+const SourcegraphToolName = proto.SourcegraphToolName
 
 //go:embed sourcegraph.md
 var sourcegraphDescription []byte

internal/llm/tools/tools.go 🔗

@@ -3,6 +3,8 @@ package tools
 import (
 	"context"
 	"encoding/json"
+
+	"github.com/charmbracelet/crush/internal/proto"
 )
 
 type ToolInfo struct {
@@ -12,27 +14,20 @@ type ToolInfo struct {
 	Required    []string
 }
 
-type toolResponseType string
-
 type (
 	sessionIDContextKey string
 	messageIDContextKey string
 )
 
 const (
-	ToolResponseTypeText  toolResponseType = "text"
-	ToolResponseTypeImage toolResponseType = "image"
+	ToolResponseTypeText  = proto.ToolResponseTypeText
+	ToolResponseTypeImage = proto.ToolResponseTypeImage
 
 	SessionIDContextKey sessionIDContextKey = "session_id"
 	MessageIDContextKey messageIDContextKey = "message_id"
 )
 
-type ToolResponse struct {
-	Type     toolResponseType `json:"type"`
-	Content  string           `json:"content"`
-	Metadata string           `json:"metadata,omitempty"`
-	IsError  bool             `json:"is_error"`
-}
+type ToolResponse = proto.ToolResponse
 
 func NewTextResponse(content string) ToolResponse {
 	return ToolResponse{
@@ -60,11 +55,7 @@ func NewTextErrorResponse(content string) ToolResponse {
 	}
 }
 
-type ToolCall struct {
-	ID    string `json:"id"`
-	Name  string `json:"name"`
-	Input string `json:"input"`
-}
+type ToolCall = proto.ToolCall
 
 type BaseTool interface {
 	Info() ToolInfo

internal/llm/tools/view.go 🔗

@@ -15,22 +15,17 @@ import (
 	"github.com/charmbracelet/crush/internal/csync"
 	"github.com/charmbracelet/crush/internal/lsp"
 	"github.com/charmbracelet/crush/internal/permission"
+	"github.com/charmbracelet/crush/internal/proto"
 )
 
 //go:embed view.md
 var viewDescription []byte
 
-type ViewParams struct {
-	FilePath string `json:"file_path"`
-	Offset   int    `json:"offset"`
-	Limit    int    `json:"limit"`
-}
-
-type ViewPermissionsParams struct {
-	FilePath string `json:"file_path"`
-	Offset   int    `json:"offset"`
-	Limit    int    `json:"limit"`
-}
+type (
+	ViewParams            = proto.ViewParams
+	ViewPermissionsParams = proto.ViewPermissionsParams
+	ViewResponseMetadata  = proto.ViewResponseMetadata
+)
 
 type viewTool struct {
 	lspClients  *csync.Map[string, *lsp.Client]
@@ -38,13 +33,8 @@ type viewTool struct {
 	permissions permission.Service
 }
 
-type ViewResponseMetadata struct {
-	FilePath string `json:"file_path"`
-	Content  string `json:"content"`
-}
-
 const (
-	ViewToolName     = "view"
+	ViewToolName     = proto.ViewToolName
 	MaxReadSize      = 250 * 1024
 	DefaultReadLimit = 2000
 	MaxLineLength    = 2000

internal/llm/tools/write.go 🔗

@@ -15,6 +15,7 @@ import (
 	"github.com/charmbracelet/crush/internal/diff"
 	"github.com/charmbracelet/crush/internal/fsext"
 	"github.com/charmbracelet/crush/internal/history"
+	"github.com/charmbracelet/crush/internal/proto"
 
 	"github.com/charmbracelet/crush/internal/lsp"
 	"github.com/charmbracelet/crush/internal/permission"
@@ -23,16 +24,11 @@ import (
 //go:embed write.md
 var writeDescription []byte
 
-type WriteParams struct {
-	FilePath string `json:"file_path"`
-	Content  string `json:"content"`
-}
-
-type WritePermissionsParams struct {
-	FilePath   string `json:"file_path"`
-	OldContent string `json:"old_content,omitempty"`
-	NewContent string `json:"new_content,omitempty"`
-}
+type (
+	WriteParams            = proto.WriteParams
+	WritePermissionsParams = proto.WritePermissionsParams
+	WriteResponseMetadata  = proto.WriteResponseMetadata
+)
 
 type writeTool struct {
 	lspClients  *csync.Map[string, *lsp.Client]
@@ -41,13 +37,7 @@ type writeTool struct {
 	workingDir  string
 }
 
-type WriteResponseMetadata struct {
-	Diff      string `json:"diff"`
-	Additions int    `json:"additions"`
-	Removals  int    `json:"removals"`
-}
-
-const WriteToolName = "write"
+const WriteToolName = proto.WriteToolName
 
 func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) BaseTool {
 	return &writeTool{

internal/proto/message.go 🔗

@@ -125,8 +125,8 @@ type ToolCall struct {
 	ID       string `json:"id"`
 	Name     string `json:"name"`
 	Input    string `json:"input"`
-	Type     string `json:"type"`
-	Finished bool   `json:"finished"`
+	Type     string `json:"type,omitempty"`
+	Finished bool   `json:"finished,omitempty"`
 }
 
 func (ToolCall) isPart() {}

internal/proto/permission.go 🔗

@@ -1,5 +1,9 @@
 package proto
 
+import (
+	"encoding/json"
+)
+
 type CreatePermissionRequest struct {
 	SessionID   string `json:"session_id"`
 	ToolCallID  string `json:"tool_call_id"`
@@ -26,3 +30,143 @@ type PermissionRequest struct {
 	Params      any    `json:"params"`
 	Path        string `json:"path"`
 }
+
+// UnmarshalJSON implements the json.Unmarshaler interface. This is needed
+// because the Params field is of type any, so we need to unmarshal it into
+// it's appropriate type based on the [PermissionRequest.ToolName].
+func (p *PermissionRequest) UnmarshalJSON(data []byte) error {
+	type Alias PermissionRequest
+	aux := &struct {
+		Params json.RawMessage `json:"params"`
+		*Alias
+	}{
+		Alias: (*Alias)(p),
+	}
+	if err := json.Unmarshal(data, &aux); err != nil {
+		return err
+	}
+
+	switch p.ToolName {
+	case BashToolName:
+		var params BashPermissionsParams
+		if err := json.Unmarshal(aux.Params, &params); err != nil {
+			return err
+		}
+		p.Params = params
+	case DownloadToolName:
+		var params DownloadPermissionsParams
+		if err := json.Unmarshal(aux.Params, &params); err != nil {
+			return err
+		}
+		p.Params = params
+	case EditToolName:
+		var params EditPermissionsParams
+		if err := json.Unmarshal(aux.Params, &params); err != nil {
+			return err
+		}
+		p.Params = params
+	case WriteToolName:
+		var params WritePermissionsParams
+		if err := json.Unmarshal(aux.Params, &params); err != nil {
+			return err
+		}
+		p.Params = params
+	case MultiEditToolName:
+		var params MultiEditPermissionsParams
+		if err := json.Unmarshal(aux.Params, &params); err != nil {
+			return err
+		}
+		p.Params = params
+	case FetchToolName:
+		var params FetchPermissionsParams
+		if err := json.Unmarshal(aux.Params, &params); err != nil {
+			return err
+		}
+		p.Params = params
+	case ViewToolName:
+		var params ViewPermissionsParams
+		if err := json.Unmarshal(aux.Params, &params); err != nil {
+			return err
+		}
+		p.Params = params
+	case LSToolName:
+		var params LSPermissionsParams
+		if err := json.Unmarshal(aux.Params, &params); err != nil {
+			return err
+		}
+		p.Params = params
+	default:
+		panic("unknown tool name: " + p.ToolName)
+	}
+	return nil
+}
+
+// UnmarshalJSON implements the json.Unmarshaler interface. This is needed
+// because the Params field is of type any, so we need to unmarshal it into
+// it's appropriate type based on the [CreatePermissionRequest.ToolName].
+func (p *CreatePermissionRequest) UnmarshalJSON(data []byte) error {
+	type Alias CreatePermissionRequest
+	aux := &struct {
+		Params json.RawMessage `json:"params"`
+		*Alias
+	}{
+		Alias: (*Alias)(p),
+	}
+	if err := json.Unmarshal(data, &aux); err != nil {
+		return err
+	}
+
+	switch p.ToolName {
+	case BashToolName:
+		var params BashPermissionsParams
+		if err := json.Unmarshal(aux.Params, &params); err != nil {
+			return err
+		}
+		p.Params = params
+	case DownloadToolName:
+		var params DownloadPermissionsParams
+		if err := json.Unmarshal(aux.Params, &params); err != nil {
+			return err
+		}
+		p.Params = params
+	case EditToolName:
+		var params EditPermissionsParams
+		if err := json.Unmarshal(aux.Params, &params); err != nil {
+			return err
+		}
+		p.Params = params
+	case WriteToolName:
+		var params WritePermissionsParams
+		if err := json.Unmarshal(aux.Params, &params); err != nil {
+			return err
+		}
+		p.Params = params
+	case MultiEditToolName:
+		var params MultiEditPermissionsParams
+		if err := json.Unmarshal(aux.Params, &params); err != nil {
+			return err
+		}
+		p.Params = params
+	case FetchToolName:
+		var params FetchPermissionsParams
+		if err := json.Unmarshal(aux.Params, &params); err != nil {
+			return err
+		}
+		p.Params = params
+	case ViewToolName:
+		var params ViewPermissionsParams
+		if err := json.Unmarshal(aux.Params, &params); err != nil {
+			return err
+		}
+		p.Params = params
+	case LSToolName:
+		var params LSPermissionsParams
+		if err := json.Unmarshal(aux.Params, &params); err != nil {
+			return err
+		}
+		p.Params = params
+	default:
+		panic("unknown tool name: " + p.ToolName)
+	}
+	return nil
+}

internal/proto/tools.go 🔗

@@ -0,0 +1,216 @@
+package proto
+
+type ToolResponseType string
+
+const (
+	ToolResponseTypeText  ToolResponseType = "text"
+	ToolResponseTypeImage ToolResponseType = "image"
+)
+
+type ToolResponse struct {
+	Type     ToolResponseType `json:"type"`
+	Content  string           `json:"content"`
+	Metadata string           `json:"metadata,omitempty"`
+	IsError  bool             `json:"is_error"`
+}
+
+const (
+	BashToolName = "bash"
+)
+
+type BashParams struct {
+	Command string `json:"command"`
+	Timeout int    `json:"timeout"`
+}
+
+type BashPermissionsParams struct {
+	Command string `json:"command"`
+	Timeout int    `json:"timeout"`
+}
+
+type BashResponseMetadata struct {
+	StartTime        int64  `json:"start_time"`
+	EndTime          int64  `json:"end_time"`
+	Output           string `json:"output"`
+	WorkingDirectory string `json:"working_directory"`
+}
+
+type DiagnosticsParams struct {
+	FilePath string `json:"file_path"`
+}
+
+const DownloadToolName = "download"
+
+type DownloadParams struct {
+	URL      string `json:"url"`
+	FilePath string `json:"file_path"`
+	Timeout  int    `json:"timeout,omitempty"`
+}
+
+type DownloadPermissionsParams struct {
+	URL      string `json:"url"`
+	FilePath string `json:"file_path"`
+	Timeout  int    `json:"timeout,omitempty"`
+}
+
+const EditToolName = "edit"
+
+type EditParams struct {
+	FilePath   string `json:"file_path"`
+	OldString  string `json:"old_string"`
+	NewString  string `json:"new_string"`
+	ReplaceAll bool   `json:"replace_all,omitempty"`
+}
+
+type EditPermissionsParams struct {
+	FilePath   string `json:"file_path"`
+	OldContent string `json:"old_content,omitempty"`
+	NewContent string `json:"new_content,omitempty"`
+}
+
+type EditResponseMetadata struct {
+	Additions  int    `json:"additions"`
+	Removals   int    `json:"removals"`
+	OldContent string `json:"old_content,omitempty"`
+	NewContent string `json:"new_content,omitempty"`
+}
+
+const FetchToolName = "fetch"
+
+type FetchParams struct {
+	URL     string `json:"url"`
+	Format  string `json:"format"`
+	Timeout int    `json:"timeout,omitempty"`
+}
+
+type FetchPermissionsParams struct {
+	URL     string `json:"url"`
+	Format  string `json:"format"`
+	Timeout int    `json:"timeout,omitempty"`
+}
+
+const GlobToolName = "glob"
+
+type GlobParams struct {
+	Pattern string `json:"pattern"`
+	Path    string `json:"path"`
+}
+
+type GlobResponseMetadata struct {
+	NumberOfFiles int  `json:"number_of_files"`
+	Truncated     bool `json:"truncated"`
+}
+
+const GrepToolName = "grep"
+
+type GrepParams struct {
+	Pattern     string `json:"pattern"`
+	Path        string `json:"path"`
+	Include     string `json:"include"`
+	LiteralText bool   `json:"literal_text"`
+}
+type GrepResponseMetadata struct {
+	NumberOfMatches int  `json:"number_of_matches"`
+	Truncated       bool `json:"truncated"`
+}
+
+const LSToolName = "ls"
+
+type LSParams struct {
+	Path   string   `json:"path"`
+	Ignore []string `json:"ignore"`
+}
+
+type LSPermissionsParams struct {
+	Path   string   `json:"path"`
+	Ignore []string `json:"ignore"`
+}
+
+type TreeNode struct {
+	Name     string      `json:"name"`
+	Path     string      `json:"path"`
+	Type     string      `json:"type"` // "file" or "directory"
+	Children []*TreeNode `json:"children,omitempty"`
+}
+
+type LSResponseMetadata struct {
+	NumberOfFiles int  `json:"number_of_files"`
+	Truncated     bool `json:"truncated"`
+}
+
+const MultiEditToolName = "multiedit"
+
+type MultiEditOperation struct {
+	OldString  string `json:"old_string"`
+	NewString  string `json:"new_string"`
+	ReplaceAll bool   `json:"replace_all,omitempty"`
+}
+
+type MultiEditParams struct {
+	FilePath string               `json:"file_path"`
+	Edits    []MultiEditOperation `json:"edits"`
+}
+
+type MultiEditPermissionsParams struct {
+	FilePath   string `json:"file_path"`
+	OldContent string `json:"old_content,omitempty"`
+	NewContent string `json:"new_content,omitempty"`
+}
+
+type MultiEditResponseMetadata struct {
+	Additions    int    `json:"additions"`
+	Removals     int    `json:"removals"`
+	OldContent   string `json:"old_content,omitempty"`
+	NewContent   string `json:"new_content,omitempty"`
+	EditsApplied int    `json:"edits_applied"`
+}
+
+const SourcegraphToolName = "sourcegraph"
+
+type SourcegraphParams struct {
+	Query         string `json:"query"`
+	Count         int    `json:"count,omitempty"`
+	ContextWindow int    `json:"context_window,omitempty"`
+	Timeout       int    `json:"timeout,omitempty"`
+}
+
+type SourcegraphResponseMetadata struct {
+	NumberOfMatches int  `json:"number_of_matches"`
+	Truncated       bool `json:"truncated"`
+}
+
+const ViewToolName = "view"
+
+type ViewParams struct {
+	FilePath string `json:"file_path"`
+	Offset   int    `json:"offset"`
+	Limit    int    `json:"limit"`
+}
+
+type ViewPermissionsParams struct {
+	FilePath string `json:"file_path"`
+	Offset   int    `json:"offset"`
+	Limit    int    `json:"limit"`
+}
+type ViewResponseMetadata struct {
+	FilePath string `json:"file_path"`
+	Content  string `json:"content"`
+}
+
+const WriteToolName = "write"
+
+type WriteParams struct {
+	FilePath string `json:"file_path"`
+	Content  string `json:"content"`
+}
+
+type WritePermissionsParams struct {
+	FilePath   string `json:"file_path"`
+	OldContent string `json:"old_content,omitempty"`
+	NewContent string `json:"new_content,omitempty"`
+}
+type WriteResponseMetadata struct {
+	Diff      string `json:"diff"`
+	Additions int    `json:"additions"`
+	Removals  int    `json:"removals"`
+}