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}