diff --git a/cmd/picoclaw/internal/jules/jules.go b/cmd/picoclaw/internal/jules/jules.go new file mode 100644 index 000000000..db66516b1 --- /dev/null +++ b/cmd/picoclaw/internal/jules/jules.go @@ -0,0 +1,214 @@ +package jules + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + + "github.com/spf13/cobra" + + "jane/cmd/picoclaw/internal" +) + +var julesBaseURL = "https://jules.googleapis.com/v1alpha" + +func getAPIKey() (string, error) { + if key := os.Getenv("JULES_API_KEY"); key != "" { + return key, nil + } + cfg, err := internal.LoadConfig() + if err != nil { + return "", fmt.Errorf("failed to load config: %w", err) + } + if cfg.Tools.Jules.APIKey != "" { + return cfg.Tools.Jules.APIKey, nil + } + return "", fmt.Errorf("JULES_API_KEY environment variable or config value not set") +} + +func doRequest(method, url string, body []byte) error { + apiKey, err := getAPIKey() + if err != nil { + return err + } + + req, err := http.NewRequest(method, url, bytes.NewBuffer(body)) + if err != nil { + return fmt.Errorf("error creating request: %w", err) + } + + req.Header.Set("x-goog-api-key", apiKey) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("error reading response: %w", err) + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("API error (%d): %s", resp.StatusCode, string(respBody)) + } + + if len(respBody) > 0 { + var prettyJSON bytes.Buffer + if err := json.Indent(&prettyJSON, respBody, "", " "); err == nil { + fmt.Println(prettyJSON.String()) + } else { + fmt.Println(string(respBody)) + } + } else { + fmt.Println("Success") + } + + return nil +} + +func NewJulesCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "jules", + Short: "Manage Jules sessions and activities", + } + + cmd.AddCommand(newSessionCmd()) + cmd.AddCommand(newActivityCmd()) + + return cmd +} + +func newSessionCmd() *cobra.Command { + sessionCmd := &cobra.Command{ + Use: "session", + Short: "Manage Jules sessions", + } + + var prompt string + var title string + var source string + var branch string + + createCmd := &cobra.Command{ + Use: "create", + Short: "Create a new session", + RunE: func(cmd *cobra.Command, args []string) error { + if prompt == "" || source == "" { + return fmt.Errorf("--prompt and --source are required") + } + + payload := map[string]interface{}{ + "prompt": prompt, + "sourceContext": map[string]interface{}{ + "source": source, + }, + } + if title != "" { + payload["title"] = title + } + if branch != "" { + payload["sourceContext"].(map[string]interface{})["githubRepoContext"] = map[string]interface{}{ + "startingBranch": branch, + } + } + + body, _ := json.Marshal(payload) + return doRequest("POST", julesBaseURL+"/sessions", body) + }, + } + createCmd.Flags().StringVar(&prompt, "prompt", "", "The prompt for the session") + createCmd.Flags().StringVar(&title, "title", "", "The title of the session") + createCmd.Flags().StringVar(&source, "source", "", "The source repository (e.g., sources/github-owner-repo)") + createCmd.Flags().StringVar(&branch, "branch", "", "The starting branch") + + listCmd := &cobra.Command{ + Use: "list", + Short: "List sessions", + RunE: func(cmd *cobra.Command, args []string) error { + return doRequest("GET", julesBaseURL+"/sessions", nil) + }, + } + + getCmd := &cobra.Command{ + Use: "get [sessionId]", + Short: "Get a session by ID", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return doRequest("GET", julesBaseURL+"/sessions/"+args[0], nil) + }, + } + + deleteCmd := &cobra.Command{ + Use: "delete [sessionId]", + Short: "Delete a session by ID", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return doRequest("DELETE", julesBaseURL+"/sessions/"+args[0], nil) + }, + } + + var message string + messageCmd := &cobra.Command{ + Use: "message [sessionId]", + Short: "Send a message to a session", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if message == "" { + return fmt.Errorf("--message is required") + } + payload := map[string]interface{}{ + "prompt": message, + } + body, _ := json.Marshal(payload) + return doRequest("POST", julesBaseURL+"/sessions/"+args[0]+":sendMessage", body) + }, + } + messageCmd.Flags().StringVar(&message, "message", "", "The message to send") + + approveCmd := &cobra.Command{ + Use: "approve [sessionId]", + Short: "Approve a session plan", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return doRequest("POST", julesBaseURL+"/sessions/"+args[0]+":approvePlan", []byte("{}")) + }, + } + + sessionCmd.AddCommand(createCmd, listCmd, getCmd, deleteCmd, messageCmd, approveCmd) + return sessionCmd +} + +func newActivityCmd() *cobra.Command { + activityCmd := &cobra.Command{ + Use: "activity", + Short: "Manage Jules activities", + } + + listCmd := &cobra.Command{ + Use: "list [sessionId]", + Short: "List activities for a session", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return doRequest("GET", julesBaseURL+"/sessions/"+args[0]+"/activities", nil) + }, + } + + getCmd := &cobra.Command{ + Use: "get [sessionId] [activityId]", + Short: "Get an activity by ID", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + return doRequest("GET", julesBaseURL+"/sessions/"+args[0]+"/activities/"+args[1], nil) + }, + } + + activityCmd.AddCommand(listCmd, getCmd) + return activityCmd +} diff --git a/cmd/picoclaw/internal/jules/jules.go.orig b/cmd/picoclaw/internal/jules/jules.go.orig new file mode 100644 index 000000000..33ae983a4 --- /dev/null +++ b/cmd/picoclaw/internal/jules/jules.go.orig @@ -0,0 +1,214 @@ +package jules + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + + "github.com/spf13/cobra" + + "jane/cmd/picoclaw/internal" +) + +const julesBaseURL = "https://jules.googleapis.com/v1alpha" + +func getAPIKey() (string, error) { + if key := os.Getenv("JULES_API_KEY"); key != "" { + return key, nil + } + cfg, err := internal.LoadConfig() + if err != nil { + return "", fmt.Errorf("failed to load config: %w", err) + } + if cfg.Tools.Jules.APIKey != "" { + return cfg.Tools.Jules.APIKey, nil + } + return "", fmt.Errorf("JULES_API_KEY environment variable or config value not set") +} + +func doRequest(method, url string, body []byte) error { + apiKey, err := getAPIKey() + if err != nil { + return err + } + + req, err := http.NewRequest(method, url, bytes.NewBuffer(body)) + if err != nil { + return fmt.Errorf("error creating request: %w", err) + } + + req.Header.Set("x-goog-api-key", apiKey) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("error reading response: %w", err) + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("API error (%d): %s", resp.StatusCode, string(respBody)) + } + + if len(respBody) > 0 { + var prettyJSON bytes.Buffer + if err := json.Indent(&prettyJSON, respBody, "", " "); err == nil { + fmt.Println(prettyJSON.String()) + } else { + fmt.Println(string(respBody)) + } + } else { + fmt.Println("Success") + } + + return nil +} + +func NewJulesCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "jules", + Short: "Manage Jules sessions and activities", + } + + cmd.AddCommand(newSessionCmd()) + cmd.AddCommand(newActivityCmd()) + + return cmd +} + +func newSessionCmd() *cobra.Command { + sessionCmd := &cobra.Command{ + Use: "session", + Short: "Manage Jules sessions", + } + + var prompt string + var title string + var source string + var branch string + + createCmd := &cobra.Command{ + Use: "create", + Short: "Create a new session", + RunE: func(cmd *cobra.Command, args []string) error { + if prompt == "" || source == "" { + return fmt.Errorf("--prompt and --source are required") + } + + payload := map[string]interface{}{ + "prompt": prompt, + "sourceContext": map[string]interface{}{ + "source": source, + }, + } + if title != "" { + payload["title"] = title + } + if branch != "" { + payload["sourceContext"].(map[string]interface{})["githubRepoContext"] = map[string]interface{}{ + "startingBranch": branch, + } + } + + body, _ := json.Marshal(payload) + return doRequest("POST", julesBaseURL+"/sessions", body) + }, + } + createCmd.Flags().StringVar(&prompt, "prompt", "", "The prompt for the session") + createCmd.Flags().StringVar(&title, "title", "", "The title of the session") + createCmd.Flags().StringVar(&source, "source", "", "The source repository (e.g., sources/github-owner-repo)") + createCmd.Flags().StringVar(&branch, "branch", "", "The starting branch") + + listCmd := &cobra.Command{ + Use: "list", + Short: "List sessions", + RunE: func(cmd *cobra.Command, args []string) error { + return doRequest("GET", julesBaseURL+"/sessions", nil) + }, + } + + getCmd := &cobra.Command{ + Use: "get [sessionId]", + Short: "Get a session by ID", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return doRequest("GET", julesBaseURL+"/sessions/"+args[0], nil) + }, + } + + deleteCmd := &cobra.Command{ + Use: "delete [sessionId]", + Short: "Delete a session by ID", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return doRequest("DELETE", julesBaseURL+"/sessions/"+args[0], nil) + }, + } + + var message string + messageCmd := &cobra.Command{ + Use: "message [sessionId]", + Short: "Send a message to a session", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if message == "" { + return fmt.Errorf("--message is required") + } + payload := map[string]interface{}{ + "prompt": message, + } + body, _ := json.Marshal(payload) + return doRequest("POST", julesBaseURL+"/sessions/"+args[0]+":sendMessage", body) + }, + } + messageCmd.Flags().StringVar(&message, "message", "", "The message to send") + + approveCmd := &cobra.Command{ + Use: "approve [sessionId]", + Short: "Approve a session plan", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return doRequest("POST", julesBaseURL+"/sessions/"+args[0]+":approvePlan", []byte("{}")) + }, + } + + sessionCmd.AddCommand(createCmd, listCmd, getCmd, deleteCmd, messageCmd, approveCmd) + return sessionCmd +} + +func newActivityCmd() *cobra.Command { + activityCmd := &cobra.Command{ + Use: "activity", + Short: "Manage Jules activities", + } + + listCmd := &cobra.Command{ + Use: "list [sessionId]", + Short: "List activities for a session", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return doRequest("GET", julesBaseURL+"/sessions/"+args[0]+"/activities", nil) + }, + } + + getCmd := &cobra.Command{ + Use: "get [sessionId] [activityId]", + Short: "Get an activity by ID", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + return doRequest("GET", julesBaseURL+"/sessions/"+args[0]+"/activities/"+args[1], nil) + }, + } + + activityCmd.AddCommand(listCmd, getCmd) + return activityCmd +} diff --git a/cmd/picoclaw/internal/jules/jules_test.go b/cmd/picoclaw/internal/jules/jules_test.go new file mode 100644 index 000000000..05f3cce26 --- /dev/null +++ b/cmd/picoclaw/internal/jules/jules_test.go @@ -0,0 +1,192 @@ +package jules + +import ( + "bytes" + "io" + "net/http" + "net/http/httptest" + "os" + + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestJulesCommands(t *testing.T) { + os.Setenv("JULES_API_KEY", "test-api-key") + defer os.Unsetenv("JULES_API_KEY") + + var lastReq *http.Request + var lastBody []byte + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + lastReq = r + var err error + lastBody, err = io.ReadAll(r.Body) + require.NoError(t, err) + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"status": "ok"}`)) + })) + defer ts.Close() + + originalBaseURL := julesBaseURL + julesBaseURL = ts.URL + defer func() { julesBaseURL = originalBaseURL }() + + t.Run("session create", func(t *testing.T) { + cmd := NewJulesCommand() + cmd.SetArgs([]string{"session", "create", "--prompt", "test prompt", "--source", "sources/test", "--title", "test title", "--branch", "main"}) + + // Capture stdout + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + err := cmd.Execute() + require.NoError(t, err) + + w.Close() + os.Stdout = oldStdout + var buf bytes.Buffer + io.Copy(&buf, r) + + assert.Equal(t, "POST", lastReq.Method) + assert.Equal(t, "/sessions", lastReq.URL.Path) + assert.Equal(t, "test-api-key", lastReq.Header.Get("x-goog-api-key")) + + expectedBody := `{"prompt":"test prompt","sourceContext":{"githubRepoContext":{"startingBranch":"main"},"source":"sources/test"},"title":"test title"}` + // unmarshal and marshal again to compare json ignoring key order + assert.JSONEq(t, expectedBody, string(lastBody)) + assert.Contains(t, buf.String(), `"status": "ok"`) + }) + + t.Run("session list", func(t *testing.T) { + cmd := NewJulesCommand() + cmd.SetArgs([]string{"session", "list"}) + + oldStdout := os.Stdout + _, w, _ := os.Pipe() + os.Stdout = w + + err := cmd.Execute() + require.NoError(t, err) + + w.Close() + os.Stdout = oldStdout + + assert.Equal(t, "GET", lastReq.Method) + assert.Equal(t, "/sessions", lastReq.URL.Path) + assert.Equal(t, "test-api-key", lastReq.Header.Get("x-goog-api-key")) + }) + + t.Run("session get", func(t *testing.T) { + cmd := NewJulesCommand() + cmd.SetArgs([]string{"session", "get", "123"}) + + oldStdout := os.Stdout + _, w, _ := os.Pipe() + os.Stdout = w + + err := cmd.Execute() + require.NoError(t, err) + + w.Close() + os.Stdout = oldStdout + + assert.Equal(t, "GET", lastReq.Method) + assert.Equal(t, "/sessions/123", lastReq.URL.Path) + }) + + t.Run("session delete", func(t *testing.T) { + cmd := NewJulesCommand() + cmd.SetArgs([]string{"session", "delete", "123"}) + + oldStdout := os.Stdout + _, w, _ := os.Pipe() + os.Stdout = w + + err := cmd.Execute() + require.NoError(t, err) + + w.Close() + os.Stdout = oldStdout + + assert.Equal(t, "DELETE", lastReq.Method) + assert.Equal(t, "/sessions/123", lastReq.URL.Path) + }) + + t.Run("session message", func(t *testing.T) { + cmd := NewJulesCommand() + cmd.SetArgs([]string{"session", "message", "123", "--message", "hello jules"}) + + oldStdout := os.Stdout + _, w, _ := os.Pipe() + os.Stdout = w + + err := cmd.Execute() + require.NoError(t, err) + + w.Close() + os.Stdout = oldStdout + + assert.Equal(t, "POST", lastReq.Method) + assert.Equal(t, "/sessions/123:sendMessage", lastReq.URL.Path) + assert.JSONEq(t, `{"prompt":"hello jules"}`, string(lastBody)) + }) + + t.Run("session approve", func(t *testing.T) { + cmd := NewJulesCommand() + cmd.SetArgs([]string{"session", "approve", "123"}) + + oldStdout := os.Stdout + _, w, _ := os.Pipe() + os.Stdout = w + + err := cmd.Execute() + require.NoError(t, err) + + w.Close() + os.Stdout = oldStdout + + assert.Equal(t, "POST", lastReq.Method) + assert.Equal(t, "/sessions/123:approvePlan", lastReq.URL.Path) + assert.JSONEq(t, `{}`, string(lastBody)) + }) + + t.Run("activity list", func(t *testing.T) { + cmd := NewJulesCommand() + cmd.SetArgs([]string{"activity", "list", "123"}) + + oldStdout := os.Stdout + _, w, _ := os.Pipe() + os.Stdout = w + + err := cmd.Execute() + require.NoError(t, err) + + w.Close() + os.Stdout = oldStdout + + assert.Equal(t, "GET", lastReq.Method) + assert.Equal(t, "/sessions/123/activities", lastReq.URL.Path) + }) + + t.Run("activity get", func(t *testing.T) { + cmd := NewJulesCommand() + cmd.SetArgs([]string{"activity", "get", "123", "act1"}) + + oldStdout := os.Stdout + _, w, _ := os.Pipe() + os.Stdout = w + + err := cmd.Execute() + require.NoError(t, err) + + w.Close() + os.Stdout = oldStdout + + assert.Equal(t, "GET", lastReq.Method) + assert.Equal(t, "/sessions/123/activities/act1", lastReq.URL.Path) + }) +} diff --git a/cmd/picoclaw/main.go b/cmd/picoclaw/main.go index 9ba2287c7..46b3a1c15 100644 --- a/cmd/picoclaw/main.go +++ b/cmd/picoclaw/main.go @@ -17,6 +17,7 @@ import ( "jane/cmd/picoclaw/internal/auth" "jane/cmd/picoclaw/internal/cron" "jane/cmd/picoclaw/internal/gateway" + "jane/cmd/picoclaw/internal/jules" "jane/cmd/picoclaw/internal/migrate" "jane/cmd/picoclaw/internal/onboard" "jane/cmd/picoclaw/internal/skills" @@ -41,6 +42,7 @@ func NewPicoclawCommand() *cobra.Command { gateway.NewGatewayCommand(), status.NewStatusCommand(), cron.NewCronCommand(), + jules.NewJulesCommand(), migrate.NewMigrateCommand(), skills.NewSkillsCommand(), version.NewVersionCommand(), diff --git a/cmd/picoclaw/main_test.go b/cmd/picoclaw/main_test.go index 6e39b2e19..b77348249 100644 --- a/cmd/picoclaw/main_test.go +++ b/cmd/picoclaw/main_test.go @@ -38,6 +38,7 @@ func TestNewPicoclawCommand(t *testing.T) { "auth", "cron", "gateway", + "jules", "migrate", "onboard", "skills", diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index df6831799..bd5b524d7 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -404,6 +404,12 @@ func DefaultConfig() *Config { }, ExecTimeoutMinutes: 5, }, + Jules: JulesConfig{ + ToolConfig: ToolConfig{ + Enabled: true, + }, + APIKey: "", + }, Exec: ExecConfig{ ToolConfig: ToolConfig{ Enabled: true, diff --git a/pkg/config/defaults.go.orig b/pkg/config/defaults.go.orig new file mode 100644 index 000000000..df6831799 --- /dev/null +++ b/pkg/config/defaults.go.orig @@ -0,0 +1,506 @@ +// PicoClaw - Ultra-lightweight personal AI agent +// License: MIT +// +// Copyright (c) 2026 PicoClaw contributors + +package config + +import ( + "os" + "path/filepath" +) + +// DefaultConfig returns the default configuration for PicoClaw. +func DefaultConfig() *Config { + // Determine the base path for the workspace. + // Priority: $PICOCLAW_HOME > ~/.picoclaw + var homePath string + if picoclawHome := os.Getenv("PICOCLAW_HOME"); picoclawHome != "" { + homePath = picoclawHome + } else { + userHome, _ := os.UserHomeDir() + homePath = filepath.Join(userHome, ".picoclaw") + } + workspacePath := filepath.Join(homePath, "workspace") + + return &Config{ + Agents: AgentsConfig{ + Defaults: AgentDefaults{ + Workspace: workspacePath, + RestrictToWorkspace: true, + Provider: "", + Model: "", + MaxTokens: 32768, + Temperature: nil, // nil means use provider default + MaxToolIterations: 50, + SummarizeMessageThreshold: 20, + SummarizeTokenPercent: 75, + }, + List: []AgentConfig{ + { + ID: "the-clinician", + Name: "Medical Persona", + Workspace: filepath.Join(homePath, "Obsidian_Vault", "Patients"), + }, + }, + }, + Bindings: []AgentBinding{}, + Session: SessionConfig{ + DMScope: "per-channel-peer", + }, + Channels: ChannelsConfig{ + WhatsApp: WhatsAppConfig{ + Enabled: false, + BridgeURL: "ws://localhost:3001", + UseNative: false, + SessionStorePath: "", + AllowFrom: FlexibleStringSlice{}, + }, + Telegram: TelegramConfig{ + Enabled: false, + Token: "", + AllowFrom: FlexibleStringSlice{}, + Typing: TypingConfig{Enabled: true}, + Placeholder: PlaceholderConfig{ + Enabled: true, + Text: "Thinking... 💭", + }, + }, + Discord: DiscordConfig{ + Enabled: false, + Token: "", + AllowFrom: FlexibleStringSlice{}, + MentionOnly: false, + }, + MaixCam: MaixCamConfig{ + Enabled: false, + Host: "0.0.0.0", + Port: 18790, + AllowFrom: FlexibleStringSlice{}, + }, + QQ: QQConfig{ + Enabled: false, + AppID: "", + AppSecret: "", + AllowFrom: FlexibleStringSlice{}, + MaxMessageLength: 2000, + }, + DingTalk: DingTalkConfig{ + Enabled: false, + ClientID: "", + ClientSecret: "", + AllowFrom: FlexibleStringSlice{}, + }, + Slack: SlackConfig{ + Enabled: false, + BotToken: "", + AppToken: "", + AllowFrom: FlexibleStringSlice{}, + }, + Matrix: MatrixConfig{ + Enabled: false, + Homeserver: "https://matrix.org", + UserID: "", + AccessToken: "", + DeviceID: "", + JoinOnInvite: true, + AllowFrom: FlexibleStringSlice{}, + GroupTrigger: GroupTriggerConfig{ + MentionOnly: true, + }, + Placeholder: PlaceholderConfig{ + Enabled: true, + Text: "Thinking... 💭", + }, + }, + LINE: LINEConfig{ + Enabled: false, + ChannelSecret: "", + ChannelAccessToken: "", + WebhookHost: "0.0.0.0", + WebhookPort: 18791, + WebhookPath: "/webhook/line", + AllowFrom: FlexibleStringSlice{}, + GroupTrigger: GroupTriggerConfig{MentionOnly: true}, + }, + OneBot: OneBotConfig{ + Enabled: false, + WSUrl: "ws://127.0.0.1:3001", + AccessToken: "", + ReconnectInterval: 5, + GroupTriggerPrefix: []string{}, + AllowFrom: FlexibleStringSlice{}, + }, + Pico: PicoConfig{ + Enabled: false, + Token: "", + PingInterval: 30, + ReadTimeout: 60, + WriteTimeout: 10, + MaxConnections: 100, + AllowFrom: FlexibleStringSlice{}, + }, + }, + Providers: ProvidersConfig{ + OpenAI: OpenAIProviderConfig{WebSearch: true}, + }, + ModelList: []ModelConfig{ + // ============================================ + // Add your API key to the model you want to use + // ============================================ + + // Zhipu AI (智谱) - https://open.bigmodel.cn/usercenter/apikeys + { + ModelName: "glm-4.7", + Model: "zhipu/glm-4.7", + APIBase: "https://open.bigmodel.cn/api/paas/v4", + APIKey: "", + }, + + // OpenAI - https://platform.openai.com/api-keys + { + ModelName: "gpt-5.4", + Model: "openai/gpt-5.4", + APIBase: "https://api.openai.com/v1", + APIKey: "", + }, + + // Anthropic Claude - https://console.anthropic.com/settings/keys + { + ModelName: "claude-sonnet-4.6", + Model: "anthropic/claude-sonnet-4.6", + APIBase: "https://api.anthropic.com/v1", + APIKey: "", + }, + + // DeepSeek - https://platform.deepseek.com/ + { + ModelName: "deepseek-chat", + Model: "deepseek/deepseek-chat", + APIBase: "https://api.deepseek.com/v1", + APIKey: "", + }, + + // Google Gemini - https://ai.google.dev/ + { + ModelName: "gemini-2.0-flash", + Model: "gemini/gemini-2.0-flash-exp", + APIBase: "https://generativelanguage.googleapis.com/v1beta", + APIKey: "", + }, + + // Qwen (通义千问) - https://dashscope.console.aliyun.com/apiKey + { + ModelName: "qwen-plus", + Model: "qwen/qwen-plus", + APIBase: "https://dashscope.aliyuncs.com/compatible-mode/v1", + APIKey: "", + }, + + // Moonshot (月之暗面) - https://platform.moonshot.cn/console/api-keys + { + ModelName: "moonshot-v1-8k", + Model: "moonshot/moonshot-v1-8k", + APIBase: "https://api.moonshot.cn/v1", + APIKey: "", + }, + + // Groq - https://console.groq.com/keys + { + ModelName: "llama-3.3-70b", + Model: "groq/llama-3.3-70b-versatile", + APIBase: "https://api.groq.com/openai/v1", + APIKey: "", + }, + + // OpenRouter (100+ models) - https://openrouter.ai/keys + { + ModelName: "openrouter-auto", + Model: "openrouter/auto", + APIBase: "https://openrouter.ai/api/v1", + APIKey: "", + }, + { + ModelName: "openrouter-gpt-5.4", + Model: "openrouter/openai/gpt-5.4", + APIBase: "https://openrouter.ai/api/v1", + APIKey: "", + }, + + // NVIDIA - https://build.nvidia.com/ + { + ModelName: "nemotron-4-340b", + Model: "nvidia/nemotron-4-340b-instruct", + APIBase: "https://integrate.api.nvidia.com/v1", + APIKey: "", + }, + + // Cerebras - https://inference.cerebras.ai/ + { + ModelName: "cerebras-llama-3.3-70b", + Model: "cerebras/llama-3.3-70b", + APIBase: "https://api.cerebras.ai/v1", + APIKey: "", + }, + + // Vivgrid - https://vivgrid.com + { + ModelName: "vivgrid-auto", + Model: "vivgrid/auto", + APIBase: "https://api.vivgrid.com/v1", + APIKey: "", + }, + + // Volcengine (火山引擎) - https://console.volcengine.com/ark + { + ModelName: "ark-code-latest", + Model: "volcengine/ark-code-latest", + APIBase: "https://ark.cn-beijing.volces.com/api/v3", + APIKey: "", + }, + { + ModelName: "doubao-pro", + Model: "volcengine/doubao-pro-32k", + APIBase: "https://ark.cn-beijing.volces.com/api/v3", + APIKey: "", + }, + + // ShengsuanYun (神算云) + { + ModelName: "deepseek-v3", + Model: "shengsuanyun/deepseek-v3", + APIBase: "https://api.shengsuanyun.com/v1", + APIKey: "", + }, + + // Antigravity (Google Cloud Code Assist) - OAuth only + { + ModelName: "gemini-flash", + Model: "antigravity/gemini-3-flash", + AuthMethod: "oauth", + }, + + // GitHub Copilot - https://github.com/settings/tokens + { + ModelName: "copilot-gpt-5.4", + Model: "github-copilot/gpt-5.4", + APIBase: "http://localhost:4321", + AuthMethod: "oauth", + }, + + // Ollama (local) - https://ollama.com + { + ModelName: "llama3", + Model: "ollama/llama3", + APIBase: "http://localhost:11434/v1", + APIKey: "ollama", + }, + + // Mistral AI - https://console.mistral.ai/api-keys + { + ModelName: "mistral-small", + Model: "mistral/mistral-small-latest", + APIBase: "https://api.mistral.ai/v1", + APIKey: "", + }, + + // Avian - https://avian.io + { + ModelName: "deepseek-v3.2", + Model: "avian/deepseek/deepseek-v3.2", + APIBase: "https://api.avian.io/v1", + APIKey: "", + }, + { + ModelName: "kimi-k2.5", + Model: "avian/moonshotai/kimi-k2.5", + APIBase: "https://api.avian.io/v1", + APIKey: "", + }, + + // Minimax - https://api.minimaxi.com/ + { + ModelName: "MiniMax-M2.5", + Model: "minimax/MiniMax-M2.5", + APIBase: "https://api.minimaxi.com/v1", + APIKey: "", + }, + + // LongCat - https://longcat.chat/platform + { + ModelName: "LongCat-Flash-Thinking", + Model: "longcat/LongCat-Flash-Thinking", + APIBase: "https://api.longcat.chat/openai", + APIKey: "", + }, + + // VLLM (local) - http://localhost:8000 + { + ModelName: "local-model", + Model: "vllm/custom-model", + APIBase: "http://localhost:8000/v1", + APIKey: "", + }, + }, + Logger: LoggerConfig{ + TimeFormat: "15:04:05", + }, + Gateway: GatewayConfig{ + Host: "127.0.0.1", + Port: 18790, + }, + Tools: ToolsConfig{ + MediaCleanup: MediaCleanupConfig{ + ToolConfig: ToolConfig{ + Enabled: true, + }, + MaxAge: 30, + Interval: 5, + }, + Web: WebToolsConfig{ + ToolConfig: ToolConfig{ + Enabled: true, + }, + Proxy: "", + FetchLimitBytes: 10 * 1024 * 1024, // 10MB by default + Brave: BraveConfig{ + Enabled: false, + APIKey: "", + APIKeys: nil, + MaxResults: 5, + }, + Tavily: TavilyConfig{ + Enabled: false, + APIKey: "", + APIKeys: nil, + MaxResults: 5, + }, + DuckDuckGo: DuckDuckGoConfig{ + Enabled: true, + MaxResults: 5, + }, + Perplexity: PerplexityConfig{ + Enabled: false, + APIKey: "", + APIKeys: nil, + MaxResults: 5, + }, + SearXNG: SearXNGConfig{ + Enabled: false, + BaseURL: "", + MaxResults: 5, + }, + GLMSearch: GLMSearchConfig{ + Enabled: false, + APIKey: "", + BaseURL: "https://open.bigmodel.cn/api/paas/v4/web_search", + SearchEngine: "search_std", + MaxResults: 5, + }, + }, + Cron: CronToolsConfig{ + ToolConfig: ToolConfig{ + Enabled: true, + }, + ExecTimeoutMinutes: 5, + }, + Exec: ExecConfig{ + ToolConfig: ToolConfig{ + Enabled: true, + }, + EnableDenyPatterns: true, + AllowRemote: true, + TimeoutSeconds: 60, + }, + Skills: SkillsToolsConfig{ + ToolConfig: ToolConfig{ + Enabled: true, + }, + Registries: SkillsRegistriesConfig{ + ClawHub: ClawHubRegistryConfig{ + Enabled: true, + BaseURL: "https://clawhub.ai", + }, + }, + MaxConcurrentSearches: 2, + SearchCache: SearchCacheConfig{ + MaxSize: 50, + TTLSeconds: 300, + }, + }, + SendFile: ToolConfig{ + Enabled: true, + }, + MCP: MCPConfig{ + ToolConfig: ToolConfig{ + Enabled: false, + }, + Discovery: ToolDiscoveryConfig{ + Enabled: false, + TTL: 5, + MaxSearchResults: 5, + UseBM25: true, + UseRegex: false, + }, + Servers: map[string]MCPServerConfig{}, + }, + AppendFile: ToolConfig{ + Enabled: true, + }, + EditFile: ToolConfig{ + Enabled: true, + }, + FindSkills: ToolConfig{ + Enabled: true, + }, + I2C: ToolConfig{ + Enabled: false, // Hardware tool - Linux only + }, + InstallSkill: ToolConfig{ + Enabled: true, + }, + ListDir: ToolConfig{ + Enabled: true, + }, + Message: ToolConfig{ + Enabled: true, + }, + ReadFile: ReadFileToolConfig{ + Enabled: true, + MaxReadFileSize: 64 * 1024, // 64KB + }, + Spawn: ToolConfig{ + Enabled: true, + }, + SPI: ToolConfig{ + Enabled: false, // Hardware tool - Linux only + }, + Subagent: ToolConfig{ + Enabled: true, + }, + WebFetch: ToolConfig{ + Enabled: true, + }, + WriteFile: ToolConfig{ + Enabled: true, + }, + }, + Heartbeat: HeartbeatConfig{ + Enabled: true, + Interval: 30, + }, + Devices: DevicesConfig{ + Enabled: false, + MonitorUSB: true, + }, + Voice: VoiceConfig{ + EchoTranscription: false, + }, + BuildInfo: BuildInfo{ + Version: Version, + GitCommit: GitCommit, + BuildTime: BuildTime, + GoVersion: GoVersion, + }, + } +} diff --git a/pkg/config/tools.go b/pkg/config/tools.go index c47ff755f..4feba1f50 100644 --- a/pkg/config/tools.go +++ b/pkg/config/tools.go @@ -96,6 +96,11 @@ type MediaCleanupConfig struct { Interval int ` env:"PICOCLAW_MEDIA_CLEANUP_INTERVAL" json:"interval_minutes"` } +type JulesConfig struct { + ToolConfig `envPrefix:"PICOCLAW_TOOLS_JULES_"` + APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_JULES_API_KEY"` +} + type ReadFileToolConfig struct { Enabled bool `json:"enabled"` MaxReadFileSize int `json:"max_read_file_size"` @@ -106,6 +111,7 @@ type ToolsConfig struct { AllowReadPaths []string `json:"allow_read_paths" env:"PICOCLAW_TOOLS_ALLOW_READ_PATHS"` AllowWritePaths []string `json:"allow_write_paths" env:"PICOCLAW_TOOLS_ALLOW_WRITE_PATHS"` Web WebToolsConfig `json:"web"` + Jules JulesConfig `json:"jules"` Cron CronToolsConfig `json:"cron"` Exec ExecConfig `json:"exec"` Skills SkillsToolsConfig `json:"skills"` @@ -182,6 +188,8 @@ func (t *ToolsConfig) IsToolEnabled(name string) bool { switch name { case "web": return t.Web.Enabled + case "jules": + return t.Jules.Enabled case "cron": return t.Cron.Enabled case "exec": diff --git a/pkg/config/tools.go.orig b/pkg/config/tools.go.orig new file mode 100644 index 000000000..c47ff755f --- /dev/null +++ b/pkg/config/tools.go.orig @@ -0,0 +1,236 @@ +package config + +type ToolDiscoveryConfig struct { + Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_DISCOVERY_ENABLED"` + TTL int `json:"ttl" env:"PICOCLAW_TOOLS_DISCOVERY_TTL"` + MaxSearchResults int `json:"max_search_results" env:"PICOCLAW_MAX_SEARCH_RESULTS"` + UseBM25 bool `json:"use_bm25" env:"PICOCLAW_TOOLS_DISCOVERY_USE_BM25"` + UseRegex bool `json:"use_regex" env:"PICOCLAW_TOOLS_DISCOVERY_USE_REGEX"` +} + +type ToolConfig struct { + Enabled bool `json:"enabled" env:"ENABLED"` +} + +type BraveConfig struct { + Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_BRAVE_ENABLED"` + APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_BRAVE_API_KEY"` + APIKeys []string `json:"api_keys" env:"PICOCLAW_TOOLS_WEB_BRAVE_API_KEYS"` + MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_BRAVE_MAX_RESULTS"` +} + +type TavilyConfig struct { + Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_TAVILY_ENABLED"` + APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_TAVILY_API_KEY"` + APIKeys []string `json:"api_keys" env:"PICOCLAW_TOOLS_WEB_TAVILY_API_KEYS"` + BaseURL string `json:"base_url" env:"PICOCLAW_TOOLS_WEB_TAVILY_BASE_URL"` + MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_TAVILY_MAX_RESULTS"` +} + +type DuckDuckGoConfig struct { + Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_ENABLED"` + MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_MAX_RESULTS"` +} + +type PerplexityConfig struct { + Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_ENABLED"` + APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_API_KEY"` + APIKeys []string `json:"api_keys" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_API_KEYS"` + MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_PERPLEXITY_MAX_RESULTS"` +} + +type SearXNGConfig struct { + Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_SEARXNG_ENABLED"` + BaseURL string `json:"base_url" env:"PICOCLAW_TOOLS_WEB_SEARXNG_BASE_URL"` + MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_SEARXNG_MAX_RESULTS"` +} + +type GLMSearchConfig struct { + Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_GLM_ENABLED"` + APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_GLM_API_KEY"` + BaseURL string `json:"base_url" env:"PICOCLAW_TOOLS_WEB_GLM_BASE_URL"` + // SearchEngine specifies the search backend: "search_std" (default), + // "search_pro", "search_pro_sogou", or "search_pro_quark". + SearchEngine string `json:"search_engine" env:"PICOCLAW_TOOLS_WEB_GLM_SEARCH_ENGINE"` + MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_GLM_MAX_RESULTS"` +} + +type WebToolsConfig struct { + ToolConfig ` envPrefix:"PICOCLAW_TOOLS_WEB_"` + Brave BraveConfig ` json:"brave"` + Tavily TavilyConfig ` json:"tavily"` + DuckDuckGo DuckDuckGoConfig ` json:"duckduckgo"` + Perplexity PerplexityConfig ` json:"perplexity"` + SearXNG SearXNGConfig ` json:"searxng"` + GLMSearch GLMSearchConfig ` json:"glm_search"` + // Proxy is an optional proxy URL for web tools (http/https/socks5/socks5h). + // For authenticated proxies, prefer HTTP_PROXY/HTTPS_PROXY env vars instead of embedding credentials in config. + Proxy string `json:"proxy,omitempty" env:"PICOCLAW_TOOLS_WEB_PROXY"` + FetchLimitBytes int64 `json:"fetch_limit_bytes,omitempty" env:"PICOCLAW_TOOLS_WEB_FETCH_LIMIT_BYTES"` +} + +type CronToolsConfig struct { + ToolConfig ` envPrefix:"PICOCLAW_TOOLS_CRON_"` + ExecTimeoutMinutes int ` env:"PICOCLAW_TOOLS_CRON_EXEC_TIMEOUT_MINUTES" json:"exec_timeout_minutes"` // 0 means no timeout +} + +type ExecConfig struct { + ToolConfig ` envPrefix:"PICOCLAW_TOOLS_EXEC_"` + EnableDenyPatterns bool ` env:"PICOCLAW_TOOLS_EXEC_ENABLE_DENY_PATTERNS" json:"enable_deny_patterns"` + AllowRemote bool ` env:"PICOCLAW_TOOLS_EXEC_ALLOW_REMOTE" json:"allow_remote"` + CustomDenyPatterns []string ` env:"PICOCLAW_TOOLS_EXEC_CUSTOM_DENY_PATTERNS" json:"custom_deny_patterns"` + CustomAllowPatterns []string ` env:"PICOCLAW_TOOLS_EXEC_CUSTOM_ALLOW_PATTERNS" json:"custom_allow_patterns"` + TimeoutSeconds int ` env:"PICOCLAW_TOOLS_EXEC_TIMEOUT_SECONDS" json:"timeout_seconds"` // 0 means use default (60s) +} + +type SkillsToolsConfig struct { + ToolConfig ` envPrefix:"PICOCLAW_TOOLS_SKILLS_"` + Registries SkillsRegistriesConfig ` json:"registries"` + MaxConcurrentSearches int ` json:"max_concurrent_searches" env:"PICOCLAW_TOOLS_SKILLS_MAX_CONCURRENT_SEARCHES"` + SearchCache SearchCacheConfig ` json:"search_cache"` +} + +type MediaCleanupConfig struct { + ToolConfig ` envPrefix:"PICOCLAW_MEDIA_CLEANUP_"` + MaxAge int ` env:"PICOCLAW_MEDIA_CLEANUP_MAX_AGE" json:"max_age_minutes"` + Interval int ` env:"PICOCLAW_MEDIA_CLEANUP_INTERVAL" json:"interval_minutes"` +} + +type ReadFileToolConfig struct { + Enabled bool `json:"enabled"` + MaxReadFileSize int `json:"max_read_file_size"` +} + +type ToolsConfig struct { + Alpaca AlpacaConfig `json:"alpaca,omitempty"` + AllowReadPaths []string `json:"allow_read_paths" env:"PICOCLAW_TOOLS_ALLOW_READ_PATHS"` + AllowWritePaths []string `json:"allow_write_paths" env:"PICOCLAW_TOOLS_ALLOW_WRITE_PATHS"` + Web WebToolsConfig `json:"web"` + Cron CronToolsConfig `json:"cron"` + Exec ExecConfig `json:"exec"` + Skills SkillsToolsConfig `json:"skills"` + MediaCleanup MediaCleanupConfig `json:"media_cleanup"` + MCP MCPConfig `json:"mcp"` + AppendFile ToolConfig `json:"append_file" envPrefix:"PICOCLAW_TOOLS_APPEND_FILE_"` + EditFile ToolConfig `json:"edit_file" envPrefix:"PICOCLAW_TOOLS_EDIT_FILE_"` + FindSkills ToolConfig `json:"find_skills" envPrefix:"PICOCLAW_TOOLS_FIND_SKILLS_"` + I2C ToolConfig `json:"i2c" envPrefix:"PICOCLAW_TOOLS_I2C_"` + InstallSkill ToolConfig `json:"install_skill" envPrefix:"PICOCLAW_TOOLS_INSTALL_SKILL_"` + ListDir ToolConfig `json:"list_dir" envPrefix:"PICOCLAW_TOOLS_LIST_DIR_"` + Message ToolConfig `json:"message" envPrefix:"PICOCLAW_TOOLS_MESSAGE_"` + ReadFile ReadFileToolConfig `json:"read_file" envPrefix:"PICOCLAW_TOOLS_READ_FILE_"` + SendFile ToolConfig `json:"send_file" envPrefix:"PICOCLAW_TOOLS_SEND_FILE_"` + Spawn ToolConfig `json:"spawn" envPrefix:"PICOCLAW_TOOLS_SPAWN_"` + SPI ToolConfig `json:"spi" envPrefix:"PICOCLAW_TOOLS_SPI_"` + Subagent ToolConfig `json:"subagent" envPrefix:"PICOCLAW_TOOLS_SUBAGENT_"` + WebFetch ToolConfig `json:"web_fetch" envPrefix:"PICOCLAW_TOOLS_WEB_FETCH_"` + WriteFile ToolConfig `json:"write_file" envPrefix:"PICOCLAW_TOOLS_WRITE_FILE_"` + BrowserAction ToolConfig `json:"browser_action" envPrefix:"PICOCLAW_TOOLS_BROWSER_ACTION_"` + GoEval ToolConfig `json:"go_eval" envPrefix:"PICOCLAW_TOOLS_GO_EVAL_"` +} + +type SearchCacheConfig struct { + MaxSize int `json:"max_size" env:"PICOCLAW_SKILLS_SEARCH_CACHE_MAX_SIZE"` + TTLSeconds int `json:"ttl_seconds" env:"PICOCLAW_SKILLS_SEARCH_CACHE_TTL_SECONDS"` +} + +type SkillsRegistriesConfig struct { + ClawHub ClawHubRegistryConfig `json:"clawhub"` +} + +type ClawHubRegistryConfig struct { + Enabled bool `json:"enabled" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_ENABLED"` + BaseURL string `json:"base_url" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_BASE_URL"` + AuthToken string `json:"auth_token" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_AUTH_TOKEN"` + SearchPath string `json:"search_path" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_SEARCH_PATH"` + SkillsPath string `json:"skills_path" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_SKILLS_PATH"` + DownloadPath string `json:"download_path" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_DOWNLOAD_PATH"` + Timeout int `json:"timeout" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_TIMEOUT"` + MaxZipSize int `json:"max_zip_size" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_MAX_ZIP_SIZE"` + MaxResponseSize int `json:"max_response_size" env:"PICOCLAW_SKILLS_REGISTRIES_CLAWHUB_MAX_RESPONSE_SIZE"` +} + +// MCPServerConfig defines configuration for a single MCP server +type MCPServerConfig struct { + // Enabled indicates whether this MCP server is active + Enabled bool `json:"enabled"` + // Command is the executable to run (e.g., "npx", "python", "/path/to/server") + Command string `json:"command"` + // Args are the arguments to pass to the command + Args []string `json:"args,omitempty"` + // Env are environment variables to set for the server process (stdio only) + Env map[string]string `json:"env,omitempty"` + // EnvFile is the path to a file containing environment variables (stdio only) + EnvFile string `json:"env_file,omitempty"` + // Type is "stdio", "sse", or "http" (default: stdio if command is set, sse if url is set) + Type string `json:"type,omitempty"` + // URL is used for SSE/HTTP transport + URL string `json:"url,omitempty"` + // Headers are HTTP headers to send with requests (sse/http only) + Headers map[string]string `json:"headers,omitempty"` +} + +// MCPConfig defines configuration for all MCP servers +type MCPConfig struct { + ToolConfig ` envPrefix:"PICOCLAW_TOOLS_MCP_"` + Discovery ToolDiscoveryConfig ` json:"discovery"` + // Servers is a map of server name to server configuration + Servers map[string]MCPServerConfig `json:"servers,omitempty"` +} + +func (t *ToolsConfig) IsToolEnabled(name string) bool { + switch name { + case "web": + return t.Web.Enabled + case "cron": + return t.Cron.Enabled + case "exec": + return t.Exec.Enabled + case "skills": + return t.Skills.Enabled + case "media_cleanup": + return t.MediaCleanup.Enabled + case "append_file": + return t.AppendFile.Enabled + case "edit_file": + return t.EditFile.Enabled + case "find_skills": + return t.FindSkills.Enabled + case "i2c": + return t.I2C.Enabled + case "install_skill": + return t.InstallSkill.Enabled + case "list_dir": + return t.ListDir.Enabled + case "message": + return t.Message.Enabled + case "read_file": + return t.ReadFile.Enabled + case "spawn": + return t.Spawn.Enabled + case "spi": + return t.SPI.Enabled + case "subagent": + return t.Subagent.Enabled + case "web_fetch": + return t.WebFetch.Enabled + case "browser_action": + return t.BrowserAction.Enabled + case "go_eval": + return t.GoEval.Enabled + case "send_file": + return t.SendFile.Enabled + case "write_file": + return t.WriteFile.Enabled + case "mcp": + return t.MCP.Enabled + default: + return true + } +} + +type AlpacaConfig struct { + KeyID string `json:"key_id,omitempty"` + SecretKey string `json:"secret_key,omitempty"` + BaseURL string `json:"base_url,omitempty"` +}