diff --git a/go.mod b/go.mod index be3c73ef0..18d762b25 100644 --- a/go.mod +++ b/go.mod @@ -8,13 +8,16 @@ require ( github.com/bwmarrin/discordgo v0.29.0 github.com/caarlos0/env/v11 v11.3.1 github.com/chzyer/readline v1.5.1 + github.com/gdamore/tcell/v2 v2.13.8 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 + github.com/joho/godotenv v1.5.1 github.com/larksuite/oapi-sdk-go/v3 v3.5.3 github.com/mdp/qrterminal/v3 v3.2.1 github.com/mymmrac/telego v1.6.0 github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1 github.com/openai/openai-go/v3 v3.22.0 + github.com/rivo/tview v0.42.0 github.com/slack-go/slack v0.17.3 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 @@ -34,9 +37,7 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect github.com/gdamore/encoding v1.0.1 // indirect - github.com/gdamore/tcell/v2 v2.13.8 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/joho/godotenv v1.5.1 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -44,7 +45,6 @@ require ( github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/rivo/tview v0.42.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rs/zerolog v1.34.0 // indirect github.com/spf13/pflag v1.0.10 // indirect diff --git a/pkg/config/config.go b/pkg/config/config.go index ba2adc9ae..ffcb809a3 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -6,7 +6,6 @@ import ( "log" "os" "path/filepath" - "strings" "sync/atomic" "github.com/caarlos0/env/v11" @@ -590,9 +589,28 @@ type ClawHubRegistryConfig struct { func LoadConfig(path string) (*Config, error) { cfg := DefaultConfig() + // Load .env file from config directory (secrets, API keys, etc.) + // This runs before reading config.json so .env works even on fresh installs. + envFile := filepath.Join(filepath.Dir(path), ".env") + if err := godotenv.Load(envFile); err != nil { + if os.IsNotExist(err) { + log.Printf("[INFO] No .env file found at %s; skipping .env loading", envFile) + } else { + log.Printf("[WARN] Failed to load .env file from %s: %v", envFile, err) + } + } + data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { + // No config file — still apply env vars + overrides to default config + if err := env.Parse(cfg); err != nil { + return nil, err + } + loadProviderEnvOverrides(cfg) + if cfg.HasProvidersConfig() { + cfg.ModelList = ConvertProvidersToModelList(cfg) + } return cfg, nil } return nil, err @@ -616,12 +634,6 @@ func LoadConfig(path string) (*Config, error) { return nil, err } - // Load .env file from config directory (secrets, API keys, etc.) - envFile := filepath.Join(filepath.Dir(path), ".env") - if err := godotenv.Load(envFile); err != nil { - log.Printf("[INFO] No .env file loaded from %s: %v", envFile, err) - } - if err := env.Parse(cfg); err != nil { return nil, err } @@ -816,6 +828,12 @@ func loadProviderEnvOverrides(cfg *Config) { {"SHENGSUANYUN", &cfg.Providers.ShengSuanYun.APIKey, &cfg.Providers.ShengSuanYun.APIBase}, {"DEEPSEEK", &cfg.Providers.DeepSeek.APIKey, &cfg.Providers.DeepSeek.APIBase}, {"MISTRAL", &cfg.Providers.Mistral.APIKey, &cfg.Providers.Mistral.APIBase}, + {"VLLM", &cfg.Providers.VLLM.APIKey, &cfg.Providers.VLLM.APIBase}, + {"CEREBRAS", &cfg.Providers.Cerebras.APIKey, &cfg.Providers.Cerebras.APIBase}, + {"VOLCENGINE", &cfg.Providers.VolcEngine.APIKey, &cfg.Providers.VolcEngine.APIBase}, + {"QWEN", &cfg.Providers.Qwen.APIKey, &cfg.Providers.Qwen.APIBase}, + // Note: GitHubCopilot and Antigravity use different auth patterns (ConnectMode/AuthMethod), + // not standard APIKey/APIBase, so they are not included here. } for _, p := range providers { if v := os.Getenv("PICOCLAW_PROVIDERS_" + p.name + "_API_KEY"); v != "" { @@ -826,20 +844,3 @@ func loadProviderEnvOverrides(cfg *Config) { } } } - -// SanitizeForLog masks sensitive API key values for safe logging. -func SanitizeForLog(s string) string { - prefixes := []string{"sk-", "xoxb-", "xoxp-", "gsk_", "AIza"} - for _, p := range prefixes { - if strings.HasPrefix(s, p) { - if len(s) > len(p)+4 { - return s[:len(p)+4] + "****" - } - return p + "****" - } - } - if len(s) > 8 { - return s[:8] + "****" - } - return "****" -} diff --git a/pkg/tools/web.go b/pkg/tools/web.go index 63eb470f4..7130df94e 100644 --- a/pkg/tools/web.go +++ b/pkg/tools/web.go @@ -748,7 +748,11 @@ func (p *ExaSearchProvider) Search(ctx context.Context, query string, count int) } var sb strings.Builder - for i, r := range result.Results { + maxResults := count + if maxResults > len(result.Results) { + maxResults = len(result.Results) + } + for i, r := range result.Results[:maxResults] { sb.WriteString(fmt.Sprintf("%d. %s\n URL: %s\n", i+1, r.Title, r.URL)) if r.Text != "" { snippet := r.Text