package main import ( "bufio" "context" "encoding/json" "fmt" "os" "sort" "strconv" "strings" "time" "github.com/YspCoder/clawgo/pkg/config" "github.com/YspCoder/clawgo/pkg/configops" "github.com/YspCoder/clawgo/pkg/providers" ) func configCmd() { if len(os.Args) < 3 { configHelp() return } switch os.Args[2] { case "set": configSetCmd() case "get": configGetCmd() case "check": configCheckCmd() case "reload": configReloadCmd() default: fmt.Printf("Unknown config command: %s\n", os.Args[2]) configHelp() } } func configHelp() { fmt.Println("\nConfig commands:") fmt.Println(" set Set config value and trigger hot reload") fmt.Println(" get Get config value") fmt.Println(" check Validate current config") fmt.Println(" reload Trigger gateway hot reload") fmt.Println() fmt.Println("Examples:") fmt.Println(" clawgo config set channels.telegram.enabled true") fmt.Println(" clawgo config set channels.telegram.enable true") fmt.Println(" clawgo config get models.providers.openai.api_base") fmt.Println(" clawgo config check") fmt.Println(" clawgo config reload") } func configSetCmd() { if len(os.Args) < 5 { fmt.Println("Usage: clawgo config set ") return } configPath := getConfigPath() cfgMap, err := loadConfigAsMap(configPath) if err != nil { fmt.Printf("Error loading config: %v\n", err) return } path := normalizeConfigPath(os.Args[3]) args := os.Args[4:] valueParts := make([]string, 0, len(args)) for i := 0; i < len(args); i++ { part := args[i] if part == "--debug" || part == "-d" { continue } if part == "--config" { i++ continue } if strings.HasPrefix(part, "--config=") { continue } valueParts = append(valueParts, part) } if len(valueParts) == 0 { fmt.Println("Error: value is required") return } value := parseConfigValue(strings.Join(valueParts, " ")) if err := setMapValueByPath(cfgMap, path, value); err != nil { fmt.Printf("Error setting value: %v\n", err) return } data, err := json.MarshalIndent(cfgMap, "", " ") if err != nil { fmt.Printf("Error serializing config: %v\n", err) return } backupPath, err := writeConfigAtomicWithBackup(configPath, data) if err != nil { fmt.Printf("Error writing config: %v\n", err) return } fmt.Printf("Updated %s = %v\n", path, value) running, err := triggerGatewayReload() if err != nil { if running { if rbErr := rollbackConfigFromBackup(configPath, backupPath); rbErr != nil { fmt.Printf("Hot reload failed and rollback failed: %v\n", rbErr) } else { fmt.Printf("Hot reload failed, config rolled back: %v\n", err) } return } fmt.Printf("Updated config file. Hot reload not applied: %v\n", err) } else { fmt.Println("Gateway hot reload signal sent") } } func configGetCmd() { if len(os.Args) < 4 { fmt.Println("Usage: clawgo config get ") return } configPath := getConfigPath() cfgMap, err := loadConfigAsMap(configPath) if err != nil { fmt.Printf("Error loading config: %v\n", err) return } path := normalizeConfigPath(os.Args[3]) value, ok := getMapValueByPath(cfgMap, path) if !ok { fmt.Printf("Path not found: %s\n", path) return } data, err := json.Marshal(value) if err != nil { fmt.Printf("%v\n", value) return } fmt.Println(string(data)) } func configReloadCmd() { if _, err := triggerGatewayReload(); err != nil { fmt.Printf("Hot reload not applied: %v\n", err) return } fmt.Println("Gateway hot reload signal sent") } func configCheckCmd() { cfg, err := config.LoadConfig(getConfigPath()) if err != nil { fmt.Printf("Config load failed: %v\n", err) return } validationErrors := config.Validate(cfg) if len(validationErrors) == 0 { fmt.Println("Config validation passed") return } fmt.Println("Config validation failed:") for _, ve := range validationErrors { fmt.Printf(" - %v\n", ve) } } func providerCmd() { if len(os.Args) >= 3 { switch strings.TrimSpace(os.Args[2]) { case "login": providerLoginCmd() return case "list": providerListCmd() return case "use": providerUseCmd() return case "configure": // Continue into the interactive editor below. } } cfg, err := loadConfig() if err != nil { fmt.Printf("Error loading config: %v\n", err) os.Exit(1) } reader := bufio.NewReader(os.Stdin) defaultProvider, defaultModel := config.ParseProviderModelRef(cfg.Agents.Defaults.Model.Primary) if defaultProvider == "" { defaultProvider = config.PrimaryProviderName(cfg) } available := providerNames(cfg) fmt.Printf("Current primary provider: %s\n", defaultProvider) if defaultModel != "" { fmt.Printf("Current primary model: %s\n", defaultModel) } fmt.Printf("Available providers: %s\n", strings.Join(available, ", ")) argName := "" if len(os.Args) >= 3 { argName = strings.TrimSpace(os.Args[2]) } if argName == "" || strings.HasPrefix(argName, "-") { argName = defaultProvider } providerName := promptLine(reader, "Provider name to configure", argName) if providerName == "" { providerName = defaultProvider } pc := providerConfigByName(cfg, providerName) if pc.TimeoutSec <= 0 { pc.TimeoutSec = 90 } if strings.TrimSpace(pc.Auth) == "" { pc.Auth = "bearer" } if len(pc.Models) == 0 { pc.Models = []string{"gpt-5.4"} } pc.APIBase = promptLine(reader, "api_base", pc.APIBase) apiKey := promptLine(reader, "api_key (leave empty to keep current)", "") if apiKey != "" { pc.APIKey = apiKey } modelsRaw := promptLine(reader, "models (comma-separated)", strings.Join(pc.Models, ",")) if models := parseCSV(modelsRaw); len(models) > 0 { pc.Models = models } pc.Auth = promptLine(reader, "auth (bearer/oauth/hybrid/none)", pc.Auth) timeoutRaw := promptLine(reader, "timeout_sec", fmt.Sprintf("%d", pc.TimeoutSec)) pc.TimeoutSec = parseIntOrDefault(timeoutRaw, pc.TimeoutSec) pc.SupportsResponsesCompact = promptBool(reader, "supports_responses_compact", pc.SupportsResponsesCompact) if strings.EqualFold(strings.TrimSpace(pc.Auth), "oauth") || strings.EqualFold(strings.TrimSpace(pc.Auth), "hybrid") { pc.OAuth.Provider = promptLine(reader, "oauth.provider", firstNonEmptyString(pc.OAuth.Provider, "codex")) pc.OAuth.NetworkProxy = promptLine(reader, "oauth.network_proxy", pc.OAuth.NetworkProxy) pc.OAuth.CredentialFile = promptLine(reader, "oauth.credential_file", pc.OAuth.CredentialFile) pc.OAuth.CallbackPort = parseIntOrDefault(promptLine(reader, "oauth.callback_port", fmt.Sprintf("%d", defaultInt(pc.OAuth.CallbackPort, 1455))), defaultInt(pc.OAuth.CallbackPort, 1455)) pc.OAuth.CooldownSec = parseIntOrDefault(promptLine(reader, "oauth.cooldown_sec", fmt.Sprintf("%d", defaultInt(pc.OAuth.CooldownSec, 900))), defaultInt(pc.OAuth.CooldownSec, 900)) } setProviderConfigByName(cfg, providerName, pc) currentPrimaryProvider, currentPrimaryModel := config.ParseProviderModelRef(cfg.Agents.Defaults.Model.Primary) makePrimary := promptBool(reader, fmt.Sprintf("Set %s as agents.defaults.model.primary provider", providerName), providerName == currentPrimaryProvider) if makePrimary { targetModel := currentPrimaryModel if targetModel == "" && len(pc.Models) > 0 { targetModel = pc.Models[0] } cfg.Agents.Defaults.Model.Primary = providerName + "/" + targetModel } if err := config.SaveConfig(getConfigPath(), cfg); err != nil { fmt.Printf("Error saving config: %v\n", err) os.Exit(1) } fmt.Println("Provider configuration saved.") running, err := triggerGatewayReload() if err != nil { if running { fmt.Printf("Hot reload not applied: %v\n", err) return } fmt.Printf("Gateway not running, reload skipped: %v\n", err) return } fmt.Println("Gateway hot reload signal sent") } func providerListCmd() { cfg, err := loadConfig() if err != nil { fmt.Printf("Error loading config: %v\n", err) os.Exit(1) } primary := strings.TrimSpace(cfg.Agents.Defaults.Model.Primary) names := providerNames(cfg) for _, name := range names { pc, _ := config.ProviderConfigByName(cfg, name) models := strings.Join(pc.Models, ",") if models == "" { models = "-" } marker := " " if strings.HasPrefix(primary, name+"/") { marker = "*" } fmt.Printf("%s %s auth=%s models=%s api_base=%s\n", marker, name, strings.TrimSpace(pc.Auth), models, strings.TrimSpace(pc.APIBase)) } if primary != "" { fmt.Printf("\nPrimary: %s\n", primary) } } func providerUseCmd() { if len(os.Args) < 4 { fmt.Println("Usage: clawgo provider use ") return } ref := strings.TrimSpace(os.Args[3]) providerName, modelName := config.ParseProviderModelRef(ref) if providerName == "" || modelName == "" { fmt.Println("Error: expected provider/model") return } cfg, err := loadConfig() if err != nil { fmt.Printf("Error loading config: %v\n", err) os.Exit(1) } pc, ok := config.ProviderConfigByName(cfg, providerName) if !ok { fmt.Printf("Error: unknown provider %q\n", providerName) os.Exit(1) } foundModel := false for _, candidate := range pc.Models { if strings.TrimSpace(candidate) == modelName { foundModel = true break } } if !foundModel { fmt.Printf("Error: model %q not found in provider %q\n", modelName, providerName) os.Exit(1) } cfg.Agents.Defaults.Model.Primary = providerName + "/" + modelName if err := config.SaveConfig(getConfigPath(), cfg); err != nil { fmt.Printf("Error saving config: %v\n", err) os.Exit(1) } fmt.Printf("Primary model set to %s\n", cfg.Agents.Defaults.Model.Primary) if running, reloadErr := triggerGatewayReload(); reloadErr == nil { fmt.Println("Gateway hot reload signal sent") } else if running { fmt.Printf("Hot reload not applied: %v\n", reloadErr) } } func providerLoginCmd() { cfg, err := loadConfig() if err != nil { fmt.Printf("Error loading config: %v\n", err) os.Exit(1) } providerName, _ := config.ParseProviderModelRef(cfg.Agents.Defaults.Model.Primary) if providerName == "" { providerName = config.PrimaryProviderName(cfg) } manual := false noBrowser := false accountLabel := "" networkProxy := "" for i := 3; i < len(os.Args); i++ { arg := strings.TrimSpace(os.Args[i]) switch arg { case "--manual": manual = true case "--no-browser": noBrowser = true case "--label": if i+1 < len(os.Args) { i++ accountLabel = strings.TrimSpace(os.Args[i]) } case "--proxy": if i+1 < len(os.Args) { i++ networkProxy = strings.TrimSpace(os.Args[i]) } case "": default: if strings.HasPrefix(arg, "--label=") { accountLabel = strings.TrimSpace(strings.TrimPrefix(arg, "--label=")) continue } if strings.HasPrefix(arg, "--proxy=") { networkProxy = strings.TrimSpace(strings.TrimPrefix(arg, "--proxy=")) continue } if !strings.HasPrefix(arg, "-") { providerName = arg } } } pc := providerConfigByName(cfg, providerName) if !strings.EqualFold(strings.TrimSpace(pc.Auth), "oauth") && !strings.EqualFold(strings.TrimSpace(pc.Auth), "hybrid") { fmt.Printf("Provider %s is not configured with auth=oauth/hybrid\n", providerName) os.Exit(1) } if manual { noBrowser = true } if manual && strings.TrimSpace(pc.OAuth.RedirectURL) == "" && pc.OAuth.CallbackPort <= 0 { pc.OAuth.CallbackPort = 1455 } if strings.TrimSpace(networkProxy) == "" { networkProxy = strings.TrimSpace(pc.OAuth.NetworkProxy) } timeout := pc.TimeoutSec if timeout <= 0 { timeout = 90 } oauth, err := providers.NewOAuthLoginManager(pc, time.Duration(timeout)*time.Second) if err != nil { fmt.Printf("Error preparing oauth login: %v\n", err) os.Exit(1) } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() session, models, err := oauth.Login(ctx, pc.APIBase, providers.OAuthLoginOptions{ Manual: manual, NoBrowser: noBrowser, Reader: os.Stdin, AccountLabel: accountLabel, NetworkProxy: networkProxy, }) if err != nil { fmt.Printf("OAuth login failed: %v\n", err) os.Exit(1) } if len(models) > 0 { pc.Models = models } if session.CredentialFile != "" { pc.OAuth.CredentialFile = session.CredentialFile pc.OAuth.CredentialFiles = appendUniqueCSV(pc.OAuth.CredentialFiles, session.CredentialFile) } else if pc.OAuth.CredentialFile == "" { pc.OAuth.CredentialFile = oauth.CredentialFile() pc.OAuth.CredentialFiles = appendUniqueCSV(pc.OAuth.CredentialFiles, pc.OAuth.CredentialFile) } setProviderConfigByName(cfg, providerName, pc) if err := config.SaveConfig(getConfigPath(), cfg); err != nil { fmt.Printf("Error saving config: %v\n", err) os.Exit(1) } fmt.Printf("OAuth login succeeded for provider %s\n", providerName) if manual { fmt.Println("Mode: manual callback URL paste") } else if noBrowser { fmt.Println("Mode: local callback listener without auto-opening browser") } if session.Email != "" { fmt.Printf("Account: %s\n", session.Email) } if session.NetworkProxy != "" { fmt.Printf("Network proxy: %s\n", session.NetworkProxy) } fmt.Printf("Credential file: %s\n", firstNonEmptyString(session.CredentialFile, oauth.CredentialFile())) if len(pc.OAuth.CredentialFiles) > 1 { fmt.Printf("OAuth accounts: %d\n", len(pc.OAuth.CredentialFiles)) } if len(models) > 0 { fmt.Printf("Discovered models: %s\n", strings.Join(models, ", ")) } if running, reloadErr := triggerGatewayReload(); reloadErr == nil { fmt.Println("Gateway hot reload signal sent") } else if running { fmt.Printf("Hot reload not applied: %v\n", reloadErr) } } func providerNames(cfg *config.Config) []string { names := make([]string, 0, len(cfg.Models.Providers)) for k := range cfg.Models.Providers { k = strings.TrimSpace(k) if k == "" { continue } names = append(names, k) } sort.Strings(names) return names } func providerConfigByName(cfg *config.Config, name string) config.ProviderConfig { if pc, ok := config.ProviderConfigByName(cfg, name); ok { return pc } return config.ProviderConfig{ TimeoutSec: 90, Auth: "bearer", Models: []string{"gpt-5.4"}, } } func setProviderConfigByName(cfg *config.Config, name string, pc config.ProviderConfig) { name = strings.TrimSpace(name) if name == "" { return } if cfg.Models.Providers == nil { cfg.Models.Providers = map[string]config.ProviderConfig{} } cfg.Models.Providers[name] = pc } func promptLine(reader *bufio.Reader, label, defaultValue string) string { label = strings.TrimSpace(label) if defaultValue != "" { fmt.Printf("%s [%s]: ", label, defaultValue) } else { fmt.Printf("%s: ", label) } line, _ := reader.ReadString('\n') line = strings.TrimSpace(line) if line == "" { return defaultValue } return line } func promptBool(reader *bufio.Reader, label string, defaultValue bool) bool { def := "N" if defaultValue { def = "Y" } raw := promptLine(reader, label+" (y/n)", def) switch strings.ToLower(strings.TrimSpace(raw)) { case "y", "yes", "true", "1": return true case "n", "no", "false", "0": return false default: return defaultValue } } func parseCSV(raw string) []string { parts := strings.Split(raw, ",") out := make([]string, 0, len(parts)) seen := map[string]struct{}{} for _, p := range parts { v := strings.TrimSpace(p) if v == "" { continue } if _, ok := seen[v]; ok { continue } seen[v] = struct{}{} out = append(out, v) } return out } func defaultInt(value, fallback int) int { if value > 0 { return value } return fallback } func firstNonEmptyString(values ...string) string { for _, value := range values { if trimmed := strings.TrimSpace(value); trimmed != "" { return trimmed } } return "" } func appendUniqueCSV(values []string, value string) []string { trimmed := strings.TrimSpace(value) if trimmed == "" { return values } for _, item := range values { if strings.TrimSpace(item) == trimmed { return values } } return append(values, trimmed) } func parseIntOrDefault(raw string, def int) int { raw = strings.TrimSpace(raw) if raw == "" { return def } v, err := strconv.Atoi(raw) if err != nil || v <= 0 { return def } return v } func loadConfigAsMap(path string) (map[string]interface{}, error) { return configops.LoadConfigAsMap(path) } func normalizeConfigPath(path string) string { return configops.NormalizeConfigPath(path) } func parseConfigValue(raw string) interface{} { return configops.ParseConfigValue(raw) } func setMapValueByPath(root map[string]interface{}, path string, value interface{}) error { return configops.SetMapValueByPath(root, path, value) } func getMapValueByPath(root map[string]interface{}, path string) (interface{}, bool) { return configops.GetMapValueByPath(root, path) } func writeConfigAtomicWithBackup(configPath string, data []byte) (string, error) { return configops.WriteConfigAtomicWithBackup(configPath, data) } func rollbackConfigFromBackup(configPath, backupPath string) error { return configops.RollbackConfigFromBackup(configPath, backupPath) } func triggerGatewayReload() (bool, error) { return configops.TriggerGatewayReload(getConfigPath(), errGatewayNotRunning) }