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