diff --git a/openclaw.plugin.json b/openclaw.plugin.json index 64b3a73..d5976ba 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -2,7 +2,7 @@ "id": "memory-continuity", "name": "Memory Continuity", "description": "Preserves working state across /new, reset, compaction, and gateway restarts via a simple markdown checkpoint file.", - "version": "2.5.0", + "version": "2.6.0", "source": "https://github.com/dtzp555-max/memory-continuity", "configSchema": { "type": "object", diff --git a/scripts/post-install.sh b/scripts/post-install.sh index a592b07..e19ede4 100755 --- a/scripts/post-install.sh +++ b/scripts/post-install.sh @@ -276,6 +276,10 @@ else esac fi +echo "" +echo "๐Ÿง™ ๅฏๅŠจ้…็ฝฎๅ‘ๅฏผ..." +python3 "$(dirname "$0")/setup_wizard.py" || true + echo "" echo "=== Installation complete ===" echo "" diff --git a/scripts/setup_wizard.py b/scripts/setup_wizard.py new file mode 100644 index 0000000..a5335ba --- /dev/null +++ b/scripts/setup_wizard.py @@ -0,0 +1,600 @@ +#!/usr/bin/env python3 +# setup_wizard.py โ€” Interactive post-install wizard for memory-continuity skill +# +# Guides the user through: +# Step 1/4: Select default model +# Step 2/4: Configure per-agent models +# Step 3/4: Review changes + OCP check +# Step 4/4: Restart gateway +# +# Usage: +# python3 scripts/setup_wizard.py +# +# Requires Python 3.6+ + +import sys +import os +import json +import subprocess +import termios +import tty +import signal + +# --------------------------------------------------------------------------- +# Terminal colors (ANSI) +# --------------------------------------------------------------------------- +GREEN = "\033[32m" +YELLOW = "\033[33m" +CYAN = "\033[36m" +RED = "\033[31m" +BOLD = "\033[1m" +DIM = "\033[2m" +RESET = "\033[0m" + +def green(s): return f"{GREEN}{s}{RESET}" +def yellow(s): return f"{YELLOW}{s}{RESET}" +def cyan(s): return f"{CYAN}{s}{RESET}" +def red(s): return f"{RED}{s}{RESET}" +def bold(s): return f"{BOLD}{s}{RESET}" +def dim(s): return f"{DIM}{s}{RESET}" + +# --------------------------------------------------------------------------- +# Key reading (raw terminal, no curses dependency) +# --------------------------------------------------------------------------- +UP_ARROW = "UP" +DOWN_ARROW = "DOWN" +ENTER_KEY = "ENTER" +CTRL_C = "CTRL_C" + +def _read_key(): + """Read a single keypress from stdin. Returns a key name string.""" + fd = sys.stdin.fileno() + old = termios.tcgetattr(fd) + try: + tty.setraw(fd) + ch = sys.stdin.read(1) + if ch == "\x03": + return CTRL_C + if ch in ("\r", "\n"): + return ENTER_KEY + if ch == "\x1b": + # Escape sequence โ€” read more + ch2 = sys.stdin.read(1) + if ch2 == "[": + ch3 = sys.stdin.read(1) + if ch3 == "A": + return UP_ARROW + if ch3 == "B": + return DOWN_ARROW + return ch + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old) + +# --------------------------------------------------------------------------- +# Clean exit on Ctrl+C anywhere +# --------------------------------------------------------------------------- +def _handle_sigint(sig, frame): + print(f"\n\n{yellow('ๅทฒๅ–ๆถˆ๏ผŒๆœชๅšไปปไฝ•ไฟฎๆ”น')}") + sys.exit(0) + +signal.signal(signal.SIGINT, _handle_sigint) + +# --------------------------------------------------------------------------- +# Arrow-key menu selector +# --------------------------------------------------------------------------- +def arrow_select(prompt, options, default_index=0): + """ + Display a list of options with arrow-key navigation. + Returns the selected index, or raises SystemExit on Ctrl+C. + """ + selected = default_index + n = len(options) + + # Hide cursor + sys.stdout.write("\033[?25l") + sys.stdout.flush() + + def _render(): + # Move cursor up to re-draw + sys.stdout.write(f"\033[{n}A\r") + for i, opt in enumerate(options): + if i == selected: + prefix = f" {CYAN}โ–ถ {BOLD}" + suffix = RESET + else: + prefix = " " + suffix = "" + sys.stdout.write(f"\r{prefix}{opt}{suffix}\n") + sys.stdout.flush() + + # Initial render + if prompt: + print(prompt) + for i, opt in enumerate(options): + print(f" {opt}") + + try: + while True: + _render() + key = _read_key() + if key == CTRL_C: + sys.stdout.write("\033[?25h") + sys.stdout.flush() + print(f"\n\n{yellow('ๅทฒๅ–ๆถˆ๏ผŒๆœชๅšไปปไฝ•ไฟฎๆ”น')}") + sys.exit(0) + elif key == UP_ARROW: + selected = (selected - 1) % n + elif key == DOWN_ARROW: + selected = (selected + 1) % n + elif key == ENTER_KEY: + break + finally: + sys.stdout.write("\033[?25h") + sys.stdout.flush() + + return selected + +# --------------------------------------------------------------------------- +# Yes/No prompt +# --------------------------------------------------------------------------- +def yes_no(prompt, default_yes=True): + """Ask a yes/no question. Returns True for yes.""" + hint = "[Y/n]" if default_yes else "[y/N]" + while True: + try: + ans = input(f"{prompt} {hint}: ").strip().lower() + except (EOFError, KeyboardInterrupt): + print(f"\n\n{yellow('ๅทฒๅ–ๆถˆ๏ผŒๆœชๅšไปปไฝ•ไฟฎๆ”น')}") + sys.exit(0) + if ans == "": + return default_yes + if ans in ("y", "yes"): + return True + if ans in ("n", "no"): + return False + print(yellow(" ่ฏท่พ“ๅ…ฅ Y ๆˆ– N")) + +# --------------------------------------------------------------------------- +# Progress header +# --------------------------------------------------------------------------- +def print_step(n, total, title): + print() + print(bold(cyan(f"ๆญฅ้ชค {n}/{total}: {title}"))) + print(cyan("โ”€" * 56)) + +# --------------------------------------------------------------------------- +# Find openclaw.json +# --------------------------------------------------------------------------- +OPENCLAW_JSON_PATHS = [ + os.path.expanduser("~/.openclaw/openclaw.json"), + "/etc/openclaw/openclaw.json", +] + +def find_openclaw_json(): + for p in OPENCLAW_JSON_PATHS: + if os.path.isfile(p): + return p + return None + +def load_config(path): + with open(path, encoding="utf-8") as f: + return json.load(f) + +def save_config(path, data): + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + f.write("\n") + +# --------------------------------------------------------------------------- +# Model list +# --------------------------------------------------------------------------- +MODELS = [ + ("claude-sonnet-4-6", "ๆŽจ่๏ผš็ปผๅˆ่ƒฝๅŠ›ๆœ€ๅผบ๏ผŒ้€Ÿๅบฆๅ‡่กก"), + ("claude-opus-4-6", "่ƒฝๅŠ›ๆœ€ๅผบ๏ผŒ้€‚ๅˆๅคๆ‚ไปปๅŠก๏ผŒ่พƒๆ…ข"), + ("claude-haiku-4-5", "้€Ÿๅบฆๆœ€ๅฟซ๏ผŒ้€‚ๅˆ็ฎ€ๅ•ไปปๅŠก"), +] +KEEP_CURRENT = "๏ผˆไฟๆŒๅฝ“ๅ‰่ฎพ็ฝฎ๏ผ‰" +KEEP_UNCHANGED = "๏ผˆไฟๆŒไธๅ˜๏ผ‰" + +def model_display_options(include_keep_current=False, include_keep_unchanged=False): + lines = [] + for m, desc in MODELS: + lines.append(f"{m:<25} {dim(desc)}") + if include_keep_current: + lines.append(KEEP_CURRENT) + if include_keep_unchanged: + lines.append(KEEP_UNCHANGED) + return lines + +def model_values(include_keep_current=False, include_keep_unchanged=False): + vals = [m for m, _ in MODELS] + if include_keep_current: + vals.append(None) # None = keep current + if include_keep_unchanged: + vals.append(None) + return vals + +# --------------------------------------------------------------------------- +# Step 1: Default model selection +# --------------------------------------------------------------------------- +def step1_default_model(config): + print_step(1, 4, "้€‰ๆ‹ฉ้ป˜่ฎค Model") + print() + print("โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”") + print("โ”‚ ้€‰ๆ‹ฉ OpenClaw ้ป˜่ฎค Model โ”‚") + print("โ”‚ โ”‚") + print("โ”‚ claude-sonnet-4-6 โ† ๆŽจ่๏ผš็ปผๅˆ่ƒฝๅŠ›ๆœ€ๅผบ๏ผŒ้€Ÿๅบฆๅ‡่กก โ”‚") + print("โ”‚ claude-opus-4-6 ่ƒฝๅŠ›ๆœ€ๅผบ๏ผŒ้€‚ๅˆๅคๆ‚ไปปๅŠก๏ผŒ่พƒๆ…ข โ”‚") + print("โ”‚ claude-haiku-4-5 ้€Ÿๅบฆๆœ€ๅฟซ๏ผŒ้€‚ๅˆ็ฎ€ๅ•ไปปๅŠก โ”‚") + print("โ”‚ ๏ผˆไฟๆŒๅฝ“ๅ‰่ฎพ็ฝฎ๏ผ‰ โ”‚") + print("โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜") + print() + + current = (config + .get("agents", {}) + .get("defaults", {}) + .get("model", {}) + .get("primary", None)) + + if current: + print(f" {dim('ๅฝ“ๅ‰้ป˜่ฎค Model:')} {cyan(current)}") + else: + print(f" {dim('ๅฝ“ๅ‰้ป˜่ฎค Model:')} {yellow('(ๆœช่ฎพ็ฝฎ)')}") + print() + + # Find default selection index + default_idx = len(MODELS) # default: keep current + if current: + for i, (m, _) in enumerate(MODELS): + if m == current: + default_idx = i + break + + options = model_display_options(include_keep_current=True) + print(f" {bold('ไฝฟ็”จ โ†‘โ†“ ๆ–นๅ‘้”ฎ้€‰ๆ‹ฉ๏ผŒEnter ็กฎ่ฎค:')}") + print() + idx = arrow_select(None, options, default_index=default_idx) + + if idx >= len(MODELS): + # Keep current + chosen_model = current # may be None + print(f"\n {green('โœ“')} ไฟๆŒๅฝ“ๅ‰่ฎพ็ฝฎ: {cyan(current) if current else yellow('(ๆœช่ฎพ็ฝฎ)')}") + return chosen_model, False # (value, changed) + else: + chosen_model = MODELS[idx][0] + changed = (chosen_model != current) + print(f"\n {green('โœ“')} ้€‰ๆ‹ฉ: {cyan(chosen_model)}") + return chosen_model, changed + +# --------------------------------------------------------------------------- +# Step 2: Agent model configuration +# --------------------------------------------------------------------------- +def step2_agent_config(config, default_model): + print_step(2, 4, "Agent Model ้…็ฝฎ") + print() + + agent_options = [ + "ไธไฟฎๆ”นไปปไฝ• Agent๏ผˆ้ป˜่ฎค๏ผ‰", + "ๅ…จ้ƒจๆŒ‰้ป˜่ฎค Model ไฟฎๆ”น", + "้€ไธ€้…็ฝฎๆฏไธช Agent", + ] + print(f" {bold('้€‰ๆ‹ฉ้…็ฝฎๆ–นๅผ:')}") + print() + mode_idx = arrow_select(None, agent_options, default_index=0) + print() + + agents = config.get("agents", {}).get("list", []) + agent_changes = {} # agent_id -> {"primary": ..., "fallback": ...} + + if mode_idx == 0: + print(f" {green('โœ“')} ไธไฟฎๆ”นไปปไฝ• Agent") + return agent_changes + + if mode_idx == 1: + # Apply default model to all agents + if not default_model: + print(f" {yellow('โš ๏ธ ๆœช่ฎพ็ฝฎ้ป˜่ฎค Model๏ผŒ่ทณ่ฟ‡ๆ‰น้‡ไฟฎๆ”น')}") + return agent_changes + for agent in agents: + aid = agent.get("id", "") + if not aid: + continue + current_primary = (agent.get("model", {}) or {}).get("primary", None) + if current_primary != default_model: + agent_changes[aid] = {"primary": default_model, "fallback": None, "keep_fallback": True} + print(f" {green('โœ“')} ๅทฒๅฐ† {len(agent_changes)} ไธช Agent ็š„ primary ่ฎพไธบ {cyan(default_model)}") + return agent_changes + + # mode_idx == 2: configure one by one + if not agents: + print(f" {yellow('openclaw.json ไธญๆฒกๆœ‰ๆ‰พๅˆฐ Agent ๅˆ—่กจ')}") + return agent_changes + + for agent in agents: + aid = agent.get("id", "") + aname = agent.get("name", aid) + if not aid: + continue + + cur_model_block = agent.get("model", {}) or {} + cur_primary = cur_model_block.get("primary", None) + cur_fallback = cur_model_block.get("fallback", None) + + print() + print(f" {bold(cyan('Agent:'))} {aname} ({dim(aid)})") + print(f" current primary: {cyan(cur_primary) if cur_primary else yellow('(ๆœช่ฎพ็ฝฎ)')}") + print(f" current fallback: {cyan(cur_fallback) if cur_fallback else yellow('(ๆœช่ฎพ็ฝฎ)')}") + print() + + # Pick primary + print(f" {bold('้€‰ๆ‹ฉๆ–ฐ primary Model (โ†‘โ†“ Enter):')}") + print() + primary_options = model_display_options(include_keep_unchanged=True) + primary_vals = model_values(include_keep_unchanged=True) + # Default: keep unchanged + p_default = len(MODELS) + if cur_primary: + for i, (m, _) in enumerate(MODELS): + if m == cur_primary: + p_default = i + break + + p_idx = arrow_select(None, primary_options, default_index=p_default) + new_primary = primary_vals[p_idx] if p_idx < len(MODELS) else cur_primary + print(f"\n {green('โœ“')} primary: {cyan(new_primary) if new_primary else yellow('(ไฟๆŒไธๅ˜)')}") + + # Pick fallback + print() + print(f" {bold('้€‰ๆ‹ฉๆ–ฐ fallback Model (โ†‘โ†“ Enter):')}") + print() + fallback_options = model_display_options() + ["๏ผˆ่ทณ่ฟ‡ / ไธ่ฎพ็ฝฎ fallback๏ผ‰"] + f_default = len(MODELS) # skip + if cur_fallback: + for i, (m, _) in enumerate(MODELS): + if m == cur_fallback: + f_default = i + break + + f_idx = arrow_select(None, fallback_options, default_index=f_default) + if f_idx < len(MODELS): + new_fallback = MODELS[f_idx][0] + else: + new_fallback = cur_fallback # keep existing + + print(f"\n {green('โœ“')} fallback: {cyan(new_fallback) if new_fallback else yellow('(ไฟๆŒไธๅ˜)')}") + + # Record changes only if something actually differs + primary_changed = (new_primary is not None and new_primary != cur_primary) + fallback_changed = (new_fallback is not None and new_fallback != cur_fallback) + if primary_changed or fallback_changed: + agent_changes[aid] = { + "primary": new_primary if primary_changed else cur_primary, + "fallback": new_fallback if fallback_changed else cur_fallback, + } + + # Ask whether to continue to next agent + print() + try: + cont = input(f" ่ฟ˜้œ€่ฆไฟฎๆ”นๅ…ถไป– Agent ๅ—๏ผŸ[Y/n]: ").strip().lower() + except (EOFError, KeyboardInterrupt): + print(f"\n\n{yellow('ๅทฒๅ–ๆถˆ๏ผŒๆœชๅšไปปไฝ•ไฟฎๆ”น')}") + sys.exit(0) + if cont in ("n", "no"): + break + + return agent_changes + +# --------------------------------------------------------------------------- +# Step 3: Summary + OCP check + confirm +# --------------------------------------------------------------------------- +OCP_MODELS_NEEDING_PROXY = ("claude-local", "claude-sonnet", "claude-opus", "claude-haiku") + +def _needs_ocp(model_name): + if not model_name: + return False + return any(kw in model_name for kw in OCP_MODELS_NEEDING_PROXY) + +def _ocp_running(): + try: + result = subprocess.run( + ["curl", "-s", "--max-time", "3", "http://localhost:3456/health"], + capture_output=True, text=True + ) + return result.returncode == 0 and result.stdout.strip() != "" + except Exception: + return False + +def step3_summary_confirm(config, new_default_model, default_changed, agent_changes): + print_step(3, 4, "็กฎ่ฎคไฟฎๆ”น") + print() + + # Build summary lines + current_default = (config + .get("agents", {}) + .get("defaults", {}) + .get("model", {}) + .get("primary", None)) + + has_any_change = default_changed or bool(agent_changes) + + if not has_any_change: + return False # caller will handle "no changes" message + + print(f" {bold('ๅณๅฐ†ๅบ”็”จไปฅไธ‹ไฟฎๆ”น:')}") + print() + + if default_changed: + old_str = cyan(current_default) if current_default else yellow("(ๆœช่ฎพ็ฝฎ)") + new_str = cyan(new_default_model) + print(f" {bold('[defaults]')} {old_str} โ†’ {new_str}") + + agents = config.get("agents", {}).get("list", []) + agent_map = {a.get("id", ""): a for a in agents if a.get("id")} + + for aid, change in agent_changes.items(): + agent = agent_map.get(aid, {}) + cur_primary = (agent.get("model", {}) or {}).get("primary", None) + new_primary = change.get("primary", cur_primary) + old_str = cyan(cur_primary) if cur_primary else yellow("(ๆœช่ฎพ็ฝฎ)") + new_str = cyan(new_primary) if new_primary else yellow("(ๆœช่ฎพ็ฝฎ)") + aname = agent.get("name", aid) + if cur_primary == new_primary: + print(f" {bold(aname + ':')}{'':4}ไธๅ˜") + else: + print(f" {bold(aname + ':')}{'':4}{old_str} โ†’ {new_str}") + + print() + + # OCP check + all_new_models = [] + if default_changed and new_default_model: + all_new_models.append(new_default_model) + for aid, change in agent_changes.items(): + if change.get("primary"): + all_new_models.append(change["primary"]) + if change.get("fallback"): + all_new_models.append(change["fallback"]) + + needs_ocp = any(_needs_ocp(m) for m in all_new_models) + if needs_ocp and not _ocp_running(): + print(f" {yellow('โš ๏ธ ๆฃ€ๆต‹ๅˆฐไฝ ้€‰ๆ‹ฉ็š„ Model ้œ€่ฆ openclaw-claude-proxy (ocp)')}") + print(f" {yellow(' ไฝ† ocp ็›ฎๅ‰ๆœช่ฟ่กŒใ€‚ๅปบ่ฎฎๅ…ˆๅฎ‰่ฃ…ๅนถๅฏๅŠจ ocpใ€‚')}") + print() + try: + ans = input(f" ็ปง็ปญ [c] / ้€€ๅ‡บๆŸฅ็œ‹ ocp ๅฎ‰่ฃ…ๆ–‡ๆกฃ [q]: ").strip().lower() + except (EOFError, KeyboardInterrupt): + print(f"\n\n{yellow('ๅทฒๅ–ๆถˆ๏ผŒๆœชๅšไปปไฝ•ไฟฎๆ”น')}") + sys.exit(0) + if ans in ("q", "quit", "exit"): + print() + print(f" {cyan('่ฏทๅ‚่€ƒ ocp ๅฎ‰่ฃ…ๆ–‡ๆกฃ: https://github.com/openclaw/openclaw-claude-proxy')}") + print(f" {yellow('ๅทฒ้€€ๅ‡บ๏ผŒๆœชๅšไปปไฝ•ไฟฎๆ”น')}") + sys.exit(0) + + confirmed = yes_no(" ็กฎ่ฎคๅบ”็”จ๏ผŸ", default_yes=True) + return confirmed + +# --------------------------------------------------------------------------- +# Apply changes to openclaw.json +# --------------------------------------------------------------------------- +def apply_changes(config, new_default_model, default_changed, agent_changes): + if default_changed and new_default_model is not None: + agents_block = config.setdefault("agents", {}) + defaults_block = agents_block.setdefault("defaults", {}) + model_block = defaults_block.setdefault("model", {}) + model_block["primary"] = new_default_model + + if agent_changes: + agents_list = config.get("agents", {}).get("list", []) + for agent in agents_list: + aid = agent.get("id", "") + if aid in agent_changes: + change = agent_changes[aid] + if "model" not in agent or agent["model"] is None: + agent["model"] = {} + if change.get("primary") is not None: + agent["model"]["primary"] = change["primary"] + if change.get("fallback") is not None: + agent["model"]["fallback"] = change["fallback"] + +# --------------------------------------------------------------------------- +# Step 4: Restart gateway +# --------------------------------------------------------------------------- +def step4_restart_gateway(): + print_step(4, 4, "้‡ๅฏ Gateway") + print() + restart = yes_no(" ้…็ฝฎๅทฒๆ›ดๆ–ฐใ€‚ๆ˜ฏๅฆ็ซ‹ๅณ้‡ๅฏ Gateway ไปฅไฝฟ้…็ฝฎ็”Ÿๆ•ˆ๏ผŸ", default_yes=True) + if not restart: + print(f" {dim('่ทณ่ฟ‡้‡ๅฏใ€‚่ฏทๆ‰‹ๅŠจ่ฟ่กŒ: openclaw gateway restart')}") + return + + print(f" {cyan('ๆญฃๅœจๆ‰ง่กŒ: openclaw gateway restart ...')}") + try: + result = subprocess.run( + ["openclaw", "gateway", "restart"], + capture_output=True, text=True + ) + if result.returncode == 0: + print(f" {green('โœ“ Gateway ้‡ๅฏๆˆๅŠŸ')}") + if result.stdout.strip(): + print(f" {dim(result.stdout.strip())}") + else: + print(f" {yellow('โš ๏ธ Gateway ้‡ๅฏ่ฟ”ๅ›ž้ž้›ถ้€€ๅ‡บ็ ')}") + if result.stderr.strip(): + print(f" {red(result.stderr.strip())}") + except FileNotFoundError: + print(f" {yellow('โš ๏ธ ๆœชๆ‰พๅˆฐ openclaw ๅ‘ฝไปคใ€‚่ฏทๆ‰‹ๅŠจ้‡ๅฏ Gatewayใ€‚')}") + except Exception as e: + print(f" {red(f'้‡ๅฏๅคฑ่ดฅ: {e}')}") + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- +def main(): + print() + print(bold(cyan("โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—"))) + print(bold(cyan("โ•‘ OpenClaw Memory-Continuity ้…็ฝฎๅ‘ๅฏผ โ•‘"))) + print(bold(cyan("โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•"))) + print() + + # Check if stdin is a tty โ€” if not, skip interactive wizard + if not sys.stdin.isatty(): + print(f" {yellow('้žไบคไบ’ๅผ็ปˆ็ซฏ๏ผŒ่ทณ่ฟ‡้…็ฝฎๅ‘ๅฏผ')}") + sys.exit(0) + + # Find config file + config_path = find_openclaw_json() + if not config_path: + print(f" {red('้”™่ฏฏ: ๆœชๆ‰พๅˆฐ openclaw.json')}") + print(f" {dim('ๅทฒๆœ็ดขไปฅไธ‹่ทฏๅพ„:')}") + for p in OPENCLAW_JSON_PATHS: + print(f" {dim(p)}") + print(f" {yellow('่ฏทๅ…ˆๅˆๅง‹ๅŒ– OpenClaw ้…็ฝฎ๏ผŒๅ†่ฟ่กŒๆญคๅ‘ๅฏผ')}") + sys.exit(1) + + print(f" {dim('้…็ฝฎๆ–‡ไปถ:')} {cyan(config_path)}") + + config = load_config(config_path) + + # โ”€โ”€ Step 1 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + new_default_model, default_changed = step1_default_model(config) + + # โ”€โ”€ Step 2 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + agent_changes = step2_agent_config(config, new_default_model) + + # โ”€โ”€ Check for any changes โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + has_any_change = default_changed or bool(agent_changes) + if not has_any_change: + print() + print(f" {green('ๆ— ้œ€ไฟฎๆ”น๏ผŒ้…็ฝฎไฟๆŒไธๅ˜ โœ…')}") + print() + sys.exit(0) + + # โ”€โ”€ Step 3 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + confirmed = step3_summary_confirm( + config, new_default_model, default_changed, agent_changes + ) + + if not confirmed: + print() + print(f" {yellow('ๅทฒๅ–ๆถˆ๏ผŒๆœชๅšไปปไฝ•ไฟฎๆ”น')}") + print() + sys.exit(0) + + # Write changes + apply_changes(config, new_default_model, default_changed, agent_changes) + save_config(config_path, config) + print(f"\n {green('โœ“ ้…็ฝฎๅทฒๅ†™ๅ…ฅ')} {cyan(config_path)}") + + # โ”€โ”€ Step 4 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + step4_restart_gateway() + + print() + print(bold(green("โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—"))) + print(bold(green("โ•‘ ้…็ฝฎๅ‘ๅฏผๅฎŒๆˆ โœ… โ•‘"))) + print(bold(green("โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•"))) + print() + + +if __name__ == "__main__": + main()