on main help sign in
chore: make tools config independent
commita07dd6bc0d29329fb0cf2dffd46a9521f7b43849 authorKujtim Hoxha <kujtimii.h@gmail.com> dateSat 5 Jul 2025, 17:47 (1 year ago) parentf491a6d
12 files changed+80−64
cmd/root.go+3−3 internal/llm/agent/agent.go+10−9 internal/llm/agent/mcp-tools.go+11−9 internal/llm/prompt/coder.go+1−1 internal/llm/tools/bash.go+5−4 internal/llm/tools/edit.go+10−10 internal/llm/tools/fetch.go+4−3 internal/llm/tools/glob.go+8−5 internal/llm/tools/grep.go+8−5 internal/llm/tools/ls.go+9−6 internal/llm/tools/view.go+5−4 internal/llm/tools/write.go+6−5
cmd/root.go+3−3
@@ -100,7 +100,7 @@ to assist developers in writing, debugging, and understanding code directly from
100100 defer app.Shutdown()
101101
102102 // Initialize MCP tools early for both modes
103− initMCPTools(ctx, app)
103+ initMCPTools(ctx, app, cfg)
104104
105105 prompt, err = maybePrependStdin(prompt)
106106 if err != nil {
@@ -192,7 +192,7 @@ func attemptTUIRecovery(program *tea.Program) {
192192 program.Quit()
193193}
194194
195−func initMCPTools(ctx context.Context, app *app.App) {
195+func initMCPTools(ctx context.Context, app *app.App, cfg *config.Config) {
196196 go func() {
197197 defer log.RecoverPanic("MCP-goroutine", nil)
198198
@@ -201,7 +201,7 @@ func initMCPTools(ctx context.Context, app *app.App) {
201201 defer cancel()
202202
203203 // Set this up once with proper error handling
204− agent.GetMcpTools(ctxWithTimeout, app.Permissions)
204+ agent.GetMcpTools(ctxWithTimeout, app.Permissions, cfg)
205205 slog.Info("MCP message handling goroutine exiting")
206206 }()
207207}
internal/llm/agent/agent.go+10−9
@@ -94,21 +94,22 @@ func NewAgent(
9494) (Service, error) {
9595 ctx := context.Background()
9696 cfg := config.Get()
97− otherTools := GetMcpTools(ctx, permissions)
97+ otherTools := GetMcpTools(ctx, permissions, cfg)
9898 if len(lspClients) > 0 {
9999 otherTools = append(otherTools, tools.NewDiagnosticsTool(lspClients))
100100 }
101101
102+ cwd := cfg.WorkingDir()
102103 allTools := []tools.BaseTool{
103− tools.NewBashTool(permissions),
104− tools.NewEditTool(lspClients, permissions, history),
105− tools.NewFetchTool(permissions),
106− tools.NewGlobTool(),
107− tools.NewGrepTool(),
108− tools.NewLsTool(),
104+ tools.NewBashTool(permissions, cwd),
105+ tools.NewEditTool(lspClients, permissions, history, cwd),
106+ tools.NewFetchTool(permissions, cwd),
107+ tools.NewGlobTool(cwd),
108+ tools.NewGrepTool(cwd),
109+ tools.NewLsTool(cwd),
109110 tools.NewSourcegraphTool(),
110− tools.NewViewTool(lspClients),
111− tools.NewWriteTool(lspClients, permissions, history),
111+ tools.NewViewTool(lspClients, cwd),
112+ tools.NewWriteTool(lspClients, permissions, history, cwd),
112113 }
113114
114115 if agentCfg.ID == "coder" {
internal/llm/agent/mcp-tools.go+11−9
@@ -22,6 +22,7 @@ type mcpTool struct {
2222 tool mcp.Tool
2323 mcpConfig config.MCPConfig
2424 permissions permission.Service
25+ workingDir string
2526}
2627
2728type MCPClient interface {
@@ -98,7 +99,7 @@ func (b *mcpTool) Run(ctx context.Context, params tools.ToolCall) (tools.ToolRes
9899 p := b.permissions.Request(
99100 permission.CreatePermissionRequest{
100101 SessionID: sessionID,
101− Path: config.Get().WorkingDir(),
102+ Path: b.workingDir,
102103 ToolName: b.Info().Name,
103104 Action: "execute",
104105 Description: permissionDescription,
@@ -143,18 +144,19 @@ func (b *mcpTool) Run(ctx context.Context, params tools.ToolCall) (tools.ToolRes
143144 return tools.NewTextErrorResponse("invalid mcp type"), nil
144145}
145146
146−func NewMcpTool(name string, tool mcp.Tool, permissions permission.Service, mcpConfig config.MCPConfig) tools.BaseTool {
147+func NewMcpTool(name string, tool mcp.Tool, permissions permission.Service, mcpConfig config.MCPConfig, workingDir string) tools.BaseTool {
147148 return &mcpTool{
148149 mcpName: name,
149150 tool: tool,
150151 mcpConfig: mcpConfig,
151152 permissions: permissions,
153+ workingDir: workingDir,
152154 }
153155}
154156
155157var mcpTools []tools.BaseTool
156158
157−func getTools(ctx context.Context, name string, m config.MCPConfig, permissions permission.Service, c MCPClient) []tools.BaseTool {
159+func getTools(ctx context.Context, name string, m config.MCPConfig, permissions permission.Service, c MCPClient, workingDir string) []tools.BaseTool {
158160 var stdioTools []tools.BaseTool
159161 initRequest := mcp.InitializeRequest{}
160162 initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
@@ -175,17 +177,17 @@ func getTools(ctx context.Context, name string, m config.MCPConfig, permissions
175177 return stdioTools
176178 }
177179 for _, t := range tools.Tools {
178− stdioTools = append(stdioTools, NewMcpTool(name, t, permissions, m))
180+ stdioTools = append(stdioTools, NewMcpTool(name, t, permissions, m, workingDir))
179181 }
180182 defer c.Close()
181183 return stdioTools
182184}
183185
184−func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.BaseTool {
186+func GetMcpTools(ctx context.Context, permissions permission.Service, cfg *config.Config) []tools.BaseTool {
185187 if len(mcpTools) > 0 {
186188 return mcpTools
187189 }
188− for name, m := range config.Get().MCP {
190+ for name, m := range cfg.MCP {
189191 switch m.Type {
190192 case config.MCPStdio:
191193 c, err := client.NewStdioMCPClient(
@@ -198,7 +200,7 @@ func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.Ba
198200 continue
199201 }
200202
201− mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...)
203+ mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c, cfg.WorkingDir())...)
202204 case config.MCPHttp:
203205 c, err := client.NewStreamableHttpClient(
204206 m.URL,
@@ -208,7 +210,7 @@ func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.Ba
208210 slog.Error("error creating mcp client", "error", err)
209211 continue
210212 }
211− mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...)
213+ mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c, cfg.WorkingDir())...)
212214 case config.MCPSse:
213215 c, err := client.NewSSEMCPClient(
214216 m.URL,
@@ -218,7 +220,7 @@ func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.Ba
218220 slog.Error("error creating mcp client", "error", err)
219221 continue
220222 }
221− mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...)
223+ mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c, cfg.WorkingDir())...)
222224 }
223225 }
224226
internal/llm/prompt/coder.go+1−1
@@ -384,7 +384,7 @@ func getEnvironmentInfo() string {
384384 isGit := isGitRepo(cwd)
385385 platform := runtime.GOOS
386386 date := time.Now().Format("1/2/2006")
387− ls := tools.NewLsTool()
387+ ls := tools.NewLsTool(cwd)
388388 r, _ := ls.Run(context.Background(), tools.ToolCall{
389389 Input: `{"path":"."}`,
390390 })
internal/llm/tools/bash.go+5−4
@@ -8,7 +8,6 @@ import (
88 "strings"
99 "time"
1010
11− "github.com/charmbracelet/crush/internal/config"
1211 "github.com/charmbracelet/crush/internal/permission"
1312 "github.com/charmbracelet/crush/internal/shell"
1413)
@@ -29,6 +28,7 @@ type BashResponseMetadata struct {
2928}
3029type bashTool struct {
3130 permissions permission.Service
31+ workingDir string
3232}
3333
3434const (
@@ -244,9 +244,10 @@ Important:
244244- Never update git config`, bannedCommandsStr, MaxOutputLength)
245245}
246246
247−func NewBashTool(permission permission.Service) BaseTool {
247+func NewBashTool(permission permission.Service, workingDir string) BaseTool {
248248 return &bashTool{
249249 permissions: permission,
250+ workingDir: workingDir,
250251 }
251252}
252253
@@ -317,7 +318,7 @@ func (b *bashTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
317318 p := b.permissions.Request(
318319 permission.CreatePermissionRequest{
319320 SessionID: sessionID,
320− Path: config.Get().WorkingDir(),
321+ Path: b.workingDir,
321322 ToolName: BashToolName,
322323 Action: "execute",
323324 Description: fmt.Sprintf("Execute command: %s", params.Command),
@@ -337,7 +338,7 @@ func (b *bashTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
337338 defer cancel()
338339 }
339340 stdout, stderr, err := shell.
340− GetPersistentShell(config.Get().WorkingDir()).
341+ GetPersistentShell(b.workingDir).
341342 Exec(ctx, params.Command)
342343 interrupted := shell.IsInterrupt(err)
343344 exitCode := shell.ExitCode(err)
internal/llm/tools/edit.go+10−10
@@ -10,7 +10,6 @@ import (
1010 "strings"
1111 "time"
1212
13− "github.com/charmbracelet/crush/internal/config"
1413 "github.com/charmbracelet/crush/internal/diff"
1514 "github.com/charmbracelet/crush/internal/history"
1615
@@ -41,6 +40,7 @@ type editTool struct {
4140 lspClients map[string]*lsp.Client
4241 permissions permission.Service
4342 files history.Service
43+ workingDir string
4444}
4545
4646const (
@@ -99,11 +99,12 @@ WINDOWS NOTES:
9999Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each.`
100100)
101101
102−func NewEditTool(lspClients map[string]*lsp.Client, permissions permission.Service, files history.Service) BaseTool {
102+func NewEditTool(lspClients map[string]*lsp.Client, permissions permission.Service, files history.Service, workingDir string) BaseTool {
103103 return &editTool{
104104 lspClients: lspClients,
105105 permissions: permissions,
106106 files: files,
107+ workingDir: workingDir,
107108 }
108109}
109110
@@ -144,8 +145,7 @@ func (e *editTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
144145 }
145146
146147 if !filepath.IsAbs(params.FilePath) {
147− wd := config.Get().WorkingDir()
148− params.FilePath = filepath.Join(wd, params.FilePath)
148+ params.FilePath = filepath.Join(e.workingDir, params.FilePath)
149149 }
150150
151151 var response ToolResponse
@@ -206,9 +206,9 @@ func (e *editTool) createNewFile(ctx context.Context, filePath, content string)
206206 _, additions, removals := diff.GenerateDiff(
207207 "",
208208 content,
209− strings.TrimPrefix(filePath, config.Get().WorkingDir()),
209+ strings.TrimPrefix(filePath, e.workingDir),
210210 )
211− rootDir := config.Get().WorkingDir()
211+ rootDir := e.workingDir
212212 permissionPath := filepath.Dir(filePath)
213213 if strings.HasPrefix(filePath, rootDir) {
214214 permissionPath = rootDir
@@ -318,10 +318,10 @@ func (e *editTool) deleteContent(ctx context.Context, filePath, oldString string
318318 _, additions, removals := diff.GenerateDiff(
319319 oldContent,
320320 newContent,
321− strings.TrimPrefix(filePath, config.Get().WorkingDir()),
321+ strings.TrimPrefix(filePath, e.workingDir),
322322 )
323323
324− rootDir := config.Get().WorkingDir()
324+ rootDir := e.workingDir
325325 permissionPath := filepath.Dir(filePath)
326326 if strings.HasPrefix(filePath, rootDir) {
327327 permissionPath = rootDir
@@ -441,9 +441,9 @@ func (e *editTool) replaceContent(ctx context.Context, filePath, oldString, newS
441441 _, additions, removals := diff.GenerateDiff(
442442 oldContent,
443443 newContent,
444− strings.TrimPrefix(filePath, config.Get().WorkingDir()),
444+ strings.TrimPrefix(filePath, e.workingDir),
445445 )
446− rootDir := config.Get().WorkingDir()
446+ rootDir := e.workingDir
447447 permissionPath := filepath.Dir(filePath)
448448 if strings.HasPrefix(filePath, rootDir) {
449449 permissionPath = rootDir
internal/llm/tools/fetch.go+4−3
@@ -11,7 +11,6 @@ import (
1111
1212 md "github.com/JohannesKaufmann/html-to-markdown"
1313 "github.com/PuerkitoBio/goquery"
14− "github.com/charmbracelet/crush/internal/config"
1514 "github.com/charmbracelet/crush/internal/permission"
1615)
1716
@@ -30,6 +29,7 @@ type FetchPermissionsParams struct {
3029type fetchTool struct {
3130 client *http.Client
3231 permissions permission.Service
32+ workingDir string
3333}
3434
3535const (
@@ -65,7 +65,7 @@ TIPS:
6565- Set appropriate timeouts for potentially slow websites`
6666)
6767
68−func NewFetchTool(permissions permission.Service) BaseTool {
68+func NewFetchTool(permissions permission.Service, workingDir string) BaseTool {
6969 return &fetchTool{
7070 client: &http.Client{
7171 Timeout: 30 * time.Second,
@@ -76,6 +76,7 @@ func NewFetchTool(permissions permission.Service) BaseTool {
7676 },
7777 },
7878 permissions: permissions,
79+ workingDir: workingDir,
7980 }
8081}
8182
@@ -133,7 +134,7 @@ func (t *fetchTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
133134 p := t.permissions.Request(
134135 permission.CreatePermissionRequest{
135136 SessionID: sessionID,
136− Path: config.Get().WorkingDir(),
137+ Path: t.workingDir,
137138 ToolName: FetchToolName,
138139 Action: "fetch",
139140 Description: fmt.Sprintf("Fetch content from URL: %s", params.URL),
internal/llm/tools/glob.go+8−5
@@ -11,7 +11,6 @@ import (
1111 "sort"
1212 "strings"
1313
14− "github.com/charmbracelet/crush/internal/config"
1514 "github.com/charmbracelet/crush/internal/fsext"
1615)
1716
@@ -68,10 +67,14 @@ type GlobResponseMetadata struct {
6867 Truncated bool `json:"truncated"`
6968}
7069
71−type globTool struct{}
70+type globTool struct {
71+ workingDir string
72+}
7273
73−func NewGlobTool() BaseTool {
74− return &globTool{}
74+func NewGlobTool(workingDir string) BaseTool {
75+ return &globTool{
76+ workingDir: workingDir,
77+ }
7578}
7679
7780func (g *globTool) Name() string {
@@ -108,7 +111,7 @@ func (g *globTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
108111
109112 searchPath := params.Path
110113 if searchPath == "" {
111− searchPath = config.Get().WorkingDir()
114+ searchPath = g.workingDir
112115 }
113116
114117 files, truncated, err := globFiles(params.Pattern, searchPath, 100)
internal/llm/tools/grep.go+8−5
@@ -16,7 +16,6 @@ import (
1616 "sync"
1717 "time"
1818
19− "github.com/charmbracelet/crush/internal/config"
2019 "github.com/charmbracelet/crush/internal/fsext"
2120)
2221
@@ -89,7 +88,9 @@ type GrepResponseMetadata struct {
8988 Truncated bool `json:"truncated"`
9089}
9190
92−type grepTool struct{}
91+type grepTool struct {
92+ workingDir string
93+}
9394
9495const (
9596 GrepToolName = "grep"
@@ -136,8 +137,10 @@ TIPS:
136137- Use literal_text=true when searching for exact text containing special characters like dots, parentheses, etc.`
137138)
138139
139−func NewGrepTool() BaseTool {
140− return &grepTool{}
140+func NewGrepTool(workingDir string) BaseTool {
141+ return &grepTool{
142+ workingDir: workingDir,
143+ }
141144}
142145
143146func (g *grepTool) Name() string {
@@ -200,7 +203,7 @@ func (g *grepTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
200203
201204 searchPath := params.Path
202205 if searchPath == "" {
203− searchPath = config.Get().WorkingDir()
206+ searchPath = g.workingDir
204207 }
205208
206209 matches, truncated, err := searchFiles(searchPattern, searchPath, params.Include, 100)
internal/llm/tools/ls.go+9−6
@@ -8,7 +8,6 @@ import (
88 "path/filepath"
99 "strings"
1010
11− "github.com/charmbracelet/crush/internal/config"
1211 "github.com/charmbracelet/crush/internal/fsext"
1312)
1413
@@ -29,7 +28,9 @@ type LSResponseMetadata struct {
2928 Truncated bool `json:"truncated"`
3029}
3130
32−type lsTool struct{}
31+type lsTool struct {
32+ workingDir string
33+}
3334
3435const (
3536 LSToolName = "ls"
@@ -70,8 +71,10 @@ TIPS:
7071- Combine with other tools for more effective exploration`
7172)
7273
73−func NewLsTool() BaseTool {
74− return &lsTool{}
74+func NewLsTool(workingDir string) BaseTool {
75+ return &lsTool{
76+ workingDir: workingDir,
77+ }
7578}
7679
7780func (l *lsTool) Name() string {
@@ -107,11 +110,11 @@ func (l *lsTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
107110
108111 searchPath := params.Path
109112 if searchPath == "" {
110− searchPath = config.Get().WorkingDir()
113+ searchPath = l.workingDir
111114 }
112115
113116 if !filepath.IsAbs(searchPath) {
114− searchPath = filepath.Join(config.Get().WorkingDir(), searchPath)
117+ searchPath = filepath.Join(l.workingDir, searchPath)
115118 }
116119
117120 if _, err := os.Stat(searchPath); os.IsNotExist(err) {
internal/llm/tools/view.go+5−4
@@ -10,7 +10,6 @@ import (
1010 "path/filepath"
1111 "strings"
1212
13− "github.com/charmbracelet/crush/internal/config"
1413 "github.com/charmbracelet/crush/internal/lsp"
1514)
1615
@@ -22,6 +21,7 @@ type ViewParams struct {
2221
2322type viewTool struct {
2423 lspClients map[string]*lsp.Client
24+ workingDir string
2525}
2626
2727type ViewResponseMetadata struct {
@@ -71,9 +71,10 @@ TIPS:
7171- When viewing large files, use the offset parameter to read specific sections`
7272)
7373
74−func NewViewTool(lspClients map[string]*lsp.Client) BaseTool {
74+func NewViewTool(lspClients map[string]*lsp.Client, workingDir string) BaseTool {
7575 return &viewTool{
76− lspClients,
76+ lspClients: lspClients,
77+ workingDir: workingDir,
7778 }
7879}
7980
@@ -117,7 +118,7 @@ func (v *viewTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
117118 // Handle relative paths
118119 filePath := params.FilePath
119120 if !filepath.IsAbs(filePath) {
120− filePath = filepath.Join(config.Get().WorkingDir(), filePath)
121+ filePath = filepath.Join(v.workingDir, filePath)
121122 }
122123
123124 // Check if file exists
internal/llm/tools/write.go+6−5
@@ -10,7 +10,6 @@ import (
1010 "strings"
1111 "time"
1212
13− "github.com/charmbracelet/crush/internal/config"
1413 "github.com/charmbracelet/crush/internal/diff"
1514 "github.com/charmbracelet/crush/internal/history"
1615
@@ -33,6 +32,7 @@ type writeTool struct {
3332 lspClients map[string]*lsp.Client
3433 permissions permission.Service
3534 files history.Service
35+ workingDir string
3636}
3737
3838type WriteResponseMetadata struct {
@@ -77,11 +77,12 @@ TIPS:
7777- Always include descriptive comments when making changes to existing code`
7878)
7979
80−func NewWriteTool(lspClients map[string]*lsp.Client, permissions permission.Service, files history.Service) BaseTool {
80+func NewWriteTool(lspClients map[string]*lsp.Client, permissions permission.Service, files history.Service, workingDir string) BaseTool {
8181 return &writeTool{
8282 lspClients: lspClients,
8383 permissions: permissions,
8484 files: files,
85+ workingDir: workingDir,
8586 }
8687}
8788
@@ -123,7 +124,7 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
123124
124125 filePath := params.FilePath
125126 if !filepath.IsAbs(filePath) {
126− filePath = filepath.Join(config.Get().WorkingDir(), filePath)
127+ filePath = filepath.Join(w.workingDir, filePath)
127128 }
128129
129130 fileInfo, err := os.Stat(filePath)
@@ -168,10 +169,10 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
168169 diff, additions, removals := diff.GenerateDiff(
169170 oldContent,
170171 params.Content,
171− strings.TrimPrefix(filePath, config.Get().WorkingDir()),
172+ strings.TrimPrefix(filePath, w.workingDir),
172173 )
173174
174− rootDir := config.Get().WorkingDir()
175+ rootDir := w.workingDir
175176 permissionPath := filepath.Dir(filePath)
176177 if strings.HasPrefix(filePath, rootDir) {
177178 permissionPath = rootDir