crush_info.go

  1package tools
  2
  3import (
  4	"context"
  5	_ "embed"
  6	"fmt"
  7	"slices"
  8	"strings"
  9
 10	"charm.land/fantasy"
 11	"github.com/charmbracelet/crush/internal/agent/tools/mcp"
 12	"github.com/charmbracelet/crush/internal/config"
 13	"github.com/charmbracelet/crush/internal/lsp"
 14)
 15
 16const CrushInfoToolName = "crush_info"
 17
 18//go:embed crush_info.md
 19var crushInfoDescription []byte
 20
 21type CrushInfoParams struct{}
 22
 23func NewCrushInfoTool(
 24	cfg *config.ConfigStore,
 25	lspManager *lsp.Manager,
 26) fantasy.AgentTool {
 27	return fantasy.NewAgentTool(
 28		CrushInfoToolName,
 29		string(crushInfoDescription),
 30		func(ctx context.Context, _ CrushInfoParams, _ fantasy.ToolCall) (fantasy.ToolResponse, error) {
 31			return fantasy.NewTextResponse(buildCrushInfo(cfg, lspManager)), nil
 32		})
 33}
 34
 35func buildCrushInfo(cfg *config.ConfigStore, lspManager *lsp.Manager) string {
 36	var b strings.Builder
 37
 38	writeConfigFiles(&b, cfg)
 39	writeModels(&b, cfg)
 40	writeProviders(&b, cfg)
 41	writeLSP(&b, lspManager, cfg)
 42	writeMCP(&b, mcp.GetStates(), cfg)
 43	writePermissions(&b, cfg)
 44	writeDisabledTools(&b, cfg)
 45	writeOptions(&b, cfg)
 46
 47	return b.String()
 48}
 49
 50func writeConfigFiles(b *strings.Builder, cfg *config.ConfigStore) {
 51	b.WriteString("[config_files]\n")
 52	paths := cfg.LoadedPaths()
 53	for _, p := range paths {
 54		b.WriteString(p + "\n")
 55	}
 56	b.WriteString("\n")
 57}
 58
 59func writeModels(b *strings.Builder, cfg *config.ConfigStore) {
 60	c := cfg.Config()
 61	if len(c.Models) == 0 {
 62		return
 63	}
 64	b.WriteString("[model]\n")
 65	for _, typ := range []config.SelectedModelType{config.SelectedModelTypeLarge, config.SelectedModelTypeSmall} {
 66		m, ok := c.Models[typ]
 67		if !ok {
 68			continue
 69		}
 70		fmt.Fprintf(b, "%s = %s (%s)\n", typ, m.Model, m.Provider)
 71	}
 72	b.WriteString("\n")
 73}
 74
 75func writeProviders(b *strings.Builder, cfg *config.ConfigStore) {
 76	c := cfg.Config()
 77	type pv struct {
 78		name  string
 79		count int
 80	}
 81	var providers []pv
 82	for name, pc := range c.Providers.Seq2() {
 83		if pc.Disable {
 84			continue
 85		}
 86		providers = append(providers, pv{name: name, count: len(pc.Models)})
 87	}
 88	if len(providers) == 0 {
 89		return
 90	}
 91	slices.SortFunc(providers, func(a, b pv) int { return strings.Compare(a.name, b.name) })
 92	b.WriteString("[providers]\n")
 93	for _, p := range providers {
 94		fmt.Fprintf(b, "%s = enabled (%d models)\n", p.name, p.count)
 95	}
 96	b.WriteString("\n")
 97}
 98
 99func writeLSP(b *strings.Builder, lspManager *lsp.Manager, cfg *config.ConfigStore) {
100	// Write runtime LSP clients
101	if lspManager != nil && lspManager.Clients().Len() > 0 {
102		type entry struct {
103			name      string
104			state     lsp.ServerState
105			fileTypes []string
106		}
107		var entries []entry
108		for name, client := range lspManager.Clients().Seq2() {
109			entries = append(entries, entry{
110				name:      name,
111				state:     client.GetServerState(),
112				fileTypes: client.FileTypes(),
113			})
114		}
115		if len(entries) > 0 {
116			slices.SortFunc(entries, func(a, b entry) int { return strings.Compare(a.name, b.name) })
117			b.WriteString("[lsp]\n")
118			for _, e := range entries {
119				stateStr := lspStateString(e.state)
120				if len(e.fileTypes) > 0 {
121					sorted := slices.Clone(e.fileTypes)
122					slices.Sort(sorted)
123					fmt.Fprintf(b, "%s = %s (%s)\n", e.name, stateStr, strings.Join(sorted, ", "))
124				} else {
125					fmt.Fprintf(b, "%s = %s\n", e.name, stateStr)
126				}
127			}
128			b.WriteString("\n")
129		}
130	}
131
132	// Write configured but not running LSP servers
133	c := cfg.Config()
134	if len(c.LSP) > 0 {
135		runtimeNames := make(map[string]bool)
136		if lspManager != nil {
137			for name := range lspManager.Clients().Seq2() {
138				runtimeNames[name] = true
139			}
140		}
141
142		type configuredEntry struct {
143			name   string
144			status string
145		}
146		var entries []configuredEntry
147		for name, lspCfg := range c.LSP {
148			// Skip if already in runtime
149			if runtimeNames[name] {
150				continue
151			}
152			status := "not_started"
153			if lspCfg.Disabled {
154				status = "disabled"
155			}
156			entries = append(entries, configuredEntry{name: name, status: status})
157		}
158
159		if len(entries) > 0 {
160			slices.SortFunc(entries, func(a, b configuredEntry) int { return strings.Compare(a.name, b.name) })
161			b.WriteString("[lsp_configured]\n")
162			for _, e := range entries {
163				fmt.Fprintf(b, "%s = %s\n", e.name, e.status)
164			}
165			b.WriteString("\n")
166		}
167	}
168}
169
170func writeMCP(b *strings.Builder, states map[string]mcp.ClientInfo, cfg *config.ConfigStore) {
171	// Write runtime MCP states
172	if len(states) > 0 {
173		type entry struct {
174			name        string
175			state       mcp.State
176			err         error
177			tools       int
178			resources   int
179			connectedAt string
180		}
181		var entries []entry
182		for name, info := range states {
183			e := entry{
184				name:  name,
185				state: info.State,
186				err:   info.Error,
187			}
188			if info.State == mcp.StateConnected {
189				e.tools = info.Counts.Tools
190				e.resources = info.Counts.Resources
191				if !info.ConnectedAt.IsZero() {
192					e.connectedAt = info.ConnectedAt.Format("15:04:05")
193				}
194			}
195			entries = append(entries, e)
196		}
197		slices.SortFunc(entries, func(a, b entry) int { return strings.Compare(a.name, b.name) })
198		b.WriteString("[mcp]\n")
199		for _, e := range entries {
200			switch e.state {
201			case mcp.StateConnected:
202				if e.connectedAt != "" {
203					fmt.Fprintf(b, "%s = connected (%d tools, %d resources) since %s\n", e.name, e.tools, e.resources, e.connectedAt)
204				} else {
205					fmt.Fprintf(b, "%s = connected (%d tools, %d resources)\n", e.name, e.tools, e.resources)
206				}
207			case mcp.StateError:
208				if e.err != nil {
209					fmt.Fprintf(b, "%s = error: %s\n", e.name, e.err.Error())
210				} else {
211					fmt.Fprintf(b, "%s = error\n", e.name)
212				}
213			default:
214				fmt.Fprintf(b, "%s = %s\n", e.name, e.state)
215			}
216		}
217		b.WriteString("\n")
218	}
219
220	// Write configured but not running MCP servers
221	c := cfg.Config()
222	if len(c.MCP) > 0 {
223		runtimeNames := make(map[string]bool)
224		for name := range states {
225			runtimeNames[name] = true
226		}
227
228		type configuredEntry struct {
229			name   string
230			status string
231		}
232		var entries []configuredEntry
233		for name, mcpCfg := range c.MCP {
234			// Skip if already in runtime
235			if runtimeNames[name] {
236				continue
237			}
238			status := "not_started"
239			if mcpCfg.Disabled {
240				status = "disabled"
241			}
242			entries = append(entries, configuredEntry{name: name, status: status})
243		}
244
245		if len(entries) > 0 {
246			slices.SortFunc(entries, func(a, b configuredEntry) int { return strings.Compare(a.name, b.name) })
247			b.WriteString("[mcp_configured]\n")
248			for _, e := range entries {
249				fmt.Fprintf(b, "%s = %s\n", e.name, e.status)
250			}
251			b.WriteString("\n")
252		}
253	}
254}
255
256func writePermissions(b *strings.Builder, cfg *config.ConfigStore) {
257	c := cfg.Config()
258	overrides := cfg.Overrides()
259
260	if c.Permissions == nil {
261		if !overrides.SkipPermissionRequests {
262			return
263		}
264	} else if !overrides.SkipPermissionRequests && len(c.Permissions.AllowedTools) == 0 {
265		return
266	}
267	b.WriteString("[permissions]\n")
268	if overrides.SkipPermissionRequests {
269		b.WriteString("mode = yolo\n")
270	}
271	if c.Permissions != nil && len(c.Permissions.AllowedTools) > 0 {
272		sorted := slices.Clone(c.Permissions.AllowedTools)
273		slices.Sort(sorted)
274		fmt.Fprintf(b, "allowed_tools = %s\n", strings.Join(sorted, ", "))
275	}
276	b.WriteString("\n")
277}
278
279func writeDisabledTools(b *strings.Builder, cfg *config.ConfigStore) {
280	c := cfg.Config()
281	if c.Options == nil || len(c.Options.DisabledTools) == 0 {
282		return
283	}
284	sorted := slices.Clone(c.Options.DisabledTools)
285	slices.Sort(sorted)
286	b.WriteString("[tools]\n")
287	fmt.Fprintf(b, "disabled = %s\n", strings.Join(sorted, ", "))
288	b.WriteString("\n")
289}
290
291func writeOptions(b *strings.Builder, cfg *config.ConfigStore) {
292	c := cfg.Config()
293	if c.Options == nil {
294		return
295	}
296	type kv struct {
297		key   string
298		value string
299	}
300	var opts []kv
301
302	opts = append(opts, kv{"data_directory", c.Options.DataDirectory})
303	opts = append(opts, kv{"debug", fmt.Sprintf("%v", c.Options.Debug)})
304	autoLSP := c.Options.AutoLSP == nil || *c.Options.AutoLSP
305	opts = append(opts, kv{"auto_lsp", fmt.Sprintf("%v", autoLSP)})
306	autoSummarize := !c.Options.DisableAutoSummarize
307	opts = append(opts, kv{"auto_summarize", fmt.Sprintf("%v", autoSummarize)})
308
309	slices.SortFunc(opts, func(a, b kv) int { return strings.Compare(a.key, b.key) })
310	b.WriteString("[options]\n")
311	for _, o := range opts {
312		fmt.Fprintf(b, "%s = %s\n", o.key, o.value)
313	}
314	b.WriteString("\n")
315}
316
317func lspStateString(state lsp.ServerState) string {
318	switch state {
319	case lsp.StateUnstarted:
320		return "unstarted"
321	case lsp.StateStarting:
322		return "starting"
323	case lsp.StateReady:
324		return "ready"
325	case lsp.StateError:
326		return "error"
327	case lsp.StateStopped:
328		return "stopped"
329	case lsp.StateDisabled:
330		return "disabled"
331	default:
332		return "unknown"
333	}
334}