# -*- coding: utf-8 -*- """ Agent Reach CLI โ€” installer, doctor, and configuration tool. Usage: agent-reach install --env=auto agent-reach doctor agent-reach configure twitter-cookies "auth_token=xxx; ct0=yyy" agent-reach setup """ import sys import argparse import json import os # Fix Windows console encoding โ€” emoji/CJK characters crash on cp936/cp1252 if sys.platform == 'win32': import io if hasattr(sys.stdout, 'buffer'): sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') if hasattr(sys.stderr, 'buffer'): sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') from agent_reach import __version__ def _configure_logging(verbose: bool = False): """Suppress loguru output unless --verbose is set.""" from loguru import logger logger.remove() # Remove default stderr handler if verbose: logger.add(sys.stderr, level="INFO") def main(): parser = argparse.ArgumentParser( prog="agent-reach", description="๐Ÿ‘๏ธ Give your AI Agent eyes to see the entire internet", ) parser.add_argument("-v", "--verbose", action="store_true", help="Show debug logs") parser.add_argument("--version", action="version", version=f"Agent Reach v{__version__}") sub = parser.add_subparsers(dest="command", help="Available commands") # โ”€โ”€ read โ”€โ”€ # โ”€โ”€ setup โ”€โ”€ sub.add_parser("setup", help="Interactive configuration wizard") # โ”€โ”€ install โ”€โ”€ p_install = sub.add_parser("install", help="One-shot installer with flags") p_install.add_argument("--env", choices=["local", "server", "auto"], default="auto", help="Environment: local, server, or auto-detect") p_install.add_argument("--proxy", default="", help="Residential proxy for Reddit/Bilibili (http://user:pass@ip:port)") p_install.add_argument("--safe", action="store_true", help="Safe mode: skip automatic system changes, show what's needed instead") p_install.add_argument("--dry-run", action="store_true", help="Show what would be done without making any changes") # โ”€โ”€ configure โ”€โ”€ p_conf = sub.add_parser("configure", help="Set a config value or auto-extract from browser") p_conf.add_argument("key", nargs="?", default=None, choices=["proxy", "github-token", "groq-key", "twitter-cookies", "youtube-cookies"], help="What to configure (omit if using --from-browser)") p_conf.add_argument("value", nargs="*", help="The value(s) to set") p_conf.add_argument("--from-browser", metavar="BROWSER", choices=["chrome", "firefox", "edge", "brave", "opera"], help="Auto-extract ALL platform cookies from browser (chrome/firefox/edge/brave/opera)") # โ”€โ”€ 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") # โ”€โ”€ watch โ”€โ”€ sub.add_parser("watch", help="Quick health check + update check (for scheduled tasks)") # โ”€โ”€ version โ”€โ”€ sub.add_parser("version", help="Show version") args = parser.parse_args() # Suppress loguru noise unless --verbose _configure_logging(getattr(args, "verbose", False)) if not args.command: parser.print_help() sys.exit(0) if args.command == "version": print(f"Agent Reach v{__version__}") sys.exit(0) if args.command == "doctor": _cmd_doctor() elif args.command == "check-update": _cmd_check_update() elif args.command == "watch": _cmd_watch() elif args.command == "setup": _cmd_setup() elif args.command == "install": _cmd_install(args) elif args.command == "configure": _cmd_configure(args) elif args.command == "uninstall": _cmd_uninstall(args) # โ”€โ”€ Command handlers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ def _cmd_install(args): """One-shot deterministic installer.""" import os from agent_reach.config import Config from agent_reach.doctor import check_all, format_report safe_mode = args.safe dry_run = args.dry_run config = Config() print() print("๐Ÿ‘๏ธ Agent Reach Installer") print("=" * 40) if dry_run: print("๐Ÿ” DRY RUN โ€” showing what would be done (no changes)") print() if safe_mode: print("๐Ÿ›ก๏ธ SAFE MODE โ€” skipping automatic system changes") print() # Auto-detect environment env = args.env if env == "auto": env = _detect_environment() if env == "server": print(f"๐Ÿ“ก Environment: Server/VPS (auto-detected)") else: print(f"๐Ÿ’ป Environment: Local computer (auto-detected)") # Apply explicit flags if args.proxy: if dry_run: print(f"[dry-run] Would configure proxy for Reddit + Bilibili") else: config.set("reddit_proxy", args.proxy) config.set("bilibili_proxy", args.proxy) print(f"โœ… Proxy configured for Reddit + Bilibili") # โ”€โ”€ Install system dependencies โ”€โ”€ print() if dry_run: _install_system_deps_dryrun() elif safe_mode: _install_system_deps_safe() else: _install_system_deps() # โ”€โ”€ mcporter (for Exa search + XiaoHongShu) โ”€โ”€ print() if dry_run: print("๐Ÿ“ฆ [dry-run] Would install mcporter and configure Exa search") elif safe_mode: _install_mcporter_safe() else: _install_mcporter() # Auto-import cookies on local computers if env == "local" and not safe_mode and not dry_run: print() print("๐Ÿช Trying to import cookies from browser...") try: from agent_reach.cookie_extract import configure_from_browser results = configure_from_browser("chrome", config) found = False for platform, success, message in results: if success: print(f" โœ… {platform}: {message}") found = True if not found: # Try firefox results = configure_from_browser("firefox", config) for platform, success, message in results: if success: print(f" โœ… {platform}: {message}") found = True if not found: print(" โฌœ No cookies found (normal if you haven't logged into these sites)") except Exception: print(" โฌœ Could not read browser cookies (browser might be open)") elif env == "local" and safe_mode: print() print("๐Ÿช Skipped auto cookie import (safe mode).") print(" To import manually: agent-reach configure --from-browser chrome") elif env == "local" and dry_run: print() print("๐Ÿช [dry-run] Would try to import cookies from Chrome/Firefox") # Environment-specific advice if env == "server": print() print("๐Ÿ’ก Tip: Reddit and Bilibili block server IPs.") print(" Reddit search still works via Exa (free).") print(" For full access: agent-reach configure proxy http://user:pass@ip:port") print(" Cheap option: https://www.webshare.io ($1/month)") # Test channels if not dry_run: print() print("Testing channels...") results = check_all(config) ok = sum(1 for r in results.values() if r["status"] == "ok") total = len(results) # Final status print() print(format_report(results)) print() # โ”€โ”€ Install agent skill โ”€โ”€ _install_skill() print(f"โœ… Installation complete! {ok}/{total} channels active.") if ok < total: print() print("๐Ÿค– Some channels are not active yet. If you're an AI Agent, try to fix them yourself โ€” then run `agent-reach doctor` again.") else: print() print("๐Ÿ” Dry run complete. No changes were made.") def _install_skill(): """Install Agent Reach as an agent skill (OpenClaw / Claude Code).""" import os import importlib.resources # Determine skill install path skill_dirs = [ os.path.expanduser("~/.openclaw/skills"), # OpenClaw os.path.expanduser("~/.claude/skills"), # Claude Code (if exists) os.path.expanduser("~/.agents/skills"), # Generic agents ] installed = False for skill_dir in skill_dirs: if os.path.isdir(skill_dir): target = os.path.join(skill_dir, "agent-reach") try: os.makedirs(target, exist_ok=True) # Read SKILL.md from package data skill_md = importlib.resources.files("agent_reach").joinpath("skill", "SKILL.md").read_text() with open(os.path.join(target, "SKILL.md"), "w") as f: f.write(skill_md) platform_name = "OpenClaw" if "openclaw" in skill_dir else "Claude Code" if "claude" in skill_dir else "Agent" print(f"๐Ÿงฉ Skill installed for {platform_name}: {target}") installed = True except Exception: pass if not installed: # No known skill directory found โ€” create for OpenClaw by default target = os.path.expanduser("~/.openclaw/skills/agent-reach") try: os.makedirs(target, exist_ok=True) skill_md = importlib.resources.files("agent_reach").joinpath("skill", "SKILL.md").read_text() with open(os.path.join(target, "SKILL.md"), "w") as f: f.write(skill_md) print(f"๐Ÿงฉ Skill installed: {target}") except Exception: print(" โฌœ Could not install agent skill (optional)") def _install_system_deps(): """Install system-level dependencies: gh CLI, Node.js (for mcporter).""" import shutil import subprocess import platform print("๐Ÿ”ง Checking system dependencies...") # โ”€โ”€ gh CLI โ”€โ”€ if shutil.which("gh"): print(" โœ… gh CLI already installed") else: print(" ๐Ÿ“ฅ Installing gh CLI...") os_type = platform.system().lower() if os_type == "linux": try: # Official GitHub method for Linux cmds = [ "curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg 2>/dev/null", 'echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null', "apt-get update -qq 2>/dev/null", "apt-get install -y -qq gh 2>/dev/null", ] for cmd in cmds: subprocess.run(cmd, shell=True, capture_output=True, timeout=60) if shutil.which("gh"): print(" โœ… gh CLI installed") else: print(" โš ๏ธ gh CLI install failed. You can try: snap install gh, or download from https://github.com/cli/cli/releases") except Exception: print(" โš ๏ธ gh CLI install failed. You can try: snap install gh, or download from https://github.com/cli/cli/releases") elif os_type == "darwin": if shutil.which("brew"): try: subprocess.run(["brew", "install", "gh"], capture_output=True, timeout=120) if shutil.which("gh"): print(" โœ… gh CLI installed") else: print(" โš ๏ธ gh CLI install failed. Try: brew install gh") except Exception: print(" โš ๏ธ gh CLI install failed. Try: brew install gh") else: print(" โš ๏ธ gh CLI not found. Install: https://cli.github.com") else: print(" โš ๏ธ gh CLI not found. Install: https://cli.github.com") # โ”€โ”€ Node.js (needed for mcporter) โ”€โ”€ if shutil.which("node") and shutil.which("npm"): print(" โœ… Node.js already installed") else: print(" ๐Ÿ“ฅ Installing Node.js...") try: # Use NodeSource for quick install subprocess.run( "curl -fsSL https://deb.nodesource.com/setup_22.x | bash - 2>/dev/null && apt-get install -y -qq nodejs 2>/dev/null", shell=True, capture_output=True, timeout=120, ) if shutil.which("node"): print(" โœ… Node.js installed") else: print(" โš ๏ธ Node.js install failed. Try: apt install nodejs npm, or nvm install 22, or download from https://nodejs.org") except Exception: print(" โš ๏ธ Node.js install failed. Try: apt install nodejs npm, or nvm install 22, or download from https://nodejs.org") # โ”€โ”€ bird CLI (for Twitter search) โ”€โ”€ if shutil.which("bird") or shutil.which("birdx"): print(" โœ… bird CLI already installed") else: if shutil.which("npm"): try: subprocess.run( ["npm", "install", "-g", "@steipete/bird"], capture_output=True, text=True, timeout=120, ) if shutil.which("bird"): print(" โœ… bird CLI installed (Twitter search + timeline)") else: print(" โฌœ bird CLI install failed (optional โ€” Twitter reading still works via Jina)") except Exception: print(" โฌœ bird CLI install failed (optional โ€” Twitter reading still works via Jina)") else: print(" โฌœ bird CLI requires Node.js (optional โ€” Twitter reading still works via Jina)") # โ”€โ”€ undici (proxy support for Node.js fetch) โ”€โ”€ if shutil.which("npm"): npm_root = subprocess.run(["npm", "root", "-g"], capture_output=True, text=True, timeout=5).stdout.strip() undici_path = os.path.join(npm_root, "undici", "index.js") if npm_root else "" if os.path.exists(undici_path): print(" โœ… undici already installed (Node.js proxy support)") else: try: subprocess.run(["npm", "install", "-g", "undici"], capture_output=True, text=True, timeout=60) print(" โœ… undici installed (Node.js proxy support)") except Exception: print(" โฌœ undici install failed (optional โ€” bird may not work behind proxies)") def _install_system_deps_safe(): """Safe mode: check what's installed, print instructions for what's missing.""" import shutil print("๐Ÿ”ง Checking system dependencies (safe mode โ€” no auto-install)...") deps = [ ("gh", ["gh"], "GitHub CLI", "https://cli.github.com โ€” or: apt install gh / brew install gh"), ("node", ["node", "npm"], "Node.js", "https://nodejs.org โ€” or: apt install nodejs npm"), ("bird", ["bird", "birdx"], "bird CLI (Twitter)", "npm install -g @steipete/bird"), ] missing = [] for name, binaries, label, install_hint in deps: found = any(shutil.which(b) for b in binaries) if found: print(f" โœ… {label} already installed") else: print(f" โฌœ {label} not found") missing.append((label, install_hint)) if missing: print() print(" To install missing dependencies manually:") for label, hint in missing: print(f" {label}: {hint}") else: print(" All system dependencies are installed!") def _install_system_deps_dryrun(): """Dry-run: just show what would be checked/installed.""" import shutil print("๐Ÿ”ง [dry-run] System dependency check:") checks = [ ("gh CLI", ["gh"], "apt install gh / brew install gh"), ("Node.js", ["node"], "curl NodeSource setup | bash + apt install nodejs"), ("bird CLI", ["bird", "birdx"], "npm install -g @steipete/bird"), ] for label, binaries, method in checks: found = any(shutil.which(b) for b in binaries) if found: print(f" โœ… {label}: already installed, skip") else: print(f" ๐Ÿ“ฅ {label}: would install via: {method}") def _install_mcporter(): """Install mcporter and configure Exa + XiaoHongShu MCP servers.""" import shutil import subprocess print("๐Ÿ“ฆ Setting up mcporter (search + XiaoHongShu backend)...") if shutil.which("mcporter"): print(" โœ… mcporter already installed") else: # Check for npm/npx if not shutil.which("npm") and not shutil.which("npx"): print(" โš ๏ธ mcporter requires Node.js. Install Node.js first:") print(" https://nodejs.org/ or: curl -fsSL https://fnm.vercel.app/install | bash") return try: subprocess.run( ["npm", "install", "-g", "mcporter"], capture_output=True, text=True, timeout=120, ) if shutil.which("mcporter"): print(" โœ… mcporter installed") else: print(" โŒ mcporter install failed. Retry: npm install -g mcporter (check network/timeout), or try: npx mcporter@latest list") return except Exception as e: print(f" โŒ mcporter install failed: {e}") return # Configure Exa MCP (free, no key needed) try: r = subprocess.run( ["mcporter", "config", "list"], capture_output=True, text=True, timeout=5 ) if "exa" not in r.stdout: subprocess.run( ["mcporter", "config", "add", "exa", "https://mcp.exa.ai/mcp"], capture_output=True, text=True, timeout=10, ) print(" โœ… Exa search configured (free, no API key needed)") else: print(" โœ… Exa search already configured") except Exception: print(" โš ๏ธ Could not configure Exa. Run manually: mcporter config add exa https://mcp.exa.ai/mcp") # Check XiaoHongShu MCP (only if server is running) try: r = subprocess.run( ["mcporter", "config", "list"], capture_output=True, text=True, timeout=5 ) if "xiaohongshu" in r.stdout: print(" โœ… XiaoHongShu MCP already configured") else: # Check if XHS MCP server is running on localhost:18060 import requests try: requests.get("http://localhost:18060/", timeout=3) subprocess.run( ["mcporter", "config", "add", "xiaohongshu", "http://localhost:18060/mcp"], capture_output=True, text=True, timeout=10, ) print(" โœ… XiaoHongShu MCP auto-detected and configured") except Exception: print(" โฌœ XiaoHongShu MCP not detected (optional)") print(" Install: docker run -d --name xiaohongshu-mcp -p 18060:18060 xpzouying/xiaohongshu-mcp") print(" Then: mcporter config add xiaohongshu http://localhost:18060/mcp") print(" Repo: https://github.com/xpzouying/xiaohongshu-mcp") except Exception: pass def _install_mcporter_safe(): """Safe mode: check mcporter status, print instructions.""" import shutil print("๐Ÿ“ฆ Checking mcporter (safe mode)...") if shutil.which("mcporter"): print(" โœ… mcporter already installed") print(" To configure Exa search: mcporter config add exa https://mcp.exa.ai/mcp") else: print(" โฌœ mcporter not installed") print(" To install: npm install -g mcporter") print(" Then configure Exa: mcporter config add exa https://mcp.exa.ai/mcp") def _detect_environment(): """Auto-detect if running on local computer or server.""" import os # Check common server indicators indicators = 0 # SSH session if os.environ.get("SSH_CONNECTION") or os.environ.get("SSH_CLIENT"): indicators += 2 # Docker / container if os.path.exists("/.dockerenv") or os.path.exists("/run/.containerenv"): indicators += 2 # No display (headless) if not os.environ.get("DISPLAY") and not os.environ.get("WAYLAND_DISPLAY"): indicators += 1 # Cloud VM identifiers for cloud_file in ["/sys/hypervisor/uuid", "/sys/class/dmi/id/product_name"]: if os.path.exists(cloud_file): try: content = open(cloud_file).read().lower() if any(x in content for x in ["amazon", "google", "microsoft", "digitalocean", "linode", "vultr", "hetzner"]): indicators += 2 except: pass # systemd-detect-virt try: import subprocess result = subprocess.run(["systemd-detect-virt"], capture_output=True, text=True, timeout=3) if result.returncode == 0 and result.stdout.strip() != "none": indicators += 1 except: pass return "server" if indicators >= 2 else "local" def _cmd_configure(args): """Set a config value and test it, or auto-extract from browser.""" import shutil from agent_reach.config import Config config = Config() # โ”€โ”€ Auto-extract from browser โ”€โ”€ if args.from_browser: from agent_reach.cookie_extract import configure_from_browser browser = args.from_browser print(f"๐Ÿ” Extracting cookies from {browser}...") print() results = configure_from_browser(browser, config) found_any = False for platform, success, message in results: if success: print(f" โœ… {platform}: {message}") found_any = True else: print(f" โฌœ {platform}: {message}") print() if found_any: print("โœ… Cookies configured! Run `agent-reach doctor` to see updated status.") else: print(f"No cookies found. Make sure you're logged into the platforms in {browser}.") return # โ”€โ”€ Manual configure โ”€โ”€ if not args.key: print("Usage: agent-reach configure ") print(" or: agent-reach configure --from-browser chrome") return value = " ".join(args.value) if args.value else "" if not value: print(f"Missing value for {args.key}") return if args.key == "proxy": config.set("reddit_proxy", value) config.set("bilibili_proxy", value) print(f"โœ… Proxy configured for Reddit + Bilibili!") # Auto-test print("Testing Reddit access...", end=" ") try: import requests resp = requests.get( "https://www.reddit.com/r/test.json?limit=1", headers={"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"}, proxies={"http": value, "https": value}, timeout=10, ) if resp.status_code == 200: print("โœ… Reddit works!") else: print(f"โš ๏ธ Reddit returned {resp.status_code}") except Exception as e: print(f"โŒ Failed: {e}") elif args.key == "twitter-cookies": # Accept two formats: # 1. auth_token ct0 (two separate values) # 2. Full cookie header string: "auth_token=xxx; ct0=yyy; ..." auth_token = None ct0 = None if "auth_token=" in value and "ct0=" in value: # Full cookie string โ€” parse it for part in value.replace(";", " ").split(): if part.startswith("auth_token="): auth_token = part.split("=", 1)[1] elif part.startswith("ct0="): ct0 = part.split("=", 1)[1] elif len(value.split()) == 2 and "=" not in value: # Two separate values: AUTH_TOKEN CT0 parts = value.split() auth_token = parts[0] ct0 = parts[1] if auth_token and ct0: config.set("twitter_auth_token", auth_token) config.set("twitter_ct0", ct0) print(f"โœ… Twitter cookies configured!") print("Testing Twitter access...", end=" ") try: import subprocess bird = shutil.which("bird") or shutil.which("birdx") if not bird: print("โš ๏ธ bird CLI not installed. Run: npm install -g @steipete/bird") else: import os env = os.environ.copy() env["AUTH_TOKEN"] = auth_token env["CT0"] = ct0 result = subprocess.run( [bird, "search", "test", "-n", "1"], capture_output=True, text=True, timeout=15, env=env, ) if result.returncode == 0 and result.stdout.strip(): print("โœ… Twitter Advanced works!") else: print(f"โš ๏ธ Test returned no results (cookies might be wrong)") except Exception as e: print(f"โŒ Failed: {e}") else: print("โŒ Could not find auth_token and ct0 in your input.") print(" Accepted formats:") print(" 1. agent-reach configure twitter-cookies AUTH_TOKEN CT0") print(' 2. agent-reach configure twitter-cookies "auth_token=xxx; ct0=yyy; ..."') elif args.key == "youtube-cookies": config.set("youtube_cookies_from", value) print(f"โœ… YouTube cookie source configured: {value}") print(" yt-dlp will use cookies from this browser for age-restricted/member videos.") elif args.key == "github-token": config.set("github_token", value) print(f"โœ… GitHub token configured!") elif args.key == "groq-key": config.set("groq_api_key", value) 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 config = Config() results = check_all(config) print(format_report(results)) def _cmd_setup(): from agent_reach.config import Config config = Config() print() print("๐Ÿ‘๏ธ Agent Reach Setup") print("=" * 40) print() # Step 1: Exa print("ใ€ๆŽจ่ใ€‘ๅ…จ็ฝ‘ๆœ็ดข โ€” Exa Search API") print(" ๅ…่ดน 1000 ๆฌก/ๆœˆ๏ผŒๆณจๅ†Œๅœฐๅ€: https://exa.ai") current = config.get("exa_api_key") if current: print(f" ๅฝ“ๅ‰็Šถๆ€: โœ… ๅทฒ้…็ฝฎ ({current[:8]}...)") change = input(" ่ฆๆ›ดๆขๅ—๏ผŸ[y/N]: ").strip().lower() if change != "y": print() else: key = input(" EXA_API_KEY: ").strip() if key: config.set("exa_api_key", key) print(" โœ… ๅทฒๆ›ดๆ–ฐ๏ผ") print() else: print(" ๅฝ“ๅ‰็Šถๆ€: โฌœ ๆœช้…็ฝฎ") key = input(" EXA_API_KEY (ๅ›ž่ฝฆ่ทณ่ฟ‡): ").strip() if key: config.set("exa_api_key", key) print(" โœ… ๅ…จ็ฝ‘ๆœ็ดข + Redditๆœ็ดข + Twitterๆœ็ดข ๅทฒๅผ€ๅฏ๏ผ") else: print(" โ„น๏ธ ่ทณ่ฟ‡ใ€‚็จๅŽๅฏ่ฟ่กŒ agent-reach setup ้…็ฝฎ") print() # Step 2: GitHub token print("ใ€ๅฏ้€‰ใ€‘GitHub Token โ€” ๆ้ซ˜ API ้™้ข") print(" ๆ—  token: 60 ๆฌก/ๅฐๆ—ถ | ๆœ‰ token: 5000 ๆฌก/ๅฐๆ—ถ") print(" ่Žทๅ–: https://github.com/settings/tokens (ๆ— ้œ€ไปปไฝ•ๆƒ้™)") current = config.get("github_token") if current: print(f" ๅฝ“ๅ‰็Šถๆ€: โœ… ๅทฒ้…็ฝฎ") else: key = input(" GITHUB_TOKEN (ๅ›ž่ฝฆ่ทณ่ฟ‡): ").strip() if key: config.set("github_token", key) print(" โœ… GitHub API ๅทฒๆๅ‡่‡ณ 5000 ๆฌก/ๅฐๆ—ถ๏ผ") else: print(" โ„น๏ธ ่ทณ่ฟ‡ใ€‚ๅ…ฌๅผ€ API ไนŸ่ƒฝ็”จ") print() # Step 3: Reddit proxy print("ใ€ๅฏ้€‰ใ€‘Reddit ไปฃ็† โ€” ๅฎŒๆ•ด้˜…่ฏป Reddit ๅธ–ๅญ+่ฏ„่ฎบ") print(" Reddit ๅฐ้”ๅพˆๅคš IP๏ผŒ้œ€่ฆ ISP ไปฃ็†ๆ‰่ƒฝ็›ดๆŽฅ่ฎฟ้—ฎ") print(" ๆ ผๅผ: http://็”จๆˆทๅ:ๅฏ†็ @IP:็ซฏๅฃ") current = config.get("reddit_proxy") if current: print(f" ๅฝ“ๅ‰็Šถๆ€: โœ… ๅทฒ้…็ฝฎ") else: proxy = input(" REDDIT_PROXY (ๅ›ž่ฝฆ่ทณ่ฟ‡): ").strip() if proxy: config.set("reddit_proxy", proxy) print(" โœ… Reddit ๅฎŒๆ•ด้˜…่ฏปๅทฒๅผ€ๅฏ๏ผ") else: print(" โ„น๏ธ ่ทณ่ฟ‡ใ€‚ไปๅฏ้€š่ฟ‡ๆœ็ดข่Žทๅ– Reddit ๅ†…ๅฎน") print() # Step 4: Groq (Whisper) print("ใ€ๅฏ้€‰ใ€‘Groq API โ€” ่ง†้ข‘ๆ— ๅญ—ๅน•ๆ—ถ็š„่ฏญ้Ÿณ่ฝฌๆ–‡ๅญ—") print(" ๅ…่ดน้ขๅบฆ๏ผŒๆณจๅ†Œ: https://console.groq.com") current = config.get("groq_api_key") if current: print(f" ๅฝ“ๅ‰็Šถๆ€: โœ… ๅทฒ้…็ฝฎ") else: key = input(" GROQ_API_KEY (ๅ›ž่ฝฆ่ทณ่ฟ‡): ").strip() if key: config.set("groq_api_key", key) print(" โœ… ่ฏญ้Ÿณ่ฝฌๆ–‡ๅญ—ๅทฒๅผ€ๅฏ๏ผ") else: print(" โ„น๏ธ ่ทณ่ฟ‡") print() # Summary print("=" * 40) print(f"โœ… ้…็ฝฎๅทฒไฟๅญ˜ๅˆฐ {config.config_path}") print("่ฟ่กŒ agent-reach doctor ๆŸฅ็œ‹ๅฎŒๆ•ด็Šถๆ€") print() def _cmd_check_update(): """Check for newer versions on GitHub.""" import requests from agent_reach import __version__ print(f"๐Ÿ“ฆ ๅฝ“ๅ‰็‰ˆๆœฌ: v{__version__}") try: # Fetch latest version from GitHub resp = requests.get( "https://api.github.com/repos/Panniantong/Agent-Reach/releases/latest", timeout=10, ) if resp.status_code == 200: data = resp.json() latest = data.get("tag_name", "").lstrip("v") body = data.get("body", "") if latest and latest != __version__: print(f"๐Ÿ†• ๆœ€ๆ–ฐ็‰ˆๆœฌ: v{latest} โ† ๆœ‰ๆ›ดๆ–ฐ๏ผ") if body: print() print("ๆ›ดๆ–ฐๅ†…ๅฎน๏ผš") # Show first 20 lines of release notes for line in body.strip().split("\n")[:20]: print(f" {line}") print() print("ๆ›ดๆ–ฐๅ‘ฝไปค:") print(" pip install --upgrade https://github.com/Panniantong/agent-reach/archive/main.zip") return "update_available" else: print(f"โœ… ๅทฒๆ˜ฏๆœ€ๆ–ฐ็‰ˆๆœฌ") return "up_to_date" else: # No releases yet, fall back to comparing commit resp2 = requests.get( "https://api.github.com/repos/Panniantong/Agent-Reach/commits/main", timeout=10, ) if resp2.status_code == 200: commit = resp2.json() sha = commit.get("sha", "")[:7] msg = commit.get("commit", {}).get("message", "").split("\n")[0] date = commit.get("commit", {}).get("committer", {}).get("date", "")[:10] print(f"๐Ÿ” ๆœ€ๆ–ฐๆไบค: {sha} ({date}) {msg}") print() print("ๆ›ดๆ–ฐๅ‘ฝไปค:") print(" pip install --upgrade https://github.com/Panniantong/agent-reach/archive/main.zip") return "unknown" else: print("โš ๏ธ ๆ— ๆณ•ๆฃ€ๆŸฅๆ›ดๆ–ฐ๏ผˆ็ฝ‘็ปœ้—ฎ้ข˜๏ผ‰") return "error" except Exception as e: print(f"โš ๏ธ ๆ— ๆณ•ๆฃ€ๆŸฅๆ›ดๆ–ฐ: {e}") return "error" def _cmd_watch(): """Quick health check + update check, designed for scheduled tasks. Only outputs problems. If everything is fine, outputs a single line. """ from agent_reach.config import Config from agent_reach.doctor import check_all import requests from agent_reach import __version__ config = Config() issues = [] # Check channels results = check_all(config) ok = sum(1 for r in results.values() if r["status"] == "ok") total = len(results) # Find broken channels (were working, now broken) for key, r in results.items(): if r["status"] in ("off", "error"): issues.append(f"โŒ {r['name']}๏ผš{r['message']}") elif r["status"] == "warn": issues.append(f"โš ๏ธ {r['name']}๏ผš{r['message']}") # Check for updates update_available = False new_version = "" release_body = "" try: resp = requests.get( "https://api.github.com/repos/Panniantong/Agent-Reach/releases/latest", timeout=10, ) if resp.status_code == 200: data = resp.json() latest = data.get("tag_name", "").lstrip("v") if latest and latest != __version__: update_available = True new_version = latest release_body = data.get("body", "") except Exception: pass # Output if not issues and not update_available: print(f"๐Ÿ‘๏ธ Agent Reach: ๅ…จ้ƒจๆญฃๅธธ ({ok}/{total} ๆธ ้“ๅฏ็”จ๏ผŒv{__version__} ๅทฒๆ˜ฏๆœ€ๆ–ฐ)") return print(f"๐Ÿ‘๏ธ Agent Reach ็›‘ๆŽงๆŠฅๅ‘Š") print(f"=" * 40) print(f"๐Ÿ“ฆ ็‰ˆๆœฌ: v{__version__} | ๆธ ้“: {ok}/{total}") if issues: print() for issue in issues: print(f" {issue}") if update_available: print() print(f"๐Ÿ†• ๆ–ฐ็‰ˆๆœฌๅฏ็”จ: v{new_version}") if release_body: for line in release_body.strip().split("\n")[:10]: print(f" {line}") print(f" ๆ›ดๆ–ฐ: pip install --upgrade https://github.com/Panniantong/agent-reach/archive/main.zip") if __name__ == "__main__": main()