From 4a701b3065a829682fa95e43b25acd57e0e4263d Mon Sep 17 00:00:00 2001 From: Leandro Barbosa Date: Fri, 13 Feb 2026 14:32:31 -0300 Subject: [PATCH 1/3] fix: allow claude CLI provider to run inside Claude Code sessions Remove CLAUDECODE env var from subprocess environment to allow nesting. Also parse stdout before checking exit code to handle stderr diagnostic noise without losing valid JSON output (same pattern as codex provider). --- pkg/providers/claude_cli_provider.go | 30 +++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/pkg/providers/claude_cli_provider.go b/pkg/providers/claude_cli_provider.go index a917957154..2006192e01 100644 --- a/pkg/providers/claude_cli_provider.go +++ b/pkg/providers/claude_cli_provider.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "os" "os/exec" "strings" ) @@ -41,13 +42,40 @@ func (p *ClaudeCliProvider) Chat(ctx context.Context, messages []Message, tools if p.workspace != "" { cmd.Dir = p.workspace } + + // Remove CLAUDECODE env var to allow nesting (Claude Code blocks + // subprocess launches when this variable is set). + env := os.Environ() + filtered := env[:0] + for _, e := range env { + if !strings.HasPrefix(e, "CLAUDECODE=") { + filtered = append(filtered, e) + } + } + cmd.Env = filtered + cmd.Stdin = bytes.NewReader([]byte(prompt)) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr - if err := cmd.Run(); err != nil { + err := cmd.Run() + + // Parse JSON from stdout even if exit code is non-zero, + // because claude writes diagnostic noise to stderr but still + // produces valid JSON output. + if stdoutStr := stdout.String(); stdoutStr != "" { + resp, parseErr := p.parseClaudeCliResponse(stdoutStr) + if parseErr == nil && resp != nil && resp.Content != "" { + return resp, nil + } + } + + if err != nil { + if ctx.Err() == context.Canceled { + return nil, ctx.Err() + } if stderrStr := stderr.String(); stderrStr != "" { return nil, fmt.Errorf("claude cli error: %s", stderrStr) } From dc42aa7b181afd996a13eec814eae921de4d1012 Mon Sep 17 00:00:00 2001 From: Leandro Barbosa Date: Fri, 13 Feb 2026 14:39:43 -0300 Subject: [PATCH 2/3] style: fix gofmt formatting in base_test.go and web.go Remove trailing whitespace and extra newlines flagged by CI fmt-check. --- pkg/channels/base_test.go | 1 - pkg/tools/web.go | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pkg/channels/base_test.go b/pkg/channels/base_test.go index f82b04c46f..78c6d1d669 100644 --- a/pkg/channels/base_test.go +++ b/pkg/channels/base_test.go @@ -50,4 +50,3 @@ func TestBaseChannelIsAllowed(t *testing.T) { }) } } - diff --git a/pkg/tools/web.go b/pkg/tools/web.go index 6fc89c95bb..804d9d168a 100644 --- a/pkg/tools/web.go +++ b/pkg/tools/web.go @@ -114,7 +114,7 @@ func (p *DuckDuckGoSearchProvider) Search(ctx context.Context, query string, cou func (p *DuckDuckGoSearchProvider) extractResults(html string, count int, query string) (string, error) { // Simple regex based extraction for DDG HTML // Strategy: Find all result containers or key anchors directly - + // Try finding the result links directly first, as they are the most critical // Pattern: Title // The previous regex was a bit strict. Let's make it more flexible for attributes order/content @@ -133,14 +133,14 @@ func (p *DuckDuckGoSearchProvider) extractResults(html string, count int, query // But simple global search for snippets might mismatch order. // Since we only have the raw HTML string, let's just extract snippets globally and assume order matches (risky but simple for regex) // Or better: Let's assume the snippet follows the link in the HTML - + // A better regex approach: iterate through text and find matches in order // But for now, let's grab all snippets too reSnippet := regexp.MustCompile(`([\s\S]*?)`) snippetMatches := reSnippet.FindAllStringSubmatch(html, count+5) maxItems := min(len(matches), count) - + for i := 0; i < maxItems; i++ { urlStr := matches[i][1] title := stripTags(matches[i][2]) @@ -157,7 +157,7 @@ func (p *DuckDuckGoSearchProvider) extractResults(html string, count int, query } lines = append(lines, fmt.Sprintf("%d. %s\n %s", i+1, title, urlStr)) - + // Attempt to attach snippet if available and index aligns if i < len(snippetMatches) { snippet := stripTags(snippetMatches[i][1]) From e61faea8cea473dc728927a688aadeb7d6e47d86 Mon Sep 17 00:00:00 2001 From: Leandro Barbosa Date: Fri, 13 Feb 2026 14:45:48 -0300 Subject: [PATCH 3/3] fix: update tests for WebSearchToolOptions API change Upstream refactored NewWebSearchTool to accept WebSearchToolOptions struct instead of positional args. Update config_test.go and web_test.go to match the new API. --- pkg/config/config_test.go | 8 ++++---- pkg/tools/web_test.go | 25 +++++++++---------------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 0a5e7b56f4..9320ce0f2f 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -136,11 +136,11 @@ func TestDefaultConfig_WebTools(t *testing.T) { cfg := DefaultConfig() // Verify web tools defaults - if cfg.Tools.Web.Search.MaxResults != 5 { - t.Error("Expected MaxResults 5, got ", cfg.Tools.Web.Search.MaxResults) + if cfg.Tools.Web.Brave.MaxResults != 5 { + t.Error("Expected Brave MaxResults 5, got ", cfg.Tools.Web.Brave.MaxResults) } - if cfg.Tools.Web.Search.APIKey != "" { - t.Error("Search API key should be empty by default") + if cfg.Tools.Web.Brave.APIKey != "" { + t.Error("Brave API key should be empty by default") } } diff --git a/pkg/tools/web_test.go b/pkg/tools/web_test.go index 30bc7d9910..a526ea34a0 100644 --- a/pkg/tools/web_test.go +++ b/pkg/tools/web_test.go @@ -173,30 +173,23 @@ func TestWebTool_WebFetch_Truncation(t *testing.T) { } } -// TestWebTool_WebSearch_NoApiKey verifies error handling when API key is missing +// TestWebTool_WebSearch_NoApiKey verifies that no tool is created when API key is missing func TestWebTool_WebSearch_NoApiKey(t *testing.T) { - tool := NewWebSearchTool("", 5) - ctx := context.Background() - args := map[string]interface{}{ - "query": "test", - } - - result := tool.Execute(ctx, args) - - // Should return error result - if !result.IsError { - t.Errorf("Expected error when API key is missing") + tool := NewWebSearchTool(WebSearchToolOptions{BraveEnabled: true, BraveAPIKey: ""}) + if tool != nil { + t.Errorf("Expected nil tool when Brave API key is empty") } - // Should mention missing API key - if !strings.Contains(result.ForLLM, "BRAVE_API_KEY") && !strings.Contains(result.ForUser, "BRAVE_API_KEY") { - t.Errorf("Expected API key error message, got ForLLM: %s", result.ForLLM) + // Also nil when nothing is enabled + tool = NewWebSearchTool(WebSearchToolOptions{}) + if tool != nil { + t.Errorf("Expected nil tool when no provider is enabled") } } // TestWebTool_WebSearch_MissingQuery verifies error handling for missing query func TestWebTool_WebSearch_MissingQuery(t *testing.T) { - tool := NewWebSearchTool("test-key", 5) + tool := NewWebSearchTool(WebSearchToolOptions{BraveEnabled: true, BraveAPIKey: "test-key", BraveMaxResults: 5}) ctx := context.Background() args := map[string]interface{}{}