From 4c787e6b257cea8f9972d155e141b8e4f014cc95 Mon Sep 17 00:00:00 2001 From: Ilie Barac Date: Tue, 5 May 2026 15:35:58 +0300 Subject: [PATCH 1/2] fix(install.sh): reattach stdin to /dev/tty when piped When installing Kimchi via 'curl ... | bash', stdin is a pipe from curl and is already at EOF. This caused the first-run API key prompt to immediately fail with 'error: read API key: EOF'. Redirect kimchi's stdin from /dev/tty when the script detects it is running in a non-TTY context, so the interactive prompt still works. If no TTY is available, skip auto-launch and print a helpful message. Co-Authored-By: Kimchi --- scripts/install.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/install.sh b/scripts/install.sh index f17cff2..13f4a56 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -74,4 +74,11 @@ echo -e "${GREEN}✓ Installed kimchi to ${INSTALL_PATH}${NC}" echo "" echo -e "${BLUE}Launching Kimchi...${NC}" echo "" -exec "$INSTALL_PATH" +if [ -t 0 ]; then + exec "$INSTALL_PATH" +elif [ -c /dev/tty ]; then + exec "$INSTALL_PATH" < /dev/tty +else + echo "" + echo -e "${BLUE}Run 'kimchi' to get started.${NC}" +fi From 5b38527d81b2c9034357d7a57212fb6696ed5074 Mon Sep 17 00:00:00 2001 From: Ilie Barac Date: Tue, 5 May 2026 16:34:15 +0300 Subject: [PATCH 2/2] fix(openclaw): merge config instead of replacing OpenClaw's config set defaults to replace mode, which causes the setup to fail when users already have model entries in agents.defaults.models or agents.defaults.model.fallbacks. Changes across all three code paths: 1. Batch JSON mode (writeOpenClawViaCLI): - Split batch to only cover non-conflicting fields (provider block + primary). - Use separate openclaw config set --merge calls for agents.defaults.model.fallbacks and agents.defaults.models. 2. Sequential CLI mode (writeOpenClawViaCLISequential): - Add --merge flag for agent.defaults.model.fallbacks. - Add --merge flag for agent.defaults.models. 3. Direct file-writing mode (writeOpenClawDirect): - Merge Kimchi model catalog into existing defaults["models"] instead of replacing it entirely. - Merge into existing defaults["model"] map instead of overwriting, preserving keys like temperature and max_tokens. - Append Kimchi fallbacks to existing fallbacks array. Co-Authored-By: Kimchi --- internal/tools/openclaw.go | 55 ++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/internal/tools/openclaw.go b/internal/tools/openclaw.go index 1c1aec4..6dcf099 100644 --- a/internal/tools/openclaw.go +++ b/internal/tools/openclaw.go @@ -129,7 +129,7 @@ func writeOpenClawViaCLISequential(providerBlock map[string]any, modelsCatalog m providerName + "/" + reg.Coding().Slug, providerName + "/" + reg.Sub().Slug, }) - cmd = exec.Command("openclaw", "config", "set", "agents.defaults.model.fallbacks", string(fallbacksJSON)) + cmd = exec.Command("openclaw", "config", "set", "--merge", "agents.defaults.model.fallbacks", string(fallbacksJSON)) if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("openclaw config set agents.defaults.model.fallbacks: %s: %w", string(out), err) } @@ -139,7 +139,7 @@ func writeOpenClawViaCLISequential(providerBlock map[string]any, modelsCatalog m if err != nil { return fmt.Errorf("marshal models catalog: %w", err) } - cmd = exec.Command("openclaw", "config", "set", "agents.defaults.models", string(modelsJSON)) + cmd = exec.Command("openclaw", "config", "set", "--merge", "agents.defaults.models", string(modelsJSON)) if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("openclaw config set agents.defaults.models: %s: %w", string(out), err) } @@ -177,15 +177,10 @@ func writeOpenClawViaCLI(apiKey string, reg *models.Registry) error { // Check OpenClaw version to determine which approach to use version := getOpenClawVersion() if isBatchJSONSupported(version) { - // Use --batch-json to set all config in a single CLI call (~3s vs ~12s sequential). + // Use --batch-json for the fields that don't conflict with existing user config. batchOps := []map[string]any{ {"path": "models.providers.kimchi", "value": providerBlock}, {"path": "agents.defaults.model.primary", "value": providerName + "/" + reg.Main().Slug}, - {"path": "agents.defaults.model.fallbacks", "value": []string{ - providerName + "/" + reg.Coding().Slug, - providerName + "/" + reg.Sub().Slug, - }}, - {"path": "agents.defaults.models", "value": modelsCatalog}, } batchJSON, err := json.Marshal(batchOps) @@ -197,6 +192,26 @@ func writeOpenClawViaCLI(apiKey string, reg *models.Registry) error { if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("openclaw config set: %s: %w", string(out), err) } + + // Merge fallbacks so existing entries are preserved. + fallbacksJSON, _ := json.Marshal([]string{ + providerName + "/" + reg.Coding().Slug, + providerName + "/" + reg.Sub().Slug, + }) + cmd = exec.Command("openclaw", "config", "set", "--merge", "agents.defaults.model.fallbacks", string(fallbacksJSON)) + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("openclaw config set --merge agents.defaults.model.fallbacks: %s: %w", string(out), err) + } + + // Merge models catalog so existing entries are preserved. + modelsJSON, err := json.Marshal(modelsCatalog) + if err != nil { + return fmt.Errorf("marshal models catalog: %w", err) + } + cmd = exec.Command("openclaw", "config", "set", "--merge", "agents.defaults.models", string(modelsJSON)) + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("openclaw config set --merge agents.defaults.models: %s: %w", string(out), err) + } } else { // Fall back to sequential calls for older versions if err := writeOpenClawViaCLISequential(providerBlock, modelsCatalog, reg); err != nil { @@ -310,13 +325,25 @@ func writeOpenClawDirect(scope config.ConfigScope, apiKey string, reg *models.Re if defaults == nil { defaults = make(map[string]any) } - defaults["model"] = map[string]any{ - "primary": providerName + "/" + reg.Main().Slug, - "fallbacks": []any{providerName + "/" + reg.Coding().Slug, providerName + "/" + reg.Sub().Slug}, + // Merge into model config to preserve existing keys like temperature, max_tokens. + modelMap, _ := defaults["model"].(map[string]any) + if modelMap == nil { + modelMap = make(map[string]any) + } + modelMap["primary"] = providerName + "/" + reg.Main().Slug + // Merge fallbacks so existing entries are preserved. + existingFallbacks, _ := modelMap["fallbacks"].([]any) + modelMap["fallbacks"] = append(existingFallbacks, + providerName+"/"+reg.Coding().Slug, + providerName+"/"+reg.Sub().Slug, + ) + defaults["model"] = modelMap + + // Merge into the allowed models catalog, preserving existing entries. + modelsCatalog, _ := defaults["models"].(map[string]any) + if modelsCatalog == nil { + modelsCatalog = make(map[string]any) } - - // Add models to the allowed models catalog. - modelsCatalog := make(map[string]any) for _, m := range reg.All() { modelsCatalog[providerName+"/"+m.Slug] = map[string]any{"alias": m.DisplayName} }