on main help sign in
feat(tools): crush_info tool for readling live config
2772734Christian Rocha
internal/agent/coordinator.go+1
@@ -447,6 +447,7 @@ func (c *coordinator) buildTools(ctx context.Context, agent config.Agent) ([]fan
447
447
448 allTools = append(allTools,
448 allTools = append(allTools,
449 tools.NewBashTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Config().Options.Attribution, modelName),
449 tools.NewBashTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Config().Options.Attribution, modelName),
450+ tools.NewCrushInfoTool(c.cfg, c.lspManager),
450 tools.NewJobOutputTool(),
451 tools.NewJobOutputTool(),
451 tools.NewJobKillTool(),
452 tools.NewJobKillTool(),
452 tools.NewDownloadTool(c.permissions, c.cfg.WorkingDir(), nil),
453 tools.NewDownloadTool(c.permissions, c.cfg.WorkingDir(), nil),
internal/config/config.go+1
@@ -462,6 +462,7 @@ func allToolNames() []string {
462 return []string{
462 return []string{
463 "agent",
463 "agent",
464 "bash",
464 "bash",
465+ "crush_info",
465 "job_output",
466 "job_output",
466 "job_kill",
467 "job_kill",
467 "download",
468 "download",
internal/config/load.go+12−4
@@ -33,7 +33,7 @@ const defaultCatwalkURL = "https://catwalk.charm.sh"
33func Load(workingDir, dataDir string, debug bool) (*ConfigStore, error) {
33func Load(workingDir, dataDir string, debug bool) (*ConfigStore, error) {
34 configPaths := lookupConfigs(workingDir)
34 configPaths := lookupConfigs(workingDir)
35
35
36− cfg, err := loadFromConfigPaths(configPaths)
36+ cfg, loadedPaths, err := loadFromConfigPaths(configPaths)
37 if err != nil {
37 if err != nil {
38 return nil, fmt.Errorf("failed to load config from paths %v: %w", configPaths, err)
38 return nil, fmt.Errorf("failed to load config from paths %v: %w", configPaths, err)
39 }
39 }
@@ -45,6 +45,7 @@ func Load(workingDir, dataDir string, debug bool) (*ConfigStore, error) {
45 workingDir: workingDir,
45 workingDir: workingDir,
46 globalDataPath: GlobalConfigData(),
46 globalDataPath: GlobalConfigData(),
47 workspacePath: filepath.Join(cfg.Options.DataDirectory, fmt.Sprintf("%s.json", appName)),
47 workspacePath: filepath.Join(cfg.Options.DataDirectory, fmt.Sprintf("%s.json", appName)),
48+ loadedPaths: loadedPaths,
48 }
49 }
49
50
50 if debug {
51 if debug {
@@ -60,6 +61,7 @@ func Load(workingDir, dataDir string, debug bool) (*ConfigStore, error) {
60 *cfg = *merged
61 *cfg = *merged
61 cfg.setDefaults(workingDir, dataDir)
62 cfg.setDefaults(workingDir, dataDir)
62 store.config = cfg
63 store.config = cfg
64+ store.loadedPaths = append(store.loadedPaths, store.workspacePath)
63 }
65 }
64 }
66 }
65
67
@@ -669,8 +671,9 @@ func lookupConfigs(cwd string) []string {
669 return append(configPaths, foundConfigs...)
671 return append(configPaths, foundConfigs...)
670}
672}
671
673
672−func loadFromConfigPaths(configPaths []string) (*Config, error) {
674+func loadFromConfigPaths(configPaths []string) (*Config, []string, error) {
673 var configs [][]byte
675 var configs [][]byte
676+ var loaded []string
674
677
675 for _, path := range configPaths {
678 for _, path := range configPaths {
676 data, err := os.ReadFile(path)
679 data, err := os.ReadFile(path)
@@ -678,15 +681,20 @@ func loadFromConfigPaths(configPaths []string) (*Config, error) {
678 if os.IsNotExist(err) {
681 if os.IsNotExist(err) {
679 continue
682 continue
680 }
683 }
681− return nil, fmt.Errorf("failed to open config file %s: %w", path, err)
684+ return nil, nil, fmt.Errorf("failed to open config file %s: %w", path, err)
682 }
685 }
683 if len(data) == 0 {
686 if len(data) == 0 {
684 continue
687 continue
685 }
688 }
686 configs = append(configs, data)
689 configs = append(configs, data)
690+ loaded = append(loaded, path)
687 }
691 }
688
692
689− return loadFromBytes(configs)
693+ cfg, err := loadFromBytes(configs)
694+ if err != nil {
695+ return nil, nil, err
696+ }
697+ return cfg, loaded, nil
690}
698}
691
699
692func loadFromBytes(configs [][]byte) (*Config, error) {
700func loadFromBytes(configs [][]byte) (*Config, error) {
internal/config/load_bench_test.go+3−3
@@ -53,7 +53,7 @@ func BenchmarkLoadFromConfigPaths(b *testing.B) {
53
53
54 b.ReportAllocs()
54 b.ReportAllocs()
55 for b.Loop() {
55 for b.Loop() {
56− _, err := loadFromConfigPaths(configPaths)
56+ _, _, err := loadFromConfigPaths(configPaths)
57 if err != nil {
57 if err != nil {
58 b.Fatal(err)
58 b.Fatal(err)
59 }
59 }
@@ -78,7 +78,7 @@ func BenchmarkLoadFromConfigPaths_MissingFiles(b *testing.B) {
78
78
79 b.ReportAllocs()
79 b.ReportAllocs()
80 for b.Loop() {
80 for b.Loop() {
81− _, err := loadFromConfigPaths(configPaths)
81+ _, _, err := loadFromConfigPaths(configPaths)
82 if err != nil {
82 if err != nil {
83 b.Fatal(err)
83 b.Fatal(err)
84 }
84 }
@@ -95,7 +95,7 @@ func BenchmarkLoadFromConfigPaths_Empty(b *testing.B) {
95
95
96 b.ReportAllocs()
96 b.ReportAllocs()
97 for b.Loop() {
97 for b.Loop() {
98− _, err := loadFromConfigPaths(configPaths)
98+ _, _, err := loadFromConfigPaths(configPaths)
99 if err != nil {
99 if err != nil {
100 b.Fatal(err)
100 b.Fatal(err)
101 }
101 }
internal/config/load_test.go+2−2
@@ -490,7 +490,7 @@ func TestConfig_setupAgentsWithDisabledTools(t *testing.T) {
490 coderAgent, ok := cfg.Agents[AgentCoder]
490 coderAgent, ok := cfg.Agents[AgentCoder]
491 require.True(t, ok)
491 require.True(t, ok)
492
492
493− assert.Equal(t, []string{"agent", "bash", "job_output", "job_kill", "multiedit", "lsp_diagnostics", "lsp_references", "lsp_restart", "fetch", "agentic_fetch", "glob", "ls", "sourcegraph", "todos", "view", "write", "list_mcp_resources", "read_mcp_resource"}, coderAgent.AllowedTools)
493+ assert.Equal(t, []string{"agent", "bash", "crush_info", "job_output", "job_kill", "multiedit", "lsp_diagnostics", "lsp_references", "lsp_restart", "fetch", "agentic_fetch", "glob", "ls", "sourcegraph", "todos", "view", "write", "list_mcp_resources", "read_mcp_resource"}, coderAgent.AllowedTools)
494
494
495 taskAgent, ok := cfg.Agents[AgentTask]
495 taskAgent, ok := cfg.Agents[AgentTask]
496 require.True(t, ok)
496 require.True(t, ok)
@@ -513,7 +513,7 @@ func TestConfig_setupAgentsWithEveryReadOnlyToolDisabled(t *testing.T) {
513 cfg.SetupAgents()
513 cfg.SetupAgents()
514 coderAgent, ok := cfg.Agents[AgentCoder]
514 coderAgent, ok := cfg.Agents[AgentCoder]
515 require.True(t, ok)
515 require.True(t, ok)
516− assert.Equal(t, []string{"agent", "bash", "job_output", "job_kill", "download", "edit", "multiedit", "lsp_diagnostics", "lsp_references", "lsp_restart", "fetch", "agentic_fetch", "todos", "write", "list_mcp_resources", "read_mcp_resource"}, coderAgent.AllowedTools)
516+ assert.Equal(t, []string{"agent", "bash", "crush_info", "job_output", "job_kill", "download", "edit", "multiedit", "lsp_diagnostics", "lsp_references", "lsp_restart", "fetch", "agentic_fetch", "todos", "write", "list_mcp_resources", "read_mcp_resource"}, coderAgent.AllowedTools)
517
517
518 taskAgent, ok := cfg.Agents[AgentTask]
518 taskAgent, ok := cfg.Agents[AgentTask]
519 require.True(t, ok)
519 require.True(t, ok)
internal/config/store.go+16−2
@@ -32,8 +32,9 @@ type ConfigStore struct {
32 config *Config
32 config *Config
33 workingDir string
33 workingDir string
34 resolver VariableResolver
34 resolver VariableResolver
35− globalDataPath string // ~/.local/share/crush/crush.json
35+ globalDataPath string // ~/.local/share/crush/crush.json
36− workspacePath string // .crush/crush.json
36+ workspacePath string // .crush/crush.json
37+ loadedPaths []string // config files that were successfully loaded
37 knownProviders []catwalk.Provider
38 knownProviders []catwalk.Provider
38 overrides RuntimeOverrides
39 overrides RuntimeOverrides
39}
40}
@@ -76,6 +77,11 @@ func (s *ConfigStore) Overrides() *RuntimeOverrides {
76 return &s.overrides
77 return &s.overrides
77}
78}
78
79
80+// LoadedPaths returns the config file paths that were successfully loaded.
81+func (s *ConfigStore) LoadedPaths() []string {
82+ return slices.Clone(s.loadedPaths)
83+}
84+
79// configPath returns the file path for the given scope.
85// configPath returns the file path for the given scope.
80func (s *ConfigStore) configPath(scope Scope) (string, error) {
86func (s *ConfigStore) configPath(scope Scope) (string, error) {
81 switch scope {
87 switch scope {
@@ -337,6 +343,14 @@ func (s *ConfigStore) recordRecentModel(scope Scope, modelType SelectedModelType
337 return nil
343 return nil
338}
344}
339
345
346+// NewTestStore creates a ConfigStore for testing purposes.
347+func NewTestStore(cfg *Config, loadedPaths ...string) *ConfigStore {
348+ return &ConfigStore{
349+ config: cfg,
350+ loadedPaths: loadedPaths,
351+ }
352+}
353+
340// ImportCopilot attempts to import a GitHub Copilot token from disk.
354// ImportCopilot attempts to import a GitHub Copilot token from disk.
341func (s *ConfigStore) ImportCopilot() (*oauth.Token, bool) {
355func (s *ConfigStore) ImportCopilot() (*oauth.Token, bool) {
342 if s.HasConfigField(ScopeGlobal, "providers.copilot.api_key") || s.HasConfigField(ScopeGlobal, "providers.copilot.oauth") {
356 if s.HasConfigField(ScopeGlobal, "providers.copilot.api_key") || s.HasConfigField(ScopeGlobal, "providers.copilot.oauth") {
internal/lsp/client.go+5
@@ -286,6 +286,11 @@ func (c *Client) GetName() string {
286 return c.name
286 return c.name
287}
287}
288
288
289+// FileTypes returns the file types this LSP client handles
290+func (c *Client) FileTypes() []string {
291+ return c.fileTypes
292+}
293+
289// SetDiagnosticsCallback sets the callback function for diagnostic changes
294// SetDiagnosticsCallback sets the callback function for diagnostic changes
290func (c *Client) SetDiagnosticsCallback(callback func(name string, count int)) {
295func (c *Client) SetDiagnosticsCallback(callback func(name string, count int)) {
291 c.onDiagnosticsChanged = callback
296 c.onDiagnosticsChanged = callback