diff --git a/.golangci.yaml b/.golangci.yaml index dd3cbae190..d0ba907169 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -28,9 +28,7 @@ linters: - wsl_v5 # TODO: Disabled, because they are failing at the moment, we should fix them and enable (step by step) - - bodyclose - contextcheck - - dogsled - embeddedstructfieldcheck - errcheck - errchkjson @@ -45,21 +43,16 @@ linters: - gocritic - gocyclo - godox - - goprintffuncname - gosec - ineffassign - lll - maintidx - - misspell - mnd - modernize - - nakedret - nestif - nilnil - paralleltest - perfsprint - - prealloc - - predeclared - revive - staticcheck - tagalign @@ -68,8 +61,6 @@ linters: - unparam - usestdlibvars - usetesting - - wastedassign - - whitespace settings: errcheck: check-type-assertions: true diff --git a/cmd/picoclaw/internal/helpers.go b/cmd/picoclaw/internal/helpers.go index a084dc1bea..1f52df5dd4 100644 --- a/cmd/picoclaw/internal/helpers.go +++ b/cmd/picoclaw/internal/helpers.go @@ -37,15 +37,13 @@ func FormatVersion() string { } // FormatBuildInfo returns build time and go version info -func FormatBuildInfo() (build string, goVer string) { - if buildTime != "" { - build = buildTime - } - goVer = goVersion +func FormatBuildInfo() (string, string) { + build := buildTime + goVer := goVersion if goVer == "" { goVer = runtime.Version() } - return + return build, goVer } // GetVersion returns the version string diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index dbc4a9b870..540563b071 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -800,7 +800,7 @@ func (al *AgentLoop) forceCompression(agent *AgentInstance, sessionKey string) { droppedCount := mid keptConversation := conversation[mid:] - newHistory := make([]providers.Message, 0) + newHistory := make([]providers.Message, 0, 1+len(keptConversation)+1) // Append compression note to the original system prompt instead of adding a new system message // This avoids having two consecutive system messages which some APIs (like Zhipu) reject diff --git a/pkg/auth/oauth.go b/pkg/auth/oauth.go index cf8c1c9c4b..ba757ffd4c 100644 --- a/pkg/auth/oauth.go +++ b/pkg/auth/oauth.go @@ -156,7 +156,7 @@ func LoginBrowser(cfg OAuthProviderConfig) (*AuthCredential, error) { return exchangeCodeForTokens(cfg, result.code, pkce.CodeVerifier, redirectURI) case manualInput := <-manualCh: if manualInput == "" { - return nil, fmt.Errorf("manual input cancelled") + return nil, fmt.Errorf("manual input canceled") } // Extract code from URL if it's a full URL code := manualInput diff --git a/pkg/channels/discord.go b/pkg/channels/discord.go index 20f3b267ca..f6faa3373b 100644 --- a/pkg/channels/discord.go +++ b/pkg/channels/discord.go @@ -233,7 +233,7 @@ func (c *DiscordChannel) handleMessage(s *discordgo.Session, m *discordgo.Messag if localPath != "" { localFiles = append(localFiles, localPath) - transcribedText := "" + var transcribedText string if c.transcriber != nil && c.transcriber.IsAvailable() { ctx, cancel := context.WithTimeout(c.getContext(), transcriptionTimeout) result, err := c.transcriber.Transcribe(ctx, localPath) diff --git a/pkg/channels/onebot.go b/pkg/channels/onebot.go index cee8ad9d33..4576a11ce0 100644 --- a/pkg/channels/onebot.go +++ b/pkg/channels/onebot.go @@ -174,7 +174,10 @@ func (c *OneBotChannel) connect() error { header["Authorization"] = []string{"Bearer " + c.config.AccessToken} } - conn, _, err := dialer.Dial(c.config.WSUrl, header) + conn, resp, err := dialer.Dial(c.config.WSUrl, header) + if resp != nil { + resp.Body.Close() + } if err != nil { return err } @@ -310,7 +313,7 @@ func (c *OneBotChannel) sendAPIRequest(action string, params any, timeout time.D case <-time.After(timeout): return nil, fmt.Errorf("API request %s timed out after %v", action, timeout) case <-c.ctx.Done(): - return nil, fmt.Errorf("context cancelled") + return nil, fmt.Errorf("context canceled") } } @@ -695,7 +698,6 @@ func (c *OneBotChannel) parseMessageSegments(raw json.RawMessage, selfID int64) textParts = append(textParts, "[forward message]") default: - } } diff --git a/pkg/channels/slack.go b/pkg/channels/slack.go index f087aa8da8..cfb731b169 100644 --- a/pkg/channels/slack.go +++ b/pkg/channels/slack.go @@ -439,5 +439,5 @@ func parseSlackChatID(chatID string) (channelID, threadTS string) { if len(parts) > 1 { threadTS = parts[1] } - return + return channelID, threadTS } diff --git a/pkg/channels/telegram.go b/pkg/channels/telegram.go index 5cd51e8bcd..5244948498 100644 --- a/pkg/channels/telegram.go +++ b/pkg/channels/telegram.go @@ -265,7 +265,7 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes localFiles = append(localFiles, voicePath) mediaPaths = append(mediaPaths, voicePath) - transcribedText := "" + var transcribedText string if c.transcriber != nil && c.transcriber.IsAvailable() { transcriberCtx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() diff --git a/pkg/channels/whatsapp.go b/pkg/channels/whatsapp.go index 958d850bb7..2dc4017ac4 100644 --- a/pkg/channels/whatsapp.go +++ b/pkg/channels/whatsapp.go @@ -41,7 +41,10 @@ func (c *WhatsAppChannel) Start(ctx context.Context) error { dialer := websocket.DefaultDialer dialer.HandshakeTimeout = 10 * time.Second - conn, _, err := dialer.Dial(c.url, nil) + conn, resp, err := dialer.Dial(c.url, nil) + if resp != nil { + resp.Body.Close() + } if err != nil { return fmt.Errorf("failed to connect to WhatsApp bridge: %w", err) } diff --git a/pkg/heartbeat/service.go b/pkg/heartbeat/service.go index 75d6248b9b..e05a9fdbf6 100644 --- a/pkg/heartbeat/service.go +++ b/pkg/heartbeat/service.go @@ -166,7 +166,7 @@ func (hs *HeartbeatService) executeHeartbeat() { } if handler == nil { - hs.logError("Heartbeat handler not configured") + hs.logErrorf("Heartbeat handler not configured") return } @@ -175,23 +175,23 @@ func (hs *HeartbeatService) executeHeartbeat() { channel, chatID := hs.parseLastChannel(lastChannel) // Debug log for channel resolution - hs.logInfo("Resolved channel: %s, chatID: %s (from lastChannel: %s)", channel, chatID, lastChannel) + hs.logInfof("Resolved channel: %s, chatID: %s (from lastChannel: %s)", channel, chatID, lastChannel) result := handler(prompt, channel, chatID) if result == nil { - hs.logInfo("Heartbeat handler returned nil result") + hs.logInfof("Heartbeat handler returned nil result") return } // Handle different result types if result.IsError { - hs.logError("Heartbeat error: %s", result.ForLLM) + hs.logErrorf("Heartbeat error: %s", result.ForLLM) return } if result.Async { - hs.logInfo("Async task started: %s", result.ForLLM) + hs.logInfof("Async task started: %s", result.ForLLM) logger.InfoCF("heartbeat", "Async heartbeat task started", map[string]any{ "message": result.ForLLM, @@ -201,7 +201,7 @@ func (hs *HeartbeatService) executeHeartbeat() { // Check if silent if result.Silent { - hs.logInfo("Heartbeat OK - silent") + hs.logInfof("Heartbeat OK - silent") return } @@ -212,7 +212,7 @@ func (hs *HeartbeatService) executeHeartbeat() { hs.sendResponse(result.ForLLM) } - hs.logInfo("Heartbeat completed: %s", result.ForLLM) + hs.logInfof("Heartbeat completed: %s", result.ForLLM) } // buildPrompt builds the heartbeat prompt from HEARTBEAT.md @@ -225,7 +225,7 @@ func (hs *HeartbeatService) buildPrompt() string { hs.createDefaultHeartbeatTemplate() return "" } - hs.logError("Error reading HEARTBEAT.md: %v", err) + hs.logErrorf("Error reading HEARTBEAT.md: %v", err) return "" } @@ -276,9 +276,9 @@ Add your heartbeat tasks below this line: ` if err := os.WriteFile(heartbeatPath, []byte(defaultContent), 0o644); err != nil { - hs.logError("Failed to create default HEARTBEAT.md: %v", err) + hs.logErrorf("Failed to create default HEARTBEAT.md: %v", err) } else { - hs.logInfo("Created default HEARTBEAT.md template") + hs.logInfof("Created default HEARTBEAT.md template") } } @@ -289,14 +289,14 @@ func (hs *HeartbeatService) sendResponse(response string) { hs.mu.RUnlock() if msgBus == nil { - hs.logInfo("No message bus configured, heartbeat result not sent") + hs.logInfof("No message bus configured, heartbeat result not sent") return } // Get last channel from state lastChannel := hs.state.GetLastChannel() if lastChannel == "" { - hs.logInfo("No last channel recorded, heartbeat result not sent") + hs.logInfof("No last channel recorded, heartbeat result not sent") return } @@ -313,7 +313,7 @@ func (hs *HeartbeatService) sendResponse(response string) { Content: response, }) - hs.logInfo("Heartbeat result sent to %s", platform) + hs.logInfof("Heartbeat result sent to %s", platform) } // parseLastChannel parses the last channel string into platform and userID. @@ -326,7 +326,7 @@ func (hs *HeartbeatService) parseLastChannel(lastChannel string) (platform, user // Parse channel format: "platform:user_id" (e.g., "telegram:123456") parts := strings.SplitN(lastChannel, ":", 2) if len(parts) != 2 || parts[0] == "" || parts[1] == "" { - hs.logError("Invalid last channel format: %s", lastChannel) + hs.logErrorf("Invalid last channel format: %s", lastChannel) return "", "" } @@ -334,25 +334,25 @@ func (hs *HeartbeatService) parseLastChannel(lastChannel string) (platform, user // Skip internal channels if constants.IsInternalChannel(platform) { - hs.logInfo("Skipping internal channel: %s", platform) + hs.logInfof("Skipping internal channel: %s", platform) return "", "" } return platform, userID } -// logInfo logs an informational message to the heartbeat log -func (hs *HeartbeatService) logInfo(format string, args ...any) { - hs.log("INFO", format, args...) +// logInfof logs an informational message to the heartbeat log +func (hs *HeartbeatService) logInfof(format string, args ...any) { + hs.logf("INFO", format, args...) } -// logError logs an error message to the heartbeat log -func (hs *HeartbeatService) logError(format string, args ...any) { - hs.log("ERROR", format, args...) +// logErrorf logs an error message to the heartbeat log +func (hs *HeartbeatService) logErrorf(format string, args ...any) { + hs.logf("ERROR", format, args...) } -// log writes a message to the heartbeat log file -func (hs *HeartbeatService) log(level, format string, args ...any) { +// logf writes a message to the heartbeat log file +func (hs *HeartbeatService) logf(level, format string, args ...any) { logFile := filepath.Join(hs.workspace, "heartbeat.log") f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { diff --git a/pkg/heartbeat/service_test.go b/pkg/heartbeat/service_test.go index a4dfa7a720..a7aef8c3a9 100644 --- a/pkg/heartbeat/service_test.go +++ b/pkg/heartbeat/service_test.go @@ -191,7 +191,7 @@ func TestLogPath(t *testing.T) { hs := NewHeartbeatService(tmpDir, 30, true) // Write a log entry - hs.log("INFO", "Test log entry") + hs.logf("INFO", "Test log entry") // Verify log file exists at workspace root expectedLogPath := filepath.Join(tmpDir, "heartbeat.log") diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index c14fbd464e..56dc87a536 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -153,7 +153,7 @@ func formatComponent(component string) string { } func formatFields(fields map[string]any) string { - var parts []string + parts := make([]string, 0, len(fields)) for k, v := range fields { parts = append(parts, fmt.Sprintf("%s=%v", k, v)) } diff --git a/pkg/providers/codex_cli_credentials_test.go b/pkg/providers/codex_cli_credentials_test.go index 43b21700a6..1e88c1120a 100644 --- a/pkg/providers/codex_cli_credentials_test.go +++ b/pkg/providers/codex_cli_credentials_test.go @@ -43,12 +43,18 @@ func TestReadCodexCliCredentials_Valid(t *testing.T) { } } +// readCodexCliCredentialsErr calls ReadCodexCliCredentials and returns only the +// error, for tests that only need to assert on failure. +func readCodexCliCredentialsErr() error { + _, _, _, err := ReadCodexCliCredentials() //nolint:dogsled + return err +} + func TestReadCodexCliCredentials_MissingFile(t *testing.T) { tmpDir := t.TempDir() t.Setenv("CODEX_HOME", tmpDir) - _, _, _, err := ReadCodexCliCredentials() - if err == nil { + if err := readCodexCliCredentialsErr(); err == nil { t.Fatal("expected error for missing auth.json") } } @@ -64,8 +70,7 @@ func TestReadCodexCliCredentials_EmptyToken(t *testing.T) { t.Setenv("CODEX_HOME", tmpDir) - _, _, _, err := ReadCodexCliCredentials() - if err == nil { + if err := readCodexCliCredentialsErr(); err == nil { t.Fatal("expected error for empty access_token") } } @@ -80,8 +85,7 @@ func TestReadCodexCliCredentials_InvalidJSON(t *testing.T) { t.Setenv("CODEX_HOME", tmpDir) - _, _, _, err := ReadCodexCliCredentials() - if err == nil { + if err := readCodexCliCredentialsErr(); err == nil { t.Fatal("expected error for invalid JSON") } } diff --git a/pkg/providers/codex_provider.go b/pkg/providers/codex_provider.go index ecc983642c..195374bea4 100644 --- a/pkg/providers/codex_provider.go +++ b/pkg/providers/codex_provider.go @@ -106,8 +106,8 @@ func (p *CodexProvider) Chat( if evt.Type == "response.completed" || evt.Type == "response.failed" || evt.Type == "response.incomplete" { evtResp := evt.Response if evtResp.ID != "" { - copy := evtResp - resp = © + evtRespCopy := evtResp + resp = &evtRespCopy } } } diff --git a/pkg/providers/github_copilot_provider.go b/pkg/providers/github_copilot_provider.go index 9210021e1c..3fb15db2f1 100644 --- a/pkg/providers/github_copilot_provider.go +++ b/pkg/providers/github_copilot_provider.go @@ -44,7 +44,6 @@ func NewGitHubCopilotProvider(uri string, connectMode string, model string) (*Gi Hooks: &copilot.SessionHooks{}, }) if err != nil { - client.Stop() return nil, fmt.Errorf("create session failed: %w", err) } @@ -101,7 +100,7 @@ func (p *GitHubCopilotProvider) Chat( return nil, fmt.Errorf("provider closed") } - resp, err := session.SendAndWait(ctx, copilot.MessageOptions{ + resp, _ := session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: string(fullcontent), }) diff --git a/pkg/tools/shell.go b/pkg/tools/shell.go index 6883172cd6..ad1664b5bc 100644 --- a/pkg/tools/shell.go +++ b/pkg/tools/shell.go @@ -76,10 +76,9 @@ func NewExecTool(workingDir string, restrict bool) *ExecTool { func NewExecToolWithConfig(workingDir string, restrict bool, config *config.Config) *ExecTool { denyPatterns := make([]*regexp.Regexp, 0) - enableDenyPatterns := true if config != nil { execConfig := config.Tools.Exec - enableDenyPatterns = execConfig.EnableDenyPatterns + enableDenyPatterns := execConfig.EnableDenyPatterns if enableDenyPatterns { denyPatterns = append(denyPatterns, defaultDenyPatterns...) if len(execConfig.CustomDenyPatterns) > 0 { diff --git a/pkg/tools/subagent.go b/pkg/tools/subagent.go index 91ebff6368..ad371a6492 100644 --- a/pkg/tools/subagent.go +++ b/pkg/tools/subagent.go @@ -132,12 +132,12 @@ After completing the task, provide a clear summary of what was done.` }, } - // Check if context is already cancelled before starting + // Check if context is already canceled before starting select { case <-ctx.Done(): sm.mu.Lock() - task.Status = "cancelled" - task.Result = "Task cancelled before execution" + task.Status = "canceled" + task.Result = "Task canceled before execution" sm.mu.Unlock() return default: @@ -185,10 +185,10 @@ After completing the task, provide a clear summary of what was done.` if err != nil { task.Status = "failed" task.Result = fmt.Sprintf("Error: %v", err) - // Check if it was cancelled + // Check if it was canceled if ctx.Err() != nil { - task.Status = "cancelled" - task.Result = "Task cancelled during execution" + task.Status = "canceled" + task.Result = "Task canceled during execution" } result = &ToolResult{ ForLLM: task.Result,