on main help sign in
fix: improve logs, standardize capitalized (#2047)
3a2a045Carlos Alexandro Becker
AGENTS.md+2
@@ -26,6 +26,8 @@
26 need of a temporary directory. This directory does not need to be removed.
26 need of a temporary directory. This directory does not need to be removed.
27- **JSON tags**: Use snake_case for JSON field names
27- **JSON tags**: Use snake_case for JSON field names
28- **File permissions**: Use octal notation (0o755, 0o644) for file permissions
28- **File permissions**: Use octal notation (0o755, 0o644) for file permissions
29+- **Log messages**: Log messages must start with a capital letter (e.g., "Failed to save session" not "failed to save session")
30+ - This is enforced by `task lint:log` which runs as part of `task lint`
29- **Comments**: End comments in periods unless comments are at the end of the line.
31- **Comments**: End comments in periods unless comments are at the end of the line.
30
32
31## Testing with Mock Providers
33## Testing with Mock Providers
Taskfile.yaml+6
@@ -23,10 +23,16 @@ tasks:
23 lint:
23 lint:
24 desc: Run base linters
24 desc: Run base linters
25 cmds:
25 cmds:
26+ - task: lint:log
26 - golangci-lint run --path-mode=abs --config=".golangci.yml" --timeout=5m
27 - golangci-lint run --path-mode=abs --config=".golangci.yml" --timeout=5m
27 env:
28 env:
28 GOEXPERIMENT: null
29 GOEXPERIMENT: null
29
30
31+ lint:log:
32+ desc: Check that log messages start with capital letters
33+ cmds:
34+ - ./scripts/check_log_capitalization.sh
35+
30 lint:fix:
36 lint:fix:
31 desc: Run base linters and fix issues
37 desc: Run base linters and fix issues
32 cmds:
38 cmds:
internal/agent/agent.go+14−14
@@ -802,22 +802,22 @@ func (a *sessionAgent) generateTitle(ctx context.Context, sessionID string, user
802 resp, err := agent.Stream(ctx, streamCall)
802 resp, err := agent.Stream(ctx, streamCall)
803 if err == nil {
803 if err == nil {
804 // We successfully generated a title with the small model.
804 // We successfully generated a title with the small model.
805− slog.Info("generated title with small model")
805+ slog.Debug("Generated title with small model")
806 } else {
806 } else {
807 // It didn't work. Let's try with the big model.
807 // It didn't work. Let's try with the big model.
808− slog.Error("error generating title with small model; trying big model", "err", err)
808+ slog.Error("Error generating title with small model; trying big model", "err", err)
809 model = largeModel
809 model = largeModel
810 agent = newAgent(model.Model, titlePrompt, maxOutputTokens)
810 agent = newAgent(model.Model, titlePrompt, maxOutputTokens)
811 resp, err = agent.Stream(ctx, streamCall)
811 resp, err = agent.Stream(ctx, streamCall)
812 if err == nil {
812 if err == nil {
813− slog.Info("generated title with large model")
813+ slog.Debug("Generated title with large model")
814 } else {
814 } else {
815 // Welp, the large model didn't work either. Use the default
815 // Welp, the large model didn't work either. Use the default
816 // session name and return.
816 // session name and return.
817− slog.Error("error generating title with large model", "err", err)
817+ slog.Error("Error generating title with large model", "err", err)
818 saveErr := a.sessions.UpdateTitleAndUsage(ctx, sessionID, defaultSessionName, 0, 0, 0)
818 saveErr := a.sessions.UpdateTitleAndUsage(ctx, sessionID, defaultSessionName, 0, 0, 0)
819 if saveErr != nil {
819 if saveErr != nil {
820− slog.Error("failed to save session title and usage", "error", saveErr)
820+ slog.Error("Failed to save session title and usage", "error", saveErr)
821 }
821 }
822 return
822 return
823 }
823 }
@@ -826,10 +826,10 @@ func (a *sessionAgent) generateTitle(ctx context.Context, sessionID string, user
826 if resp == nil {
826 if resp == nil {
827 // Actually, we didn't get a response so we can't. Use the default
827 // Actually, we didn't get a response so we can't. Use the default
828 // session name and return.
828 // session name and return.
829− slog.Error("response is nil; can't generate title")
829+ slog.Error("Response is nil; can't generate title")
830 saveErr := a.sessions.UpdateTitleAndUsage(ctx, sessionID, defaultSessionName, 0, 0, 0)
830 saveErr := a.sessions.UpdateTitleAndUsage(ctx, sessionID, defaultSessionName, 0, 0, 0)
831 if saveErr != nil {
831 if saveErr != nil {
832− slog.Error("failed to save session title and usage", "error", saveErr)
832+ slog.Error("Failed to save session title and usage", "error", saveErr)
833 }
833 }
834 return
834 return
835 }
835 }
@@ -843,7 +843,7 @@ func (a *sessionAgent) generateTitle(ctx context.Context, sessionID string, user
843
843
844 title = strings.TrimSpace(title)
844 title = strings.TrimSpace(title)
845 if title == "" {
845 if title == "" {
846− slog.Warn("empty title; using fallback")
846+ slog.Debug("Empty title; using fallback")
847 title = defaultSessionName
847 title = defaultSessionName
848 }
848 }
849
849
@@ -878,7 +878,7 @@ func (a *sessionAgent) generateTitle(ctx context.Context, sessionID string, user
878 // concurrent session updates.
878 // concurrent session updates.
879 saveErr := a.sessions.UpdateTitleAndUsage(ctx, sessionID, title, promptTokens, completionTokens, cost)
879 saveErr := a.sessions.UpdateTitleAndUsage(ctx, sessionID, title, promptTokens, completionTokens, cost)
880 if saveErr != nil {
880 if saveErr != nil {
881− slog.Error("failed to save session title and usage", "error", saveErr)
881+ slog.Error("Failed to save session title and usage", "error", saveErr)
882 return
882 return
883 }
883 }
884}
884}
@@ -921,25 +921,25 @@ func (a *sessionAgent) Cancel(sessionID string) {
921 // fully completes (including error handling that may access the DB).
921 // fully completes (including error handling that may access the DB).
922 // The defer in processRequest will clean up the entry.
922 // The defer in processRequest will clean up the entry.
923 if cancel, ok := a.activeRequests.Get(sessionID); ok && cancel != nil {
923 if cancel, ok := a.activeRequests.Get(sessionID); ok && cancel != nil {
924− slog.Info("Request cancellation initiated", "session_id", sessionID)
924+ slog.Debug("Request cancellation initiated", "session_id", sessionID)
925 cancel()
925 cancel()
926 }
926 }
927
927
928 // Also check for summarize requests.
928 // Also check for summarize requests.
929 if cancel, ok := a.activeRequests.Get(sessionID + "-summarize"); ok && cancel != nil {
929 if cancel, ok := a.activeRequests.Get(sessionID + "-summarize"); ok && cancel != nil {
930− slog.Info("Summarize cancellation initiated", "session_id", sessionID)
930+ slog.Debug("Summarize cancellation initiated", "session_id", sessionID)
931 cancel()
931 cancel()
932 }
932 }
933
933
934 if a.QueuedPrompts(sessionID) > 0 {
934 if a.QueuedPrompts(sessionID) > 0 {
935− slog.Info("Clearing queued prompts", "session_id", sessionID)
935+ slog.Debug("Clearing queued prompts", "session_id", sessionID)
936 a.messageQueue.Del(sessionID)
936 a.messageQueue.Del(sessionID)
937 }
937 }
938}
938}
939
939
940func (a *sessionAgent) ClearQueue(sessionID string) {
940func (a *sessionAgent) ClearQueue(sessionID string) {
941 if a.QueuedPrompts(sessionID) > 0 {
941 if a.QueuedPrompts(sessionID) > 0 {
942− slog.Info("Clearing queued prompts", "session_id", sessionID)
942+ slog.Debug("Clearing queued prompts", "session_id", sessionID)
943 a.messageQueue.Del(sessionID)
943 a.messageQueue.Del(sessionID)
944 }
944 }
945}
945}
@@ -1099,7 +1099,7 @@ func (a *sessionAgent) workaroundProviderMediaLimitations(messages []fantasy.Mes
1099 if media, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentMedia](toolResult.Output); ok {
1099 if media, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentMedia](toolResult.Output); ok {
1100 decoded, err := base64.StdEncoding.DecodeString(media.Data)
1100 decoded, err := base64.StdEncoding.DecodeString(media.Data)
1101 if err != nil {
1101 if err != nil {
1102− slog.Warn("failed to decode media data", "error", err)
1102+ slog.Warn("Failed to decode media data", "error", err)
1103 textParts = append(textParts, part)
1103 textParts = append(textParts, part)
1104 continue
1104 continue
1105 }
1105 }
internal/agent/coordinator.go+6−6
@@ -151,7 +151,7 @@ func (c *coordinator) Run(ctx context.Context, sessionID string, prompt string,
151 mergedOptions, temp, topP, topK, freqPenalty, presPenalty := mergeCallOptions(model, providerCfg)
151 mergedOptions, temp, topP, topK, freqPenalty, presPenalty := mergeCallOptions(model, providerCfg)
152
152
153 if providerCfg.OAuthToken != nil && providerCfg.OAuthToken.IsExpired() {
153 if providerCfg.OAuthToken != nil && providerCfg.OAuthToken.IsExpired() {
154− slog.Info("Token needs to be refreshed", "provider", providerCfg.ID)
154+ slog.Debug("Token needs to be refreshed", "provider", providerCfg.ID)
155 if err := c.refreshOAuth2Token(ctx, providerCfg); err != nil {
155 if err := c.refreshOAuth2Token(ctx, providerCfg); err != nil {
156 return nil, err
156 return nil, err
157 }
157 }
@@ -176,18 +176,18 @@ func (c *coordinator) Run(ctx context.Context, sessionID string, prompt string,
176 if c.isUnauthorized(originalErr) {
176 if c.isUnauthorized(originalErr) {
177 switch {
177 switch {
178 case providerCfg.OAuthToken != nil:
178 case providerCfg.OAuthToken != nil:
179− slog.Info("Received 401. Refreshing token and retrying", "provider", providerCfg.ID)
179+ slog.Debug("Received 401. Refreshing token and retrying", "provider", providerCfg.ID)
180 if err := c.refreshOAuth2Token(ctx, providerCfg); err != nil {
180 if err := c.refreshOAuth2Token(ctx, providerCfg); err != nil {
181 return nil, originalErr
181 return nil, originalErr
182 }
182 }
183− slog.Info("Retrying request with refreshed OAuth token", "provider", providerCfg.ID)
183+ slog.Debug("Retrying request with refreshed OAuth token", "provider", providerCfg.ID)
184 return run()
184 return run()
185 case strings.Contains(providerCfg.APIKeyTemplate, "$"):
185 case strings.Contains(providerCfg.APIKeyTemplate, "$"):
186− slog.Info("Received 401. Refreshing API Key template and retrying", "provider", providerCfg.ID)
186+ slog.Debug("Received 401. Refreshing API Key template and retrying", "provider", providerCfg.ID)
187 if err := c.refreshApiKeyTemplate(ctx, providerCfg); err != nil {
187 if err := c.refreshApiKeyTemplate(ctx, providerCfg); err != nil {
188 return nil, originalErr
188 return nil, originalErr
189 }
189 }
190− slog.Info("Retrying request with refreshed API key", "provider", providerCfg.ID)
190+ slog.Debug("Retrying request with refreshed API key", "provider", providerCfg.ID)
191 return run()
191 return run()
192 }
192 }
193 }
193 }
@@ -428,7 +428,7 @@ func (c *coordinator) buildTools(ctx context.Context, agent config.Agent) ([]fan
428 }
428 }
429 if len(agent.AllowedMCP) == 0 {
429 if len(agent.AllowedMCP) == 0 {
430 // No MCPs allowed
430 // No MCPs allowed
431− slog.Debug("no MCPs allowed", "tool", tool.Name(), "agent", agent.Name)
431+ slog.Debug("No MCPs allowed", "tool", tool.Name(), "agent", agent.Name)
432 break
432 break
433 }
433 }
434
434
internal/agent/hyper/provider.go+1−1
@@ -49,7 +49,7 @@ var Enabled = sync.OnceValue(func() bool {
49var Embedded = sync.OnceValue(func() catwalk.Provider {
49var Embedded = sync.OnceValue(func() catwalk.Provider {
50 var provider catwalk.Provider
50 var provider catwalk.Provider
51 if err := json.Unmarshal(embedded, &provider); err != nil {
51 if err := json.Unmarshal(embedded, &provider); err != nil {
52− slog.Error("could not use embedded provider data", "err", err)
52+ slog.Error("Could not use embedded provider data", "err", err)
53 }
53 }
54 return provider
54 return provider
55})
55})
internal/agent/tools/mcp/init.go+6−6
@@ -140,7 +140,7 @@ func Initialize(ctx context.Context, permissions permission.Service, cfg *config
140 for name, m := range cfg.MCP {
140 for name, m := range cfg.MCP {
141 if m.Disabled {
141 if m.Disabled {
142 updateState(name, StateDisabled, nil, nil, Counts{})
142 updateState(name, StateDisabled, nil, nil, Counts{})
143− slog.Debug("skipping disabled mcp", "name", name)
143+ slog.Debug("Skipping disabled MCP", "name", name)
144 continue
144 continue
145 }
145 }
146
146
@@ -162,7 +162,7 @@ func Initialize(ctx context.Context, permissions permission.Service, cfg *config
162 err = fmt.Errorf("panic: %v", v)
162 err = fmt.Errorf("panic: %v", v)
163 }
163 }
164 updateState(name, StateError, err, nil, Counts{})
164 updateState(name, StateError, err, nil, Counts{})
165− slog.Error("panic in mcp client initialization", "error", err, "name", name)
165+ slog.Error("Panic in MCP client initialization", "error", err, "name", name)
166 }
166 }
167 }()
167 }()
168
168
@@ -174,7 +174,7 @@ func Initialize(ctx context.Context, permissions permission.Service, cfg *config
174
174
175 tools, err := getTools(ctx, session)
175 tools, err := getTools(ctx, session)
176 if err != nil {
176 if err != nil {
177− slog.Error("error listing tools", "error", err)
177+ slog.Error("Error listing tools", "error", err)
178 updateState(name, StateError, err, nil, Counts{})
178 updateState(name, StateError, err, nil, Counts{})
179 session.Close()
179 session.Close()
180 return
180 return
@@ -182,7 +182,7 @@ func Initialize(ctx context.Context, permissions permission.Service, cfg *config
182
182
183 prompts, err := getPrompts(ctx, session)
183 prompts, err := getPrompts(ctx, session)
184 if err != nil {
184 if err != nil {
185− slog.Error("error listing prompts", "error", err)
185+ slog.Error("Error listing prompts", "error", err)
186 updateState(name, StateError, err, nil, Counts{})
186 updateState(name, StateError, err, nil, Counts{})
187 session.Close()
187 session.Close()
188 return
188 return
@@ -277,7 +277,7 @@ func createSession(ctx context.Context, name string, m config.MCPConfig, resolve
277 transport, err := createTransport(mcpCtx, m, resolver)
277 transport, err := createTransport(mcpCtx, m, resolver)
278 if err != nil {
278 if err != nil {
279 updateState(name, StateError, err, nil, Counts{})
279 updateState(name, StateError, err, nil, Counts{})
280− slog.Error("error creating mcp client", "error", err, "name", name)
280+ slog.Error("Error creating MCP client", "error", err, "name", name)
281 cancel()
281 cancel()
282 cancelTimer.Stop()
282 cancelTimer.Stop()
283 return nil, err
283 return nil, err
@@ -319,7 +319,7 @@ func createSession(ctx context.Context, name string, m config.MCPConfig, resolve
319 }
319 }
320
320
321 cancelTimer.Stop()
321 cancelTimer.Stop()
322− slog.Info("MCP client initialized", "name", name)
322+ slog.Debug("MCP client initialized", "name", name)
323 return session, nil
323 return session, nil
324}
324}
325
325
internal/agent/tools/mcp/prompts.go+1−1
@@ -49,7 +49,7 @@ func GetPromptMessages(ctx context.Context, clientName, promptName string, args
49func RefreshPrompts(ctx context.Context, name string) {
49func RefreshPrompts(ctx context.Context, name string) {
50 session, ok := sessions.Get(name)
50 session, ok := sessions.Get(name)
51 if !ok {
51 if !ok {
52− slog.Warn("refresh prompts: no session", "name", name)
52+ slog.Warn("Refresh prompts: no session", "name", name)
53 return
53 return
54 }
54 }
55
55
internal/agent/tools/mcp/tools.go+1−1
@@ -111,7 +111,7 @@ func RunTool(ctx context.Context, name, toolName string, input string) (ToolResu
111func RefreshTools(ctx context.Context, name string) {
111func RefreshTools(ctx context.Context, name string) {
112 session, ok := sessions.Get(name)
112 session, ok := sessions.Get(name)
113 if !ok {
113 if !ok {
114− slog.Warn("refresh tools: no session", "name", name)
114+ slog.Warn("Refresh tools: no session", "name", name)
115 return
115 return
116 }
116 }
117
117
internal/app/app.go+15−11
@@ -135,7 +135,7 @@ func (app *App) Config() *config.Config {
135
135
136// RunNonInteractive runs the application in non-interactive mode with the
136// RunNonInteractive runs the application in non-interactive mode with the
137// given prompt, printing to stdout.
137// given prompt, printing to stdout.
138−func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt, largeModel, smallModel string, quiet bool) error {
138+func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt, largeModel, smallModel string, hideSpinner bool) error {
139 slog.Info("Running in non-interactive mode")
139 slog.Info("Running in non-interactive mode")
140
140
141 ctx, cancel := context.WithCancel(ctx)
141 ctx, cancel := context.WithCancel(ctx)
@@ -160,10 +160,9 @@ func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt,
160 }
160 }
161 stderrTTY = term.IsTerminal(os.Stderr.Fd())
161 stderrTTY = term.IsTerminal(os.Stderr.Fd())
162 stdinTTY = term.IsTerminal(os.Stdin.Fd())
162 stdinTTY = term.IsTerminal(os.Stdin.Fd())
163−
164 progress = app.config.Options.Progress == nil || *app.config.Options.Progress
163 progress = app.config.Options.Progress == nil || *app.config.Options.Progress
165
164
166− if !quiet && stderrTTY {
165+ if !hideSpinner && stderrTTY {
167 t := styles.CurrentTheme()
166 t := styles.CurrentTheme()
168
167
169 // Detect background color to set the appropriate color for the
168 // Detect background color to set the appropriate color for the
@@ -188,7 +187,7 @@ func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt,
188
187
189 // Helper function to stop spinner once.
188 // Helper function to stop spinner once.
190 stopSpinner := func() {
189 stopSpinner := func() {
191− if !quiet && spinner != nil {
190+ if !hideSpinner && spinner != nil {
192 spinner.Stop()
191 spinner.Stop()
193 spinner = nil
192 spinner = nil
194 }
193 }
@@ -245,6 +244,7 @@ func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt,
245
244
246 messageEvents := app.Messages.Subscribe(ctx)
245 messageEvents := app.Messages.Subscribe(ctx)
247 messageReadBytes := make(map[string]int)
246 messageReadBytes := make(map[string]int)
247+ var printed bool
248
248
249 defer func() {
249 defer func() {
250 if progress && stderrTTY {
250 if progress && stderrTTY {
@@ -268,7 +268,7 @@ func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt,
268 stopSpinner()
268 stopSpinner()
269 if result.err != nil {
269 if result.err != nil {
270 if errors.Is(result.err, context.Canceled) || errors.Is(result.err, agent.ErrRequestCancelled) {
270 if errors.Is(result.err, context.Canceled) || errors.Is(result.err, agent.ErrRequestCancelled) {
271− slog.Info("Non-interactive: agent processing cancelled", "session_id", sess.ID)
271+ slog.Debug("Non-interactive: agent processing cancelled", "session_id", sess.ID)
272 return nil
272 return nil
273 }
273 }
274 return fmt.Errorf("agent processing failed: %w", result.err)
274 return fmt.Errorf("agent processing failed: %w", result.err)
@@ -294,7 +294,11 @@ func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt,
294 if readBytes == 0 {
294 if readBytes == 0 {
295 part = strings.TrimLeft(part, " \t")
295 part = strings.TrimLeft(part, " \t")
296 }
296 }
297+ // Ignore initial whitespace-only messages.
298+ if printed || strings.TrimSpace(part) != "" {
299+ printed = true
297− fmt.Fprint(output, part)
300+ fmt.Fprint(output, part)
301+ }
298 messageReadBytes[msg.ID] = len(content)
302 messageReadBytes[msg.ID] = len(content)
299 }
303 }
300
304
@@ -433,20 +437,20 @@ func setupSubscriber[T any](
433 select {
437 select {
434 case event, ok := <-subCh:
438 case event, ok := <-subCh:
435 if !ok {
439 if !ok {
436− slog.Debug("subscription channel closed", "name", name)
440+ slog.Debug("Subscription channel closed", "name", name)
437 return
441 return
438 }
442 }
439 var msg tea.Msg = event
443 var msg tea.Msg = event
440 select {
444 select {
441 case outputCh <- msg:
445 case outputCh <- msg:
442 case <-time.After(2 * time.Second):
446 case <-time.After(2 * time.Second):
443− slog.Warn("message dropped due to slow consumer", "name", name)
447+ slog.Debug("Message dropped due to slow consumer", "name", name)
444 case <-ctx.Done():
448 case <-ctx.Done():
445− slog.Debug("subscription cancelled", "name", name)
449+ slog.Debug("Subscription cancelled", "name", name)
446 return
450 return
447 }
451 }
448 case <-ctx.Done():
452 case <-ctx.Done():
449− slog.Debug("subscription cancelled", "name", name)
453+ slog.Debug("Subscription cancelled", "name", name)
450 return
454 return
451 }
455 }
452 }
456 }
@@ -511,7 +515,7 @@ func (app *App) Subscribe(program *tea.Program) {
511// Shutdown performs a graceful shutdown of the application.
515// Shutdown performs a graceful shutdown of the application.
512func (app *App) Shutdown() {
516func (app *App) Shutdown() {
513 start := time.Now()
517 start := time.Now()
514− defer func() { slog.Info("Shutdown took " + time.Since(start).String()) }()
518+ defer func() { slog.Debug("Shutdown took " + time.Since(start).String()) }()
515
519
516 // First, cancel all agents and wait for them to finish. This must complete
520 // First, cancel all agents and wait for them to finish. This must complete
517 // before closing the DB so agents can finish writing their state.
521 // before closing the DB so agents can finish writing their state.
internal/app/lsp.go+1−1
@@ -140,7 +140,7 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, config
140 updateLSPState(name, lsp.StateReady, nil, lspClient, 0)
140 updateLSPState(name, lsp.StateReady, nil, lspClient, 0)
141 }
141 }
142
142
143− slog.Info("LSP client initialized", "name", name)
143+ slog.Debug("LSP client initialized", "name", name)
144
144
145 // Add to map with mutex protection before starting goroutine
145 // Add to map with mutex protection before starting goroutine
146 app.LSPClients.Set(name, lspClient)
146 app.LSPClients.Set(name, lspClient)
internal/cmd/run.go+11−1
@@ -8,6 +8,7 @@ import (
8 "os/signal"
8 "os/signal"
9 "strings"
9 "strings"
10
10
11+ "charm.land/log/v2"
11 "github.com/charmbracelet/crush/internal/event"
12 "github.com/charmbracelet/crush/internal/event"
12 "github.com/spf13/cobra"
13 "github.com/spf13/cobra"
13)
14)
@@ -29,9 +30,13 @@ crush run "What is this code doing?" <<< prrr.go
29
30
30# Run in quiet mode (hide the spinner)
31# Run in quiet mode (hide the spinner)
31crush run --quiet "Generate a README for this project"
32crush run --quiet "Generate a README for this project"
33+
34+# Run in verbose mode
35+crush run --verbose "Generate a README for this project"
32 `,
36 `,
33 RunE: func(cmd *cobra.Command, args []string) error {
37 RunE: func(cmd *cobra.Command, args []string) error {
34 quiet, _ := cmd.Flags().GetBool("quiet")
38 quiet, _ := cmd.Flags().GetBool("quiet")
39+ verbose, _ := cmd.Flags().GetBool("verbose")
35 largeModel, _ := cmd.Flags().GetString("model")
40 largeModel, _ := cmd.Flags().GetString("model")
36 smallModel, _ := cmd.Flags().GetString("small-model")
41 smallModel, _ := cmd.Flags().GetString("small-model")
37
42
@@ -49,6 +54,10 @@ crush run --quiet "Generate a README for this project"
49 return fmt.Errorf("no providers configured - please run 'crush' to set up a provider interactively")
54 return fmt.Errorf("no providers configured - please run 'crush' to set up a provider interactively")
50 }
55 }
51
56
57+ if verbose {
58+ slog.SetDefault(slog.New(log.New(os.Stderr)))
59+ }
60+
52 prompt := strings.Join(args, " ")
61 prompt := strings.Join(args, " ")
53
62
54 prompt, err = MaybePrependStdin(prompt)
63 prompt, err = MaybePrependStdin(prompt)
@@ -64,7 +73,7 @@ crush run --quiet "Generate a README for this project"
64 event.SetNonInteractive(true)
73 event.SetNonInteractive(true)
65 event.AppInitialized()
74 event.AppInitialized()
66
75
67− return app.RunNonInteractive(ctx, os.Stdout, prompt, largeModel, smallModel, quiet)
76+ return app.RunNonInteractive(ctx, os.Stdout, prompt, largeModel, smallModel, quiet || verbose)
68 },
77 },
69 PostRun: func(cmd *cobra.Command, args []string) {
78 PostRun: func(cmd *cobra.Command, args []string) {
70 event.AppExited()
79 event.AppExited()
@@ -73,6 +82,7 @@ crush run --quiet "Generate a README for this project"
73
82
74func init() {
83func init() {
75 runCmd.Flags().BoolP("quiet", "q", false, "Hide spinner")
84 runCmd.Flags().BoolP("quiet", "q", false, "Hide spinner")
85+ runCmd.Flags().BoolP("verbose", "v", false, "Show logs")
76 runCmd.Flags().StringP("model", "m", "", "Model to use. Accepts 'model' or 'provider/model' to disambiguate models with the same name across providers")
86 runCmd.Flags().StringP("model", "m", "", "Model to use. Accepts 'model' or 'provider/model' to disambiguate models with the same name across providers")
77 runCmd.Flags().String("small-model", "", "Small model to use. If not provided, uses the default small model for the provider")
87 runCmd.Flags().String("small-model", "", "Small model to use. If not provided, uses the default small model for the provider")
78}
88}
internal/config/config.go+2−2
@@ -317,7 +317,7 @@ func (m MCPConfig) ResolvedHeaders() map[string]string {
317 var err error
317 var err error
318 m.Headers[e], err = resolver.ResolveValue(v)
318 m.Headers[e], err = resolver.ResolveValue(v)
319 if err != nil {
319 if err != nil {
320− slog.Error("error resolving header variable", "error", err, "variable", e, "value", v)
320+ slog.Error("Error resolving header variable", "error", err, "variable", e, "value", v)
321 continue
321 continue
322 }
322 }
323 }
323 }
@@ -840,7 +840,7 @@ func resolveEnvs(envs map[string]string) []string {
840 var err error
840 var err error
841 envs[e], err = resolver.ResolveValue(v)
841 envs[e], err = resolver.ResolveValue(v)
842 if err != nil {
842 if err != nil {
843− slog.Error("error resolving environment variable", "error", err, "variable", e, "value", v)
843+ slog.Error("Error resolving environment variable", "error", err, "variable", e, "value", v)
844 continue
844 continue
845 }
845 }
846 }
846 }
internal/fsext/ls.go+6−6
@@ -144,20 +144,20 @@ func (dl *directoryLister) shouldIgnore(path string, ignorePatterns []string) bo
144 }
144 }
145
145
146 if commonIgnorePatterns().MatchesPath(relPath) {
146 if commonIgnorePatterns().MatchesPath(relPath) {
147− slog.Debug("ignoring common pattern", "path", relPath)
147+ slog.Debug("Ignoring common pattern", "path", relPath)
148 return true
148 return true
149 }
149 }
150
150
151 parentDir := filepath.Dir(path)
151 parentDir := filepath.Dir(path)
152 ignoreParser := dl.getIgnore(parentDir)
152 ignoreParser := dl.getIgnore(parentDir)
153 if ignoreParser.MatchesPath(relPath) {
153 if ignoreParser.MatchesPath(relPath) {
154− slog.Debug("ignoring dir pattern", "path", relPath, "dir", parentDir)
154+ slog.Debug("Ignoring dir pattern", "path", relPath, "dir", parentDir)
155 return true
155 return true
156 }
156 }
157
157
158 // For directories, also check with trailing slash (gitignore convention)
158 // For directories, also check with trailing slash (gitignore convention)
159 if ignoreParser.MatchesPath(relPath + "/") {
159 if ignoreParser.MatchesPath(relPath + "/") {
160− slog.Debug("ignoring dir pattern with slash", "path", relPath+"/", "dir", parentDir)
160+ slog.Debug("Ignoring dir pattern with slash", "path", relPath+"/", "dir", parentDir)
161 return true
161 return true
162 }
162 }
163
163
@@ -166,7 +166,7 @@ func (dl *directoryLister) shouldIgnore(path string, ignorePatterns []string) bo
166 }
166 }
167
167
168 if homeIgnore().MatchesPath(relPath) {
168 if homeIgnore().MatchesPath(relPath) {
169− slog.Debug("ignoring home dir pattern", "path", relPath)
169+ slog.Debug("Ignoring home dir pattern", "path", relPath)
170 return true
170 return true
171 }
171 }
172
172
@@ -177,7 +177,7 @@ func (dl *directoryLister) checkParentIgnores(path string) bool {
177 parent := filepath.Dir(filepath.Dir(path))
177 parent := filepath.Dir(filepath.Dir(path))
178 for parent != "." && path != "." {
178 for parent != "." && path != "." {
179 if dl.getIgnore(parent).MatchesPath(path) {
179 if dl.getIgnore(parent).MatchesPath(path) {
180− slog.Debug("ingoring parent dir pattern", "path", path, "dir", parent)
180+ slog.Debug("Ignoring parent dir pattern", "path", path, "dir", parent)
181 return true
181 return true
182 }
182 }
183 if parent == dl.rootPath {
183 if parent == dl.rootPath {
@@ -210,7 +210,7 @@ func ListDirectory(initialPath string, ignorePatterns []string, depth, limit int
210 found := csync.NewSlice[string]()
210 found := csync.NewSlice[string]()
211 dl := NewDirectoryLister(initialPath)
211 dl := NewDirectoryLister(initialPath)
212
212
213− slog.Debug("listing directory", "path", initialPath, "depth", depth, "limit", limit, "ignorePatterns", ignorePatterns)
213+ slog.Debug("Listing directory", "path", initialPath, "depth", depth, "limit", limit, "ignorePatterns", ignorePatterns)
214
214
215 conf := fastwalk.Config{
215 conf := fastwalk.Config{
216 Follow: true,
216 Follow: true,
internal/home/home.go+1−1
@@ -12,7 +12,7 @@ var homedir, homedirErr = os.UserHomeDir()
12
12
13func init() {
13func init() {
14 if homedirErr != nil {
14 if homedirErr != nil {
15− slog.Error("failed to get user home directory", "error", homedirErr)
15+ slog.Error("Failed to get user home directory", "error", homedirErr)
16 }
16 }
17}
17}
18
18
internal/lsp/client.go+4−4
@@ -317,12 +317,12 @@ func (c *Client) HandlesFile(path string) bool {
317 // Check if file is within working directory.
317 // Check if file is within working directory.
318 absPath, err := filepath.Abs(path)
318 absPath, err := filepath.Abs(path)
319 if err != nil {
319 if err != nil {
320− slog.Debug("cannot resolve path", "name", c.name, "file", path, "error", err)
320+ slog.Debug("Cannot resolve path", "name", c.name, "file", path, "error", err)
321 return false
321 return false
322 }
322 }
323 relPath, err := filepath.Rel(c.workDir, absPath)
323 relPath, err := filepath.Rel(c.workDir, absPath)
324 if err != nil || strings.HasPrefix(relPath, "..") {
324 if err != nil || strings.HasPrefix(relPath, "..") {
325− slog.Debug("file outside workspace", "name", c.name, "file", path, "workDir", c.workDir)
325+ slog.Debug("File outside workspace", "name", c.name, "file", path, "workDir", c.workDir)
326 return false
326 return false
327 }
327 }
328
328
@@ -339,11 +339,11 @@ func (c *Client) HandlesFile(path string) bool {
339 suffix = "." + suffix
339 suffix = "." + suffix
340 }
340 }
341 if strings.HasSuffix(name, suffix) || filetype == string(kind) {
341 if strings.HasSuffix(name, suffix) || filetype == string(kind) {
342− slog.Debug("handles file", "name", c.name, "file", name, "filetype", filetype, "kind", kind)
342+ slog.Debug("Handles file", "name", c.name, "file", name, "filetype", filetype, "kind", kind)
343 return true
343 return true
344 }
344 }
345 }
345 }
346− slog.Debug("doesn't handle file", "name", c.name, "file", name)
346+ slog.Debug("Doesn't handle file", "name", c.name, "file", name)
347 return false
347 return false
348}
348}
349
349
internal/session/session.go+1−1
@@ -203,7 +203,7 @@ func (s *service) List(ctx context.Context) ([]Session, error) {
203func (s service) fromDBItem(item db.Session) Session {
203func (s service) fromDBItem(item db.Session) Session {
204 todos, err := unmarshalTodos(item.Todos.String)
204 todos, err := unmarshalTodos(item.Todos.String)
205 if err != nil {
205 if err != nil {
206− slog.Error("failed to unmarshal todos", "session_id", item.ID, "error", err)
206+ slog.Error("Failed to unmarshal todos", "session_id", item.ID, "error", err)
207 }
207 }
208 return Session{
208 return Session{
209 ID: item.ID,
209 ID: item.ID,
internal/ui/image/image.go+1−1
@@ -168,7 +168,7 @@ func (e Encoding) Transmit(id string, img image.Image, cs CellSize, cols, rows i
168 return chunk
168 return chunk
169 },
169 },
170 }); err != nil {
170 }); err != nil {
171− slog.Error("failed to encode image for kitty graphics", "err", err)
171+ slog.Error("Failed to encode image for kitty graphics", "err", err)
172 return uiutil.InfoMsg{
172 return uiutil.InfoMsg{
173 Type: uiutil.InfoTypeError,
173 Type: uiutil.InfoTypeError,
174 Msg: "failed to encode image",
174 Msg: "failed to encode image",
internal/ui/model/history.go+1−1
@@ -27,7 +27,7 @@ func (m *UI) loadPromptHistory() tea.Cmd {
27 messages, err = m.com.App.Messages.ListAllUserMessages(ctx)
27 messages, err = m.com.App.Messages.ListAllUserMessages(ctx)
28 }
28 }
29 if err != nil {
29 if err != nil {
30− slog.Error("failed to load prompt history", "error", err)
30+ slog.Error("Failed to load prompt history", "error", err)
31 return promptHistoryLoadedMsg{messages: nil}
31 return promptHistoryLoadedMsg{messages: nil}
32 }
32 }
33
33
internal/ui/model/ui.go+3−3
@@ -330,7 +330,7 @@ func (m *UI) loadCustomCommands() tea.Cmd {
330 return func() tea.Msg {
330 return func() tea.Msg {
331 customCommands, err := commands.LoadCustomCommands(m.com.Config())
331 customCommands, err := commands.LoadCustomCommands(m.com.Config())
332 if err != nil {
332 if err != nil {
333− slog.Error("failed to load custom commands", "error", err)
333+ slog.Error("Failed to load custom commands", "error", err)
334 }
334 }
335 return userCommandsLoadedMsg{Commands: customCommands}
335 return userCommandsLoadedMsg{Commands: customCommands}
336 }
336 }
@@ -341,7 +341,7 @@ func (m *UI) loadMCPrompts() tea.Cmd {
341 return func() tea.Msg {
341 return func() tea.Msg {
342 prompts, err := commands.LoadMCPPrompts()
342 prompts, err := commands.LoadMCPPrompts()
343 if err != nil {
343 if err != nil {
344− slog.Error("failed to load mcp prompts", "error", err)
344+ slog.Error("Failed to load MCP prompts", "error", err)
345 }
345 }
346 if prompts == nil {
346 if prompts == nil {
347 // flag them as loaded even if there is none or an error
347 // flag them as loaded even if there is none or an error
@@ -683,7 +683,7 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
683 }
683 }
684 case uv.KittyGraphicsEvent:
684 case uv.KittyGraphicsEvent:
685 if !bytes.HasPrefix(msg.Payload, []byte("OK")) {
685 if !bytes.HasPrefix(msg.Payload, []byte("OK")) {
686− slog.Warn("unexpected Kitty graphics response",
686+ slog.Warn("Unexpected Kitty graphics response",
687 "response", string(msg.Payload),
687 "response", string(msg.Payload),
688 "options", msg.Options)
688 "options", msg.Options)
689 }
689 }
scripts/check_log_capitalization.shnew file+5
@@ -0,0 +1,5 @@
1+#!/bin/bash
2+if grep -rE 'slog\.(Error|Info|Warn|Debug|Fatal|Print|Println|Printf)\(["\"][a-z]' --include="*.go" . 2>/dev/null; then
3+ echo "❌ Log messages must start with a capital letter. Found lowercase logs above."
4+ exit 1
5+fi