Merge pull request #24 from Daiyimo/main
feat: add uninstall command; fix utf-8 encoding and minor cleanups
This commit is contained in:
commit
dc4d7cd7a5
4 changed files with 130 additions and 17 deletions
18
README.md
18
README.md
|
|
@ -207,6 +207,24 @@ Agent Reach 在设计上重视安全:
|
||||||
| 安全模式 | `agent-reach install --env=auto --safe` | 生产服务器、多人共用机器 |
|
| 安全模式 | `agent-reach install --env=auto --safe` | 生产服务器、多人共用机器 |
|
||||||
| 仅预览 | `agent-reach install --env=auto --dry-run` | 先看看会做什么 |
|
| 仅预览 | `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`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 贡献
|
## 贡献
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,13 @@ def main():
|
||||||
# ── doctor ──
|
# ── doctor ──
|
||||||
sub.add_parser("doctor", help="Check platform availability")
|
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 ──
|
# ── check-update ──
|
||||||
sub.add_parser("check-update", help="Check for new versions and changes")
|
sub.add_parser("check-update", help="Check for new versions and changes")
|
||||||
|
|
||||||
|
|
@ -105,6 +112,8 @@ def main():
|
||||||
_cmd_install(args)
|
_cmd_install(args)
|
||||||
elif args.command == "configure":
|
elif args.command == "configure":
|
||||||
_cmd_configure(args)
|
_cmd_configure(args)
|
||||||
|
elif args.command == "uninstall":
|
||||||
|
_cmd_uninstall(args)
|
||||||
|
|
||||||
|
|
||||||
# ── Command handlers ────────────────────────────────
|
# ── Command handlers ────────────────────────────────
|
||||||
|
|
@ -680,6 +689,104 @@ def _cmd_configure(args):
|
||||||
print(f"✅ Groq key configured!")
|
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():
|
def _cmd_doctor():
|
||||||
from agent_reach.config import Config
|
from agent_reach.config import Config
|
||||||
from agent_reach.doctor import check_all, format_report
|
from agent_reach.doctor import check_all, format_report
|
||||||
|
|
@ -688,19 +795,6 @@ def _cmd_doctor():
|
||||||
print(format_report(results))
|
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():
|
def _cmd_setup():
|
||||||
from agent_reach.config import Config
|
from agent_reach.config import Config
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ class Config:
|
||||||
def load(self):
|
def load(self):
|
||||||
"""Load config from YAML file."""
|
"""Load config from YAML file."""
|
||||||
if self.config_path.exists():
|
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 {}
|
self.data = yaml.safe_load(f) or {}
|
||||||
else:
|
else:
|
||||||
self.data = {}
|
self.data = {}
|
||||||
|
|
@ -49,7 +49,7 @@ class Config:
|
||||||
def save(self):
|
def save(self):
|
||||||
"""Save config to YAML file."""
|
"""Save config to YAML file."""
|
||||||
self._ensure_dir()
|
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)
|
yaml.dump(self.data, f, default_flow_style=False, allow_unicode=True)
|
||||||
# Restrict permissions — config may contain credentials
|
# Restrict permissions — config may contain credentials
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -74,11 +74,12 @@ def format_report(results: Dict[str, dict]) -> str:
|
||||||
if ok_count < total:
|
if ok_count < total:
|
||||||
lines.append("运行 `agent-reach setup` 解锁更多渠道")
|
lines.append("运行 `agent-reach setup` 解锁更多渠道")
|
||||||
|
|
||||||
# Security check: config file permissions
|
# Security check: config file permissions (Unix only)
|
||||||
import os
|
import os
|
||||||
import stat
|
import stat
|
||||||
|
import sys
|
||||||
config_path = Config.CONFIG_DIR / "config.yaml"
|
config_path = Config.CONFIG_DIR / "config.yaml"
|
||||||
if config_path.exists():
|
if config_path.exists() and sys.platform != "win32":
|
||||||
try:
|
try:
|
||||||
mode = config_path.stat().st_mode
|
mode = config_path.stat().st_mode
|
||||||
if mode & (stat.S_IRGRP | stat.S_IROTH):
|
if mode & (stat.S_IRGRP | stat.S_IROTH):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue