From 5b637adaecee2919f6e3b3777dea7f3d87d037f5 Mon Sep 17 00:00:00 2001 From: YS Liu Date: Thu, 26 Feb 2026 15:06:32 +0800 Subject: [PATCH] Add config agent commands and config-cli docs - config agent defaults: get [key] / set key value for agents.defaults - config agent list: list agents in agents.list - config agent add/remove/update: manage agents by id with optional flags - docs/config-cli.md: English usage for model_list and agent CLI Made-with: Cursor --- cmd/picoclaw/internal/configcmd/agent_add.go | 83 +++++++++ .../internal/configcmd/agent_defaults.go | 18 ++ .../internal/configcmd/agent_defaults_get.go | 104 +++++++++++ .../internal/configcmd/agent_defaults_set.go | 120 ++++++++++++ cmd/picoclaw/internal/configcmd/agent_list.go | 69 +++++++ .../internal/configcmd/agent_remove.go | 56 ++++++ .../internal/configcmd/agent_update.go | 87 +++++++++ cmd/picoclaw/internal/configcmd/command.go | 20 +- docs/config-cli.md | 173 ++++++++++++++++++ 9 files changed, 729 insertions(+), 1 deletion(-) create mode 100644 cmd/picoclaw/internal/configcmd/agent_add.go create mode 100644 cmd/picoclaw/internal/configcmd/agent_defaults.go create mode 100644 cmd/picoclaw/internal/configcmd/agent_defaults_get.go create mode 100644 cmd/picoclaw/internal/configcmd/agent_defaults_set.go create mode 100644 cmd/picoclaw/internal/configcmd/agent_list.go create mode 100644 cmd/picoclaw/internal/configcmd/agent_remove.go create mode 100644 cmd/picoclaw/internal/configcmd/agent_update.go create mode 100644 docs/config-cli.md diff --git a/cmd/picoclaw/internal/configcmd/agent_add.go b/cmd/picoclaw/internal/configcmd/agent_add.go new file mode 100644 index 000000000..57fd1810a --- /dev/null +++ b/cmd/picoclaw/internal/configcmd/agent_add.go @@ -0,0 +1,83 @@ +package configcmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/sipeed/picoclaw/cmd/picoclaw/internal" + "github.com/sipeed/picoclaw/pkg/config" +) + +func newAgentAddCommand() *cobra.Command { + var ( + name string + model string + workspace string + defaultAgent bool + ) + + cmd := &cobra.Command{ + Use: "add ", + Short: "Add an agent to agents.list", + Args: cobra.ExactArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + return runAgentAdd(args[0], name, model, workspace, defaultAgent) + }, + } + + cmd.Flags().StringVar(&name, "name", "", "Display name") + cmd.Flags().StringVar(&model, "model", "", "Model name (from model_list)") + cmd.Flags().StringVar(&workspace, "workspace", "", "Workspace path") + cmd.Flags().BoolVar(&defaultAgent, "default", false, "Set as default agent") + + return cmd +} + +func runAgentAdd(id, name, model, workspace string, defaultAgent bool) error { + cfg, err := internal.LoadConfig() + if err != nil { + return fmt.Errorf("loading config: %w", err) + } + + configPath := internal.GetConfigPath() + if _, err := os.Stat(configPath); err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("config not found; run: picoclaw onboard") + } + return err + } + + // Interactive prompt when TTY and missing fields + if IsTTY() { + if name == "" { + name, _ = Prompt("Name (display): ") + } + if model == "" { + model, _ = Prompt("Model (model_name from model_list): ") + } + if workspace == "" { + workspace, _ = Prompt("Workspace: ") + } + } + + entry := config.AgentConfig{ + ID: id, + Default: defaultAgent, + Name: name, + Workspace: workspace, + } + if model != "" { + entry.Model = &config.AgentModelConfig{Primary: model} + } + + cfg.Agents.List = append(cfg.Agents.List, entry) + + if err := config.SaveConfig(configPath, cfg); err != nil { + return fmt.Errorf("saving config: %w", err) + } + + fmt.Printf("Added agent %q to agents.list.\n", id) + return nil +} diff --git a/cmd/picoclaw/internal/configcmd/agent_defaults.go b/cmd/picoclaw/internal/configcmd/agent_defaults.go new file mode 100644 index 000000000..12ed1c76a --- /dev/null +++ b/cmd/picoclaw/internal/configcmd/agent_defaults.go @@ -0,0 +1,18 @@ +package configcmd + +import ( + "github.com/spf13/cobra" +) + +func newAgentDefaultsCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "defaults", + Short: "Get or set agents.defaults", + RunE: func(cmd *cobra.Command, _ []string) error { + return cmd.Help() + }, + } + cmd.AddCommand(newAgentDefaultsGetCommand()) + cmd.AddCommand(newAgentDefaultsSetCommand()) + return cmd +} diff --git a/cmd/picoclaw/internal/configcmd/agent_defaults_get.go b/cmd/picoclaw/internal/configcmd/agent_defaults_get.go new file mode 100644 index 000000000..0f04f6b90 --- /dev/null +++ b/cmd/picoclaw/internal/configcmd/agent_defaults_get.go @@ -0,0 +1,104 @@ +package configcmd + +import ( + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" + + "github.com/sipeed/picoclaw/cmd/picoclaw/internal" + "github.com/sipeed/picoclaw/pkg/config" +) + +// agentDefaultsKeys in display order for get (all). +var agentDefaultsKeys = []string{ + "workspace", "restrict_to_workspace", "provider", "model_name", "model", + "model_fallbacks", "image_model", "image_model_fallbacks", + "max_tokens", "temperature", "max_tool_iterations", +} + +var agentDefaultsKeySet map[string]bool + +func init() { + agentDefaultsKeySet = make(map[string]bool, len(agentDefaultsKeys)) + for _, k := range agentDefaultsKeys { + agentDefaultsKeySet[k] = true + } +} + +func newAgentDefaultsGetCommand() *cobra.Command { + return &cobra.Command{ + Use: "get [key]", + Short: "Get agents.defaults (all fields or one key)", + Args: cobra.MaximumNArgs(1), + RunE: runAgentDefaultsGet, + } +} + +func runAgentDefaultsGet(_ *cobra.Command, args []string) error { + cfg, err := internal.LoadConfig() + if err != nil { + return fmt.Errorf("loading config: %w", err) + } + + configPath := internal.GetConfigPath() + if _, err := os.Stat(configPath); err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("config not found; run: picoclaw onboard") + } + return err + } + + d := &cfg.Agents.Defaults + + if len(args) == 0 { + for _, key := range agentDefaultsKeys { + value := agentDefaultsGetValue(d, key) + fmt.Printf("%s: %s\n", key, value) + } + return nil + } + + key := args[0] + if !agentDefaultsKeySet[key] { + return fmt.Errorf("invalid key %q; allowed: %s", key, strings.Join(agentDefaultsKeys, ", ")) + } + fmt.Println(agentDefaultsGetValue(d, key)) + return nil +} + +func agentDefaultsGetValue(d *config.AgentDefaults, key string) string { + switch key { + case "workspace": + return d.Workspace + case "restrict_to_workspace": + if d.RestrictToWorkspace { + return "true" + } + return "false" + case "provider": + return d.Provider + case "model_name": + return d.ModelName + case "model": + return d.Model + case "model_fallbacks": + return strings.Join(d.ModelFallbacks, ",") + case "image_model": + return d.ImageModel + case "image_model_fallbacks": + return strings.Join(d.ImageModelFallbacks, ",") + case "max_tokens": + return fmt.Sprintf("%d", d.MaxTokens) + case "temperature": + if d.Temperature == nil { + return "" + } + return fmt.Sprintf("%g", *d.Temperature) + case "max_tool_iterations": + return fmt.Sprintf("%d", d.MaxToolIterations) + default: + return "" + } +} diff --git a/cmd/picoclaw/internal/configcmd/agent_defaults_set.go b/cmd/picoclaw/internal/configcmd/agent_defaults_set.go new file mode 100644 index 000000000..3b787bfb6 --- /dev/null +++ b/cmd/picoclaw/internal/configcmd/agent_defaults_set.go @@ -0,0 +1,120 @@ +package configcmd + +import ( + "fmt" + "os" + "strconv" + "strings" + + "github.com/spf13/cobra" + + "github.com/sipeed/picoclaw/cmd/picoclaw/internal" + "github.com/sipeed/picoclaw/pkg/config" +) + +func newAgentDefaultsSetCommand() *cobra.Command { + return &cobra.Command{ + Use: "set ", + Short: "Set a single field in agents.defaults", + Args: cobra.ExactArgs(2), + RunE: runAgentDefaultsSet, + } +} + +func runAgentDefaultsSet(_ *cobra.Command, args []string) error { + cfg, err := internal.LoadConfig() + if err != nil { + return fmt.Errorf("loading config: %w", err) + } + + configPath := internal.GetConfigPath() + if _, err := os.Stat(configPath); err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("config not found; run: picoclaw onboard") + } + return err + } + + key, value := args[0], args[1] + if !agentDefaultsKeySet[key] { + return fmt.Errorf("invalid key %q; allowed: %s", key, strings.Join(agentDefaultsKeys, ", ")) + } + + d := &cfg.Agents.Defaults + + switch key { + case "workspace": + d.Workspace = value + case "restrict_to_workspace": + b, err := parseBool(value) + if err != nil { + return fmt.Errorf("restrict_to_workspace: %w", err) + } + d.RestrictToWorkspace = b + case "provider": + d.Provider = value + case "model_name": + d.ModelName = value + case "model": + d.Model = value + case "model_fallbacks": + if value == "" { + d.ModelFallbacks = nil + } else { + d.ModelFallbacks = strings.Split(value, ",") + for i := range d.ModelFallbacks { + d.ModelFallbacks[i] = strings.TrimSpace(d.ModelFallbacks[i]) + } + } + case "image_model": + d.ImageModel = value + case "image_model_fallbacks": + if value == "" { + d.ImageModelFallbacks = nil + } else { + d.ImageModelFallbacks = strings.Split(value, ",") + for i := range d.ImageModelFallbacks { + d.ImageModelFallbacks[i] = strings.TrimSpace(d.ImageModelFallbacks[i]) + } + } + case "max_tokens": + n, err := strconv.Atoi(value) + if err != nil { + return fmt.Errorf("max_tokens: %w", err) + } + d.MaxTokens = n + case "temperature": + if value == "" { + d.Temperature = nil + } else { + f, err := strconv.ParseFloat(value, 64) + if err != nil { + return fmt.Errorf("temperature: %w", err) + } + d.Temperature = &f + } + case "max_tool_iterations": + n, err := strconv.Atoi(value) + if err != nil { + return fmt.Errorf("max_tool_iterations: %w", err) + } + d.MaxToolIterations = n + } + + if err := config.SaveConfig(configPath, cfg); err != nil { + return fmt.Errorf("saving config: %w", err) + } + fmt.Printf("Set agents.defaults %s.\n", key) + return nil +} + +func parseBool(s string) (bool, error) { + switch strings.ToLower(s) { + case "true", "1", "yes": + return true, nil + case "false", "0", "no": + return false, nil + default: + return false, fmt.Errorf("expected true/false, got %q", s) + } +} diff --git a/cmd/picoclaw/internal/configcmd/agent_list.go b/cmd/picoclaw/internal/configcmd/agent_list.go new file mode 100644 index 000000000..0ad817ef8 --- /dev/null +++ b/cmd/picoclaw/internal/configcmd/agent_list.go @@ -0,0 +1,69 @@ +package configcmd + +import ( + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" + + "github.com/sipeed/picoclaw/cmd/picoclaw/internal" +) + +func newAgentListCommand() *cobra.Command { + return &cobra.Command{ + Use: "list", + Short: "List all agents in agents.list", + Args: cobra.NoArgs, + RunE: runAgentList, + } +} + +func runAgentList(_ *cobra.Command, _ []string) error { + cfg, err := internal.LoadConfig() + if err != nil { + return fmt.Errorf("loading config: %w", err) + } + + configPath := internal.GetConfigPath() + if _, err := os.Stat(configPath); err != nil { + if os.IsNotExist(err) { + fmt.Println("No config file found. Run: picoclaw onboard") + return nil + } + return err + } + + if len(cfg.Agents.List) == 0 { + fmt.Println("agents.list is empty.") + return nil + } + + fmt.Printf("%-20s %-25s %-20s %s\n", "ID", "NAME", "MODEL", "WORKSPACE") + fmt.Println(strings.Repeat("-", 85)) + + for _, a := range cfg.Agents.List { + model := "" + if a.Model != nil && a.Model.Primary != "" { + model = a.Model.Primary + } + if len(model) > 18 { + model = model[:15] + "..." + } + name := a.Name + if len(name) > 23 { + name = name[:20] + "..." + } + id := a.ID + if len(id) > 18 { + id = id[:15] + "..." + } + ws := a.Workspace + if len(ws) > 35 { + ws = ws[:32] + "..." + } + fmt.Printf("%-20s %-25s %-20s %s\n", id, name, model, ws) + } + + return nil +} diff --git a/cmd/picoclaw/internal/configcmd/agent_remove.go b/cmd/picoclaw/internal/configcmd/agent_remove.go new file mode 100644 index 000000000..30ddce4d6 --- /dev/null +++ b/cmd/picoclaw/internal/configcmd/agent_remove.go @@ -0,0 +1,56 @@ +package configcmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/sipeed/picoclaw/cmd/picoclaw/internal" + "github.com/sipeed/picoclaw/pkg/config" +) + +func newAgentRemoveCommand() *cobra.Command { + return &cobra.Command{ + Use: "remove ", + Short: "Remove an agent from agents.list by id", + Args: cobra.ExactArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + return runAgentRemove(args[0]) + }, + } +} + +func runAgentRemove(id string) error { + cfg, err := internal.LoadConfig() + if err != nil { + return fmt.Errorf("loading config: %w", err) + } + + configPath := internal.GetConfigPath() + if _, err := os.Stat(configPath); err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("config not found; run: picoclaw onboard") + } + return err + } + + var kept []config.AgentConfig + for _, a := range cfg.Agents.List { + if a.ID != id { + kept = append(kept, a) + } + } + + if len(kept) == len(cfg.Agents.List) { + return fmt.Errorf("no agent with id %q", id) + } + + cfg.Agents.List = kept + if err := config.SaveConfig(configPath, cfg); err != nil { + return fmt.Errorf("saving config: %w", err) + } + + fmt.Printf("Removed agent %q from agents.list.\n", id) + return nil +} diff --git a/cmd/picoclaw/internal/configcmd/agent_update.go b/cmd/picoclaw/internal/configcmd/agent_update.go new file mode 100644 index 000000000..d85056e35 --- /dev/null +++ b/cmd/picoclaw/internal/configcmd/agent_update.go @@ -0,0 +1,87 @@ +package configcmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/sipeed/picoclaw/cmd/picoclaw/internal" + "github.com/sipeed/picoclaw/pkg/config" +) + +func newAgentUpdateCommand() *cobra.Command { + var ( + name string + model string + workspace string + defaultAgent bool + ) + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update the first matching agent in agents.list", + Args: cobra.ExactArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + return runAgentUpdate(args[0], name, model, workspace, defaultAgent) + }, + } + + cmd.Flags().StringVar(&name, "name", "", "Display name") + cmd.Flags().StringVar(&model, "model", "", "Model name (from model_list)") + cmd.Flags().StringVar(&workspace, "workspace", "", "Workspace path") + cmd.Flags().BoolVar(&defaultAgent, "default", false, "Set as default agent") + + return cmd +} + +func runAgentUpdate(id, name, model, workspace string, defaultAgent bool) error { + cfg, err := internal.LoadConfig() + if err != nil { + return fmt.Errorf("loading config: %w", err) + } + + configPath := internal.GetConfigPath() + if _, err := os.Stat(configPath); err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("config not found; run: picoclaw onboard") + } + return err + } + + idx := -1 + for i := range cfg.Agents.List { + if cfg.Agents.List[i].ID == id { + idx = i + break + } + } + if idx < 0 { + return fmt.Errorf("no agent with id %q", id) + } + + entry := &cfg.Agents.List[idx] + + if name != "" { + entry.Name = name + } + if model != "" { + if entry.Model == nil { + entry.Model = &config.AgentModelConfig{} + } + entry.Model.Primary = model + } + if workspace != "" { + entry.Workspace = workspace + } + if defaultAgent { + entry.Default = true + } + + if err := config.SaveConfig(configPath, cfg); err != nil { + return fmt.Errorf("saving config: %w", err) + } + + fmt.Printf("Updated agent %q in agents.list.\n", id) + return nil +} diff --git a/cmd/picoclaw/internal/configcmd/command.go b/cmd/picoclaw/internal/configcmd/command.go index c2663ea4e..1d8ab9e93 100644 --- a/cmd/picoclaw/internal/configcmd/command.go +++ b/cmd/picoclaw/internal/configcmd/command.go @@ -7,13 +7,14 @@ import ( func NewConfigCommand() *cobra.Command { cmd := &cobra.Command{ Use: "config", - Short: "Manage configuration (model_list)", + Short: "Manage configuration (model_list, agents)", RunE: func(cmd *cobra.Command, _ []string) error { return cmd.Help() }, } cmd.AddCommand(newModelListCommand()) + cmd.AddCommand(newAgentCommand()) return cmd } @@ -36,3 +37,20 @@ func newModelListCommand() *cobra.Command { ) return cmd } + +func newAgentCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "agent", + Short: "Manage agents (defaults, list, add, remove, update)", + RunE: func(cmd *cobra.Command, _ []string) error { + return cmd.Help() + }, + } + + cmd.AddCommand(newAgentDefaultsCommand()) + cmd.AddCommand(newAgentListCommand()) + cmd.AddCommand(newAgentAddCommand()) + cmd.AddCommand(newAgentRemoveCommand()) + cmd.AddCommand(newAgentUpdateCommand()) + return cmd +} diff --git a/docs/config-cli.md b/docs/config-cli.md new file mode 100644 index 000000000..3b2900ec0 --- /dev/null +++ b/docs/config-cli.md @@ -0,0 +1,173 @@ +# Config CLI Usage + +Use `picoclaw config` to manage `model_list` and `agents` without editing `config.json` directly. + +Default config path: `~/.picoclaw/config.json`. If the file does not exist, some commands will prompt you to run `picoclaw onboard` first. + +--- + +## 1. model_list + +### list — List all models + +```bash +picoclaw config model_list list +``` + +Prints the current `model_list` in a table (MODEL_NAME, MODEL, API_BASE, AUTH, etc.). Shows a message when the list is empty or the config file is missing. + +### get — Inspect a single model’s config + +```bash +# Show all fields for that model (sensitive fields are masked) +picoclaw config model_list get + +# Show only one key’s value (for scripting; not masked) +picoclaw config model_list get +``` + +Supported keys (snake_case, matching JSON): +`model_name`, `model`, `api_base`, `api_key`, `proxy`, `auth_method`, `connect_mode`, `workspace`, `token_url`, `client_id`, `client_secret`, `max_tokens_field`, `rpm`. + +### set — Set a single field + +```bash +picoclaw config model_list set +``` + +Sets the given key for the **first** entry whose `model_name` matches, then writes the config. +`rpm` is an integer; all other keys are strings. The key must be one of the supported keys above. + +Examples: + +```bash +picoclaw config model_list set gpt4 api_base https://api.openai.com/v1 +picoclaw config model_list set gpt4 rpm 60 +``` + +### add — Add a model + +```bash +# Specify everything with flags +picoclaw config model_list add --model-name qwen-turbo --model litellm/qwen-turbo \ + --api-base https://litellm.example.com \ + --token-url https://keycloak.example.com/realms/xxx/protocol/openid-connect/token \ + --client-id ai-bot --client-secret xxx + +# Interactive: pass model_name only or omit; in a TTY you’ll be prompted for the rest +picoclaw config model_list add qwen-turbo +picoclaw config model_list add +``` + +Common flags: +`--model-name`, `--model`, `--api-base`, `--api-key`, `--proxy`, `--auth-method`, `--max-tokens-field`, `--token-url`, `--client-id`, `--client-secret`. +For `litellm/...` protocol you must provide `api_base`, `token_url`, `client_id`, and `client_secret`; in a TTY, missing values are prompted interactively. + +### remove — Remove model(s) + +```bash +picoclaw config model_list remove +``` + +Removes entries by `model_name`. If there are multiple entries with the same name (e.g. round-robin), all are removed by default. Use `--first` to remove only the first match: + +```bash +picoclaw config model_list remove loadbalanced-gpt4 --first +``` + +### update — Update a model + +```bash +picoclaw config model_list update [flags] +``` + +Updates the **first** entry matching `model_name`; only the fields corresponding to the given flags are changed. +Flags are the same as for add: `--model`, `--api-base`, `--api-key`, `--token-url`, `--client-id`, `--client-secret`, `--proxy`, `--auth-method`, `--max-tokens-field`, etc. + +Example: + +```bash +picoclaw config model_list update qwen-turbo --api-base https://new.example.com +``` + +--- + +## 2. agent + +### defaults — Global defaults (agents.defaults) + +**get** + +```bash +# Print all agents.defaults fields +picoclaw config agent defaults get + +# Print a single key +picoclaw config agent defaults get model_name +``` + +Supported keys: +`workspace`, `restrict_to_workspace`, `provider`, `model_name`, `model`, `model_fallbacks`, `image_model`, `image_model_fallbacks`, `max_tokens`, `temperature`, `max_tool_iterations`. + +**set** + +```bash +picoclaw config agent defaults set +``` + +Examples: + +```bash +picoclaw config agent defaults set model_name gpt4 +picoclaw config agent defaults set max_tokens 8192 +picoclaw config agent defaults set restrict_to_workspace true +picoclaw config agent defaults set model_fallbacks "gpt4,claude" +``` + +`restrict_to_workspace` is true/false; `max_tokens` and `max_tool_iterations` are integers; `temperature` is a float; `model_fallbacks` and `image_model_fallbacks` are comma-separated strings. + +### list — List all agents + +```bash +picoclaw config agent list +``` + +Prints `agents.list` in a table (ID, NAME, MODEL, WORKSPACE). Shows a message when the list is empty. + +### add — Add an agent + +```bash +picoclaw config agent add [--name "Display name"] [--model gpt4] [--workspace ~/ws] [--default] +``` + +`id` is required; `--name`, `--model`, and `--workspace` are optional. In a TTY, missing values are prompted. `--default` sets this agent as the default. + +### remove — Remove an agent + +```bash +picoclaw config agent remove +``` + +Removes the agent with the given id from `agents.list` and saves the config. + +### update — Update an agent + +```bash +picoclaw config agent update [--name "New name"] [--model gpt4] [--workspace ~/ws] [--default] +``` + +Only the provided fields are updated; others are left unchanged. + +--- + +## 3. Quick reference + +| Purpose | Command | +|----------------------|--------| +| List models | `picoclaw config model_list list` | +| Get/set one model | `picoclaw config model_list get/set [key] [value]` | +| Add/remove/update models | `picoclaw config model_list add/remove/update ...` | +| Agent defaults | `picoclaw config agent defaults get [key]` / `set ` | +| List/add/remove/update agents | `picoclaw config agent list/add/remove/update ...` | + +For more options and descriptions, run `picoclaw config --help`, `picoclaw config model_list --help`, and `picoclaw config agent --help`.