diff --git a/README.ja.md b/README.ja.md index e7556b5b3..f4ee9a4bc 100644 --- a/README.ja.md +++ b/README.ja.md @@ -160,7 +160,7 @@ docker compose --profile gateway up -d ### 🚀 クイックスタート(ネイティブ) > [!TIP] -> `~/.picoclaw/config.json` に API キーを設定してください。 +> API キーは `~/.picoclaw/config.json`(または `~/.picoclaw/config.yml`)に設定してください。 > API キーの取得先: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM) > Web 検索は **任意** です - 無料の [Brave Search API](https://brave.com/search/api) (月 2000 クエリ無料) @@ -170,7 +170,7 @@ docker compose --profile gateway up -d picoclaw onboard ``` -**2. 設定** (`~/.picoclaw/config.json`) +**2. 設定** (`~/.picoclaw/config.json` または `~/.picoclaw/config.yml`) ```json { @@ -432,7 +432,7 @@ picoclaw gateway ## ⚙️ 設定 -設定ファイル: `~/.picoclaw/config.json` +設定ファイル: `~/.picoclaw/config.json`(`~/.picoclaw/config.yml` も対応) ### ワークスペース構成 @@ -741,7 +741,7 @@ Discord: https://discord.gg/V4sAZ9XWpN Web 検索を有効にするには: 1. [https://brave.com/search/api](https://brave.com/search/api) で無料の API キーを取得(月 2000 クエリ無料) -2. `~/.picoclaw/config.json` に追加: +2. 設定ファイル(`~/.picoclaw/config.json` または `~/.picoclaw/config.yml`)に追加: ```json { "tools": { diff --git a/README.md b/README.md index 59b9bea7c..c87ad8b0c 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,7 @@ docker compose --profile gateway up -d ### 🚀 Quick Start > [!TIP] -> Set your API key in `~/.picoclaw/config.json`. +> Set your API key in `~/.picoclaw/config.json` (or `~/.picoclaw/config.yml`). > Get API keys: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM) > Web search is **optional** - get free [Brave Search API](https://brave.com/search/api) (2000 free queries/month) or use built-in auto fallback. @@ -202,7 +202,7 @@ docker compose --profile gateway up -d picoclaw onboard ``` -**2. Configure** (`~/.picoclaw/config.json`) +**2. Configure** (`~/.picoclaw/config.json` or `~/.picoclaw/config.yml`) ```json { @@ -472,7 +472,7 @@ Connect Picoclaw to the Agent Social Network simply by sending a single message ## ⚙️ Configuration -Config file: `~/.picoclaw/config.json` +Config file: `~/.picoclaw/config.json` (also supports `~/.picoclaw/config.yml`) ### Workspace Layout @@ -835,7 +835,7 @@ To enable web search: 1. **Option 1 (Recommended)**: Get a free API key at [https://brave.com/search/api](https://brave.com/search/api) (2000 free queries/month) for the best results. 2. **Option 2 (No Credit Card)**: If you don't have a key, we automatically fall back to **DuckDuckGo** (no key required). -Add the key to `~/.picoclaw/config.json` if using Brave: +Add the key to your config file (`~/.picoclaw/config.json` or `~/.picoclaw/config.yml`) if using Brave: ```json { diff --git a/README.pt-br.md b/README.pt-br.md index d250cc956..37e526993 100644 --- a/README.pt-br.md +++ b/README.pt-br.md @@ -198,7 +198,7 @@ docker compose --profile gateway up -d ### 🚀 Inicio Rapido > [!TIP] -> Configure sua API key em `~/.picoclaw/config.json`. +> Configure sua API key em `~/.picoclaw/config.json` (ou `~/.picoclaw/config.yml`). > Obtenha API keys: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM) > Busca web e **opcional** — obtenha a [Brave Search API](https://brave.com/search/api) gratuita (2000 consultas gratis/mes) ou use o fallback automatico integrado. @@ -208,7 +208,7 @@ docker compose --profile gateway up -d picoclaw onboard ``` -**2. Configurar** (`~/.picoclaw/config.json`) +**2. Configurar** (`~/.picoclaw/config.json` ou `~/.picoclaw/config.yml`) ```json { @@ -478,7 +478,7 @@ Conecte o PicoClaw a Rede Social de Agentes simplesmente enviando uma unica mens ## ⚙️ Configuracao Detalhada -Arquivo de configuracao: `~/.picoclaw/config.json` +Arquivo de configuracao: `~/.picoclaw/config.json` (tambem suporta `~/.picoclaw/config.yml`) ### Estrutura do Workspace @@ -841,7 +841,7 @@ Para habilitar a busca web: 1. **Opcao 1 (Recomendado)**: Obtenha uma API key gratuita em [https://brave.com/search/api](https://brave.com/search/api) (2000 consultas gratis/mes) para os melhores resultados. 2. **Opcao 2 (Sem Cartao de Credito)**: Se voce nao tem uma key, o sistema automaticamente usa o **DuckDuckGo** como fallback (sem necessidade de key). -Adicione a key em `~/.picoclaw/config.json` se usar o Brave: +Adicione a key no arquivo de configuracao (`~/.picoclaw/config.json` ou `~/.picoclaw/config.yml`) se usar o Brave: ```json { diff --git a/README.zh.md b/README.zh.md index 6c87ba785..af4f48b21 100644 --- a/README.zh.md +++ b/README.zh.md @@ -200,7 +200,7 @@ docker compose --profile gateway up -d ### 🚀 快速开始 > [!TIP] -> 在 `~/.picoclaw/config.json` 中设置您的 API Key。 +> 在配置文件中设置您的 API Key:`~/.picoclaw/config.json`(或 `~/.picoclaw/config.yml`)。 > 获取 API Key: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu (智谱)](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM) > 网络搜索是 **可选的** - 获取免费的 [Brave Search API](https://brave.com/search/api) (每月 2000 次免费查询) @@ -211,7 +211,7 @@ picoclaw onboard ``` -**2. 配置 (Configure)** (`~/.picoclaw/config.json`) +**2. 配置 (Configure)** (`~/.picoclaw/config.json` 或 `~/.picoclaw/config.yml`) ```json { @@ -438,7 +438,7 @@ picoclaw gateway ## ⚙️ 配置详解 -配置文件路径: `~/.picoclaw/config.json` +配置文件路径: `~/.picoclaw/config.json`(也支持 `~/.picoclaw/config.yml`) ### 工作区布局 (Workspace Layout) @@ -707,7 +707,7 @@ Discord: [https://discord.gg/V4sAZ9XWpN](https://discord.gg/V4sAZ9XWpN) 启用网络搜索: 1. 在 [https://brave.com/search/api](https://brave.com/search/api) 获取免费 API Key (每月 2000 次免费查询) -2. 添加到 `~/.picoclaw/config.json`: +2. 添加到配置文件(`~/.picoclaw/config.json` 或 `~/.picoclaw/config.yml`): ```json { "tools": { @@ -741,4 +741,4 @@ Discord: [https://discord.gg/V4sAZ9XWpN](https://discord.gg/V4sAZ9XWpN) | **OpenRouter** | 200K tokens/月 | 多模型聚合 (Claude, GPT-4 等) | | **智谱 (Zhipu)** | 200K tokens/月 | 最适合中国用户 | | **Brave Search** | 2000 次查询/月 | 网络搜索功能 | -| **Groq** | 提供免费层级 | 极速推理 (Llama, Mixtral) | \ No newline at end of file +| **Groq** | 提供免费层级 | 极速推理 (Llama, Mixtral) | diff --git a/cmd/picoclaw/main.go b/cmd/picoclaw/main.go index 128f8c421..ceacc77c8 100644 --- a/cmd/picoclaw/main.go +++ b/cmd/picoclaw/main.go @@ -985,7 +985,19 @@ func authStatusCmd() { func getConfigPath() string { home, _ := os.UserHomeDir() - return filepath.Join(home, ".picoclaw", "config.json") + configDir := filepath.Join(home, ".picoclaw") + + jsonPath := filepath.Join(configDir, "config.json") + if _, err := os.Stat(jsonPath); err == nil { + return jsonPath + } + + ymlPath := filepath.Join(configDir, "config.yml") + if _, err := os.Stat(ymlPath); err == nil { + return ymlPath + } + + return jsonPath } func setupCronTool(agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration, config *config.Config) *cron.CronService { diff --git a/docs/tools_configuration.md b/docs/tools_configuration.md index 8777ddbd6..ed009f447 100644 --- a/docs/tools_configuration.md +++ b/docs/tools_configuration.md @@ -1,6 +1,6 @@ # Tools Configuration -PicoClaw's tools configuration is located in the `tools` field of `config.json`. +PicoClaw's tools configuration is located in the `tools` field of your config file (`config.json` or `config.yml`). ## Directory Structure diff --git a/pkg/channels/telegram_commands.go b/pkg/channels/telegram_commands.go index df245e156..36bff93e7 100644 --- a/pkg/channels/telegram_commands.go +++ b/pkg/channels/telegram_commands.go @@ -116,7 +116,7 @@ func (c *cmd) List(ctx context.Context, message telego.Message) error { if provider == "" { provider = "configured default" } - response = fmt.Sprintf("Configured Model: %s\nProvider: %s\n\nTo change models, update config.yaml", + response = fmt.Sprintf("Configured Model: %s\nProvider: %s\n\nTo change models, update config.json/config.yml", c.config.Agents.Defaults.Model, provider) case "channels": diff --git a/pkg/config/config.go b/pkg/config/config.go index a1cc978b6..f87df45e6 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -5,9 +5,11 @@ import ( "fmt" "os" "path/filepath" + "strings" "sync" "github.com/caarlos0/env/v11" + "gopkg.in/yaml.v3" ) // FlexibleStringSlice is a []string that also accepts JSON numbers, @@ -379,7 +381,7 @@ func LoadConfig(path string) (*Config, error) { return nil, err } - if err := json.Unmarshal(data, cfg); err != nil { + if err := unmarshalConfigData(path, data, cfg); err != nil { return nil, err } @@ -394,7 +396,7 @@ func SaveConfig(path string, cfg *Config) error { cfg.mu.RLock() defer cfg.mu.RUnlock() - data, err := json.MarshalIndent(cfg, "", " ") + data, err := marshalConfigData(path, cfg) if err != nil { return err } @@ -407,6 +409,75 @@ func SaveConfig(path string, cfg *Config) error { return os.WriteFile(path, data, 0600) } +func isYAMLPath(path string) bool { + ext := strings.ToLower(filepath.Ext(path)) + return ext == ".yml" +} + +func unmarshalConfigData(path string, data []byte, cfg *Config) error { + if isYAMLPath(path) { + return unmarshalYAMLIntoConfig(data, cfg) + } + return json.Unmarshal(data, cfg) +} + +func unmarshalYAMLIntoConfig(data []byte, cfg *Config) error { + var raw interface{} + if err := yaml.Unmarshal(data, &raw); err != nil { + return err + } + + normalized := normalizeYAMLValue(raw) + jsonData, err := json.Marshal(normalized) + if err != nil { + return err + } + return json.Unmarshal(jsonData, cfg) +} + +func normalizeYAMLValue(v interface{}) interface{} { + switch t := v.(type) { + case map[string]interface{}: + m := make(map[string]interface{}, len(t)) + for k, vv := range t { + m[k] = normalizeYAMLValue(vv) + } + return m + case map[interface{}]interface{}: + m := make(map[string]interface{}, len(t)) + for k, vv := range t { + m[fmt.Sprintf("%v", k)] = normalizeYAMLValue(vv) + } + return m + case []interface{}: + out := make([]interface{}, len(t)) + for i := range t { + out[i] = normalizeYAMLValue(t[i]) + } + return out + default: + return v + } +} + +func marshalConfigData(path string, cfg *Config) ([]byte, error) { + if !isYAMLPath(path) { + return json.MarshalIndent(cfg, "", " ") + } + + jsonData, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + var raw map[string]interface{} + if err := json.Unmarshal(jsonData, &raw); err != nil { + return nil, err + } + + return yaml.Marshal(raw) +} + func (c *Config) WorkspacePath() string { c.mu.RLock() defer c.mu.RUnlock() diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index a1f73f0b3..a75c7d10a 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -4,6 +4,7 @@ import ( "os" "path/filepath" "runtime" + "strings" "testing" ) @@ -243,3 +244,81 @@ func TestLoadConfig_OpenAIWebSearchCanBeDisabled(t *testing.T) { t.Fatal("OpenAI codex web search should be false when disabled in config file") } } + +func TestLoadConfig_YAML(t *testing.T) { + dir := t.TempDir() + configPath := filepath.Join(dir, "config.yml") + data := ` +agents: + defaults: + model: gpt-4o-mini +providers: + openai: + web_search: false +channels: + telegram: + enabled: true +` + if err := os.WriteFile(configPath, []byte(data), 0o600); err != nil { + t.Fatalf("WriteFile() error: %v", err) + } + + cfg, err := LoadConfig(configPath) + if err != nil { + t.Fatalf("LoadConfig() error: %v", err) + } + + if cfg.Agents.Defaults.Model != "gpt-4o-mini" { + t.Fatalf("Agents.Defaults.Model = %q, want %q", cfg.Agents.Defaults.Model, "gpt-4o-mini") + } + if cfg.Providers.OpenAI.WebSearch { + t.Fatal("OpenAI web_search should be false from YAML") + } + if !cfg.Channels.Telegram.Enabled { + t.Fatal("Channels.Telegram.Enabled should be true from YAML") + } +} + +func TestSaveConfig_YAML(t *testing.T) { + dir := t.TempDir() + configPath := filepath.Join(dir, "config.yml") + + cfg := DefaultConfig() + cfg.Agents.Defaults.Model = "test-model" + if err := SaveConfig(configPath, cfg); err != nil { + t.Fatalf("SaveConfig() error: %v", err) + } + + raw, err := os.ReadFile(configPath) + if err != nil { + t.Fatalf("ReadFile() error: %v", err) + } + + content := string(raw) + if !strings.Contains(content, "agents:") { + t.Fatalf("yaml output should contain root key 'agents', got: %s", content) + } + if !strings.Contains(content, "defaults:") { + t.Fatalf("yaml output should contain key 'defaults', got: %s", content) + } + if !strings.Contains(content, "model: test-model") { + t.Fatalf("yaml output should contain updated model, got: %s", content) + } +} + +func TestLoadConfig_YAML_ExtensionYAMLNotSupported(t *testing.T) { + dir := t.TempDir() + configPath := filepath.Join(dir, "config.yaml") + data := ` +agents: + defaults: + model: gpt-4o-mini +` + if err := os.WriteFile(configPath, []byte(data), 0o600); err != nil { + t.Fatalf("WriteFile() error: %v", err) + } + + if _, err := LoadConfig(configPath); err == nil { + t.Fatal("LoadConfig() should fail for .yaml; only .yml is supported") + } +}