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
This commit is contained in:
YS Liu 2026-02-26 15:06:32 +08:00
parent 59e196e3ee
commit 5b637adaec
9 changed files with 729 additions and 1 deletions

View file

@ -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 <id>",
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
}

View file

@ -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
}

View file

@ -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 ""
}
}

View file

@ -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 <key> <value>",
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)
}
}

View file

@ -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
}

View file

@ -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 <id>",
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
}

View file

@ -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 <id>",
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
}

View file

@ -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
}

173
docs/config-cli.md Normal file
View file

@ -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 models config
```bash
# Show all fields for that model (sensitive fields are masked)
picoclaw config model_list get <model_name>
# Show only one keys value (for scripting; not masked)
picoclaw config model_list get <model_name> <key>
```
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 <model_name> <key> <value>
```
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 youll 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 <model_name>
```
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 <model_name> [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 <key> <value>
```
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 <id> [--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 <id>
```
Removes the agent with the given id from `agents.list` and saves the config.
### update — Update an agent
```bash
picoclaw config agent update <id> [--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 <model_name> [key] [value]` |
| Add/remove/update models | `picoclaw config model_list add/remove/update ...` |
| Agent defaults | `picoclaw config agent defaults get [key]` / `set <key> <value>` |
| 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`.