From 9c6c04c800a80f1c3b7fffa3fb8ccd9152c222ac Mon Sep 17 00:00:00 2001 From: Daiyimo Date: Thu, 26 Feb 2026 16:01:22 +0800 Subject: [PATCH] feat: add uninstall command; fix utf-8 encoding and minor cleanups --- README.md | 18 +++++++ agent_reach/cli.py | 120 +++++++++++++++++++++++++++++++++++++----- agent_reach/config.py | 4 +- agent_reach/doctor.py | 5 +- 4 files changed, 130 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 85c115e..bf67a29 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,24 @@ Agent Reach 在设计上重视安全: | 安全模式 | `agent-reach install --env=auto --safe` | 生产服务器、多人共用机器 | | 仅预览 | `agent-reach install --env=auto --dry-run` | 先看看会做什么 | +### 🗑️ 卸载 + +```bash +agent-reach uninstall +``` + +会清除:`~/.agent-reach/`(含所有 token/cookie)、各 Agent 的 skill 文件、mcporter 中的 MCP 配置。 + +```bash +# 只预览,不实际删除 +agent-reach uninstall --dry-run + +# 只删 skill 文件,保留 token 配置(重装时用) +agent-reach uninstall --keep-config +``` + +卸载 Python 包本身:`pip uninstall agent-reach` + --- ## 贡献 diff --git a/agent_reach/cli.py b/agent_reach/cli.py index f5f3e7e..c2c2a45 100644 --- a/agent_reach/cli.py +++ b/agent_reach/cli.py @@ -71,6 +71,13 @@ def main(): # ── doctor ── sub.add_parser("doctor", help="Check platform availability") + # ── uninstall ── + p_uninstall = sub.add_parser("uninstall", help="Remove all Agent Reach config, tokens, and skill files") + p_uninstall.add_argument("--dry-run", action="store_true", + help="Show what would be removed without making any changes") + p_uninstall.add_argument("--keep-config", action="store_true", + help="Remove skill files only, keep ~/.agent-reach/ config and tokens") + # ── check-update ── sub.add_parser("check-update", help="Check for new versions and changes") @@ -105,6 +112,8 @@ def main(): _cmd_install(args) elif args.command == "configure": _cmd_configure(args) + elif args.command == "uninstall": + _cmd_uninstall(args) # ── Command handlers ──────────────────────────────── @@ -680,6 +689,104 @@ def _cmd_configure(args): print(f"✅ Groq key configured!") +def _cmd_uninstall(args): + """Remove all Agent Reach config, tokens, and skill files.""" + import shutil + import subprocess + + dry_run = args.dry_run + keep_config = args.keep_config + + print() + print("Agent Reach Uninstaller") + print("=" * 40) + + if dry_run: + print("DRY RUN — showing what would be removed (no changes)") + print() + + removed_any = False + + # ── 1. Config directory (~/.agent-reach/) ── + config_dir = os.path.expanduser("~/.agent-reach") + if not keep_config: + if os.path.isdir(config_dir): + if dry_run: + print(f"[dry-run] Would remove config directory: {config_dir}") + print(" (contains config.yaml with all tokens/cookies/API keys)") + else: + try: + shutil.rmtree(config_dir) + print(f" Removed config directory: {config_dir}") + removed_any = True + except Exception as e: + print(f" Could not remove {config_dir}: {e}") + else: + print(f" Config directory not found (already clean): {config_dir}") + else: + print(f" Skipping config directory (--keep-config): {config_dir}") + + # ── 2. Skill files ── + skill_dirs = [ + ("~/.openclaw/skills/agent-reach", "OpenClaw"), + ("~/.claude/skills/agent-reach", "Claude Code"), + ("~/.agents/skills/agent-reach", "Agent"), + ] + + for skill_path_template, platform_name in skill_dirs: + skill_path = os.path.expanduser(skill_path_template) + if os.path.isdir(skill_path): + if dry_run: + print(f"[dry-run] Would remove {platform_name} skill: {skill_path}") + else: + try: + shutil.rmtree(skill_path) + print(f" Removed {platform_name} skill: {skill_path}") + removed_any = True + except Exception as e: + print(f" Could not remove {skill_path}: {e}") + + # ── 3. mcporter MCP entries ── + if shutil.which("mcporter"): + for mcp_name in ("exa", "xiaohongshu"): + try: + r = subprocess.run( + ["mcporter", "list"], capture_output=True, text=True, timeout=10 + ) + if mcp_name in r.stdout: + if dry_run: + print(f"[dry-run] Would remove mcporter entry: {mcp_name}") + else: + subprocess.run( + ["mcporter", "config", "remove", mcp_name], + capture_output=True, text=True, timeout=10, + ) + print(f" Removed mcporter entry: {mcp_name}") + removed_any = True + except Exception: + pass + + # ── 4. Summary and optional steps ── + print() + if dry_run: + print("Dry run complete. No changes were made.") + print("Run without --dry-run to actually remove the above.") + else: + if removed_any: + print("Agent Reach data removed.") + else: + print("Nothing to remove — already clean.") + + print() + print("Optional: remove the Agent Reach Python package itself:") + print(" pip uninstall agent-reach") + print() + print("Optional: remove tools installed by Agent Reach:") + print(" npm uninstall -g mcporter") + print(" npm uninstall -g @steipete/bird") + print(" npm uninstall -g undici") + + def _cmd_doctor(): from agent_reach.config import Config from agent_reach.doctor import check_all, format_report @@ -688,19 +795,6 @@ def _cmd_doctor(): print(format_report(results)) -def _parse_cookie_header(cookie_str: str) -> dict: - """Parse Cookie-Editor 'Header String' format into a dict.""" - cookies = {} - for part in cookie_str.split(";"): - part = part.strip() - if "=" in part: - k, v = part.split("=", 1) - cookies[k.strip()] = v.strip() - return cookies - - - - def _cmd_setup(): from agent_reach.config import Config diff --git a/agent_reach/config.py b/agent_reach/config.py index 59b7c74..49564b3 100644 --- a/agent_reach/config.py +++ b/agent_reach/config.py @@ -41,7 +41,7 @@ class Config: def load(self): """Load config from YAML file.""" if self.config_path.exists(): - with open(self.config_path, "r") as f: + with open(self.config_path, "r", encoding="utf-8") as f: self.data = yaml.safe_load(f) or {} else: self.data = {} @@ -49,7 +49,7 @@ class Config: def save(self): """Save config to YAML file.""" self._ensure_dir() - with open(self.config_path, "w") as f: + with open(self.config_path, "w", encoding="utf-8") as f: yaml.dump(self.data, f, default_flow_style=False, allow_unicode=True) # Restrict permissions — config may contain credentials try: diff --git a/agent_reach/doctor.py b/agent_reach/doctor.py index 3b71c2a..eaac322 100644 --- a/agent_reach/doctor.py +++ b/agent_reach/doctor.py @@ -74,11 +74,12 @@ def format_report(results: Dict[str, dict]) -> str: if ok_count < total: lines.append("运行 `agent-reach setup` 解锁更多渠道") - # Security check: config file permissions + # Security check: config file permissions (Unix only) import os import stat + import sys config_path = Config.CONFIG_DIR / "config.yaml" - if config_path.exists(): + if config_path.exists() and sys.platform != "win32": try: mode = config_path.stat().st_mode if mode & (stat.S_IRGRP | stat.S_IROTH):