feat: add Jules API integration as a CLI command\n\n- Added JulesConfig to pkg/config/tools.go to store API Key configuration.\n- Created a new CLI command package cmd/picoclaw/internal/jules to interact with Jules API (manage sessions, list activities, approve plans, etc.).\n- Registered the command into the main.go entrypoint.\n- Wrote comprehensive unit tests mocking HTTP requests to verify command functionality.
Co-authored-by: hobbyistlabs-coder <267281733+hobbyistlabs-coder@users.noreply.github.com>
This commit is contained in:
parent
a41d5d1bcc
commit
f61a148e7c
9 changed files with 1379 additions and 0 deletions
214
cmd/picoclaw/internal/jules/jules.go
Normal file
214
cmd/picoclaw/internal/jules/jules.go
Normal file
|
|
@ -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
|
||||
}
|
||||
214
cmd/picoclaw/internal/jules/jules.go.orig
Normal file
214
cmd/picoclaw/internal/jules/jules.go.orig
Normal file
|
|
@ -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
|
||||
}
|
||||
192
cmd/picoclaw/internal/jules/jules_test.go
Normal file
192
cmd/picoclaw/internal/jules/jules_test.go
Normal file
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ func TestNewPicoclawCommand(t *testing.T) {
|
|||
"auth",
|
||||
"cron",
|
||||
"gateway",
|
||||
"jules",
|
||||
"migrate",
|
||||
"onboard",
|
||||
"skills",
|
||||
|
|
|
|||
|
|
@ -404,6 +404,12 @@ func DefaultConfig() *Config {
|
|||
},
|
||||
ExecTimeoutMinutes: 5,
|
||||
},
|
||||
Jules: JulesConfig{
|
||||
ToolConfig: ToolConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
APIKey: "",
|
||||
},
|
||||
Exec: ExecConfig{
|
||||
ToolConfig: ToolConfig{
|
||||
Enabled: true,
|
||||
|
|
|
|||
506
pkg/config/defaults.go.orig
Normal file
506
pkg/config/defaults.go.orig
Normal file
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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":
|
||||
|
|
|
|||
236
pkg/config/tools.go.orig
Normal file
236
pkg/config/tools.go.orig
Normal file
|
|
@ -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"`
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue