fix: restore full cli file for xhs cookie import
This commit is contained in:
+704
-3
@@ -1,5 +1,3 @@
|
|||||||
Total output lines: 1762
|
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
Agent Reach CLI — installer, doctor, and configuration tool.
|
Agent Reach CLI — installer, doctor, and configuration tool.
|
||||||
@@ -518,7 +516,710 @@ def _install_system_deps():
|
|||||||
with open(list_path, "w", encoding="utf-8") as f:
|
with open(list_path, "w", encoding="utf-8") as f:
|
||||||
f.write(repo_line)
|
f.write(repo_line)
|
||||||
subprocess.run(["apt-get", "update", "-qq"], capture_output=True, timeout=60)
|
subprocess.run(["apt-get", "update", "-qq"], capture_output=True, timeout=60)
|
||||||
subproc…6779 tokens truncated… JSON array: \'[{"name":"x","value":"y","domain":".xiaohongshu.com",...}]\'')
|
subprocess.run(["apt-get", "install", "-y", "-qq", "gh"], 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 setup script without invoking a shell pipeline.
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".sh") as tf:
|
||||||
|
script_path = tf.name
|
||||||
|
subprocess.run(
|
||||||
|
["curl", "-fsSL", "https://deb.nodesource.com/setup_22.x", "-o", script_path],
|
||||||
|
capture_output=True, timeout=60,
|
||||||
|
)
|
||||||
|
subprocess.run(
|
||||||
|
["bash", script_path],
|
||||||
|
capture_output=True, timeout=120,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
os.unlink(script_path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
subprocess.run(
|
||||||
|
["apt-get", "install", "-y", "-qq", "nodejs"],
|
||||||
|
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")
|
||||||
|
|
||||||
|
# ── undici (proxy support for Node.js fetch) ──
|
||||||
|
npm_cmd = shutil.which("npm")
|
||||||
|
if npm_cmd:
|
||||||
|
npm_root = subprocess.run([npm_cmd, "root", "-g"], capture_output=True, encoding="utf-8", errors="replace", 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_cmd, "install", "-g", "undici"], capture_output=True, encoding="utf-8", errors="replace", timeout=60)
|
||||||
|
print(" ✅ undici installed (Node.js proxy support)")
|
||||||
|
except Exception:
|
||||||
|
print(" -- undici install failed (optional — may not work behind proxies)")
|
||||||
|
|
||||||
|
# ── yt-dlp JS runtime config (YouTube requires external JS runtime) ──
|
||||||
|
if shutil.which("node"):
|
||||||
|
ytdlp_config_dir = os.path.expanduser("~/.config/yt-dlp")
|
||||||
|
ytdlp_config = os.path.join(ytdlp_config_dir, "config")
|
||||||
|
needs_config = True
|
||||||
|
if os.path.exists(ytdlp_config):
|
||||||
|
with open(ytdlp_config, "r") as f:
|
||||||
|
if "--js-runtimes" in f.read():
|
||||||
|
needs_config = False
|
||||||
|
print(" ✅ yt-dlp JS runtime already configured")
|
||||||
|
if needs_config:
|
||||||
|
try:
|
||||||
|
os.makedirs(ytdlp_config_dir, exist_ok=True)
|
||||||
|
with open(ytdlp_config, "a") as f:
|
||||||
|
f.write("--js-runtimes node\n")
|
||||||
|
print(" ✅ yt-dlp configured to use Node.js as JS runtime (YouTube)")
|
||||||
|
except Exception:
|
||||||
|
print(" -- Could not configure yt-dlp JS runtime (YouTube may not work)")
|
||||||
|
|
||||||
|
# NOTE: twitter-cli, weibo, xiaoyuzhou, wechat, xhs-cli etc. are optional.
|
||||||
|
# They are installed via --channels flag, not here.
|
||||||
|
# See CHANNEL_INSTALLERS in _cmd_install().
|
||||||
|
|
||||||
|
|
||||||
|
def _install_xiaoyuzhou_deps():
|
||||||
|
"""Install Xiaoyuzhou podcast transcription script."""
|
||||||
|
import shutil
|
||||||
|
from agent_reach.config import Config
|
||||||
|
|
||||||
|
config = Config()
|
||||||
|
print("Setting up Xiaoyuzhou podcast transcription...")
|
||||||
|
|
||||||
|
tools_dir = os.path.expanduser("~/.agent-reach/tools/xiaoyuzhou")
|
||||||
|
script_dst = os.path.join(tools_dir, "transcribe.sh")
|
||||||
|
|
||||||
|
if os.path.isfile(script_dst):
|
||||||
|
print(" ✅ Xiaoyuzhou transcription script already installed")
|
||||||
|
else:
|
||||||
|
# Copy script from package
|
||||||
|
script_src = os.path.join(os.path.dirname(__file__), "scripts", "transcribe_xiaoyuzhou.sh")
|
||||||
|
if os.path.isfile(script_src):
|
||||||
|
try:
|
||||||
|
os.makedirs(tools_dir, exist_ok=True)
|
||||||
|
import shutil as _shutil
|
||||||
|
_shutil.copy2(script_src, script_dst)
|
||||||
|
os.chmod(script_dst, 0o755)
|
||||||
|
print(" ✅ Xiaoyuzhou transcription script installed")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [!] Failed to install script: {e}")
|
||||||
|
else:
|
||||||
|
print(" [!] Script source not found in package")
|
||||||
|
|
||||||
|
# Check ffmpeg
|
||||||
|
if shutil.which("ffmpeg"):
|
||||||
|
print(" ✅ ffmpeg available")
|
||||||
|
else:
|
||||||
|
print(" -- ffmpeg not found. Install: apt install -y ffmpeg (or brew install ffmpeg)")
|
||||||
|
|
||||||
|
# Check GROQ_API_KEY
|
||||||
|
has_key = bool(os.environ.get("GROQ_API_KEY")) or bool(config.get("groq_api_key"))
|
||||||
|
if has_key:
|
||||||
|
print(" ✅ Groq API key configured")
|
||||||
|
else:
|
||||||
|
print(" -- Groq API key not set. Get free key at https://console.groq.com")
|
||||||
|
print(" Then run: agent-reach configure groq-key gsk_xxxxx")
|
||||||
|
|
||||||
|
|
||||||
|
def _install_twitter_deps():
|
||||||
|
"""Install twitter-cli for Twitter search + timeline."""
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
print("Setting up Twitter (twitter-cli)...")
|
||||||
|
if shutil.which("twitter"):
|
||||||
|
print(" ✅ twitter-cli already installed")
|
||||||
|
return
|
||||||
|
for tool, cmd in [("pipx", ["pipx", "install", "twitter-cli"]),
|
||||||
|
("uv", ["uv", "tool", "install", "twitter-cli"])]:
|
||||||
|
if shutil.which(tool):
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd, capture_output=True, encoding="utf-8",
|
||||||
|
errors="replace", timeout=120)
|
||||||
|
if shutil.which("twitter"):
|
||||||
|
print(" ✅ twitter-cli installed")
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
print(" [!] twitter-cli install failed. Run: pipx install twitter-cli")
|
||||||
|
|
||||||
|
|
||||||
|
def _install_xhs_deps():
|
||||||
|
"""Install xhs-cli (xiaohongshu-cli) for XiaoHongShu."""
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
print("Setting up XiaoHongShu (xhs-cli)...")
|
||||||
|
if shutil.which("xhs"):
|
||||||
|
print(" ✅ xhs-cli already installed")
|
||||||
|
return
|
||||||
|
for tool, cmd in [("pipx", ["pipx", "install", "xiaohongshu-cli"]),
|
||||||
|
("uv", ["uv", "tool", "install", "xiaohongshu-cli"])]:
|
||||||
|
if shutil.which(tool):
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd, capture_output=True, encoding="utf-8",
|
||||||
|
errors="replace", timeout=120)
|
||||||
|
if shutil.which("xhs"):
|
||||||
|
print(" ✅ xhs-cli installed (run `xhs login` to authenticate)")
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
print(" [!] xhs-cli install failed. Run: pipx install xiaohongshu-cli")
|
||||||
|
|
||||||
|
|
||||||
|
def _install_reddit_deps():
|
||||||
|
"""Install rdt-cli for Reddit search + reading."""
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
print("Setting up Reddit (rdt-cli)...")
|
||||||
|
if shutil.which("rdt"):
|
||||||
|
print(" ✅ rdt-cli already installed")
|
||||||
|
return
|
||||||
|
for tool, cmd in [("pipx", ["pipx", "install", "rdt-cli"]),
|
||||||
|
("uv", ["uv", "tool", "install", "rdt-cli"])]:
|
||||||
|
if shutil.which(tool):
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd, capture_output=True, encoding="utf-8",
|
||||||
|
errors="replace", timeout=120)
|
||||||
|
if shutil.which("rdt"):
|
||||||
|
print(" ✅ rdt-cli installed")
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
print(" [!] rdt-cli install failed. Run: pipx install rdt-cli")
|
||||||
|
|
||||||
|
|
||||||
|
def _install_bili_deps():
|
||||||
|
"""Install bili-cli for Bilibili hot/rank/search."""
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
print("Setting up Bilibili (bili-cli)...")
|
||||||
|
if shutil.which("bili"):
|
||||||
|
print(" ✅ bili-cli already installed")
|
||||||
|
return
|
||||||
|
for tool, cmd in [("pipx", ["pipx", "install", "bilibili-cli"]),
|
||||||
|
("uv", ["uv", "tool", "install", "bilibili-cli"])]:
|
||||||
|
if shutil.which(tool):
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd, capture_output=True, encoding="utf-8",
|
||||||
|
errors="replace", timeout=120)
|
||||||
|
if shutil.which("bili"):
|
||||||
|
print(" ✅ bili-cli installed")
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
print(" [!] bili-cli install failed. Run: pipx install bilibili-cli")
|
||||||
|
|
||||||
|
|
||||||
|
def _install_weibo_deps():
|
||||||
|
"""Install Weibo MCP server (Panniantong fork with visitor passport auth)."""
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
print("Setting up Weibo MCP server...")
|
||||||
|
|
||||||
|
# Check if already installed and working
|
||||||
|
mcporter = shutil.which("mcporter")
|
||||||
|
if mcporter:
|
||||||
|
try:
|
||||||
|
r = subprocess.run(
|
||||||
|
[mcporter, "config", "list"], capture_output=True,
|
||||||
|
encoding="utf-8", errors="replace", timeout=5
|
||||||
|
)
|
||||||
|
if "weibo" in r.stdout:
|
||||||
|
print(" ✅ Weibo MCP already configured")
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Install from our fork (has visitor passport auth fix)
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
[sys.executable, "-m", "pip", "install", "-q",
|
||||||
|
"git+https://github.com/Panniantong/mcp-server-weibo.git"],
|
||||||
|
check=True, timeout=120
|
||||||
|
)
|
||||||
|
print(" ✅ mcp-server-weibo installed (Panniantong fork)")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [!] mcp-server-weibo install failed: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Register with mcporter
|
||||||
|
if mcporter:
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
[mcporter, "config", "add", "weibo", "--command", "mcp-server-weibo"],
|
||||||
|
check=True, capture_output=True, timeout=10
|
||||||
|
)
|
||||||
|
print(" ✅ Weibo MCP registered with mcporter")
|
||||||
|
except Exception:
|
||||||
|
print(" [!] mcporter config add failed. Run manually: mcporter config add weibo --command 'mcp-server-weibo'")
|
||||||
|
else:
|
||||||
|
print(" -- mcporter not found, skipping MCP registration. Install mcporter first, then run: mcporter config add weibo --command 'mcp-server-weibo'")
|
||||||
|
|
||||||
|
|
||||||
|
def _install_wechat_deps():
|
||||||
|
"""Install WeChat article reading and search dependencies."""
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
print("Setting up WeChat article tools...")
|
||||||
|
|
||||||
|
# Check if already installed
|
||||||
|
has_camoufox = False
|
||||||
|
has_miku = False
|
||||||
|
try:
|
||||||
|
import camoufox # noqa: F401
|
||||||
|
has_camoufox = True
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
import miku_ai # noqa: F401
|
||||||
|
has_miku = True
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Install Python packages
|
||||||
|
if has_camoufox and has_miku:
|
||||||
|
print(" ✅ WeChat Python packages already installed")
|
||||||
|
else:
|
||||||
|
pkgs = []
|
||||||
|
if not has_camoufox:
|
||||||
|
pkgs.extend(["camoufox[geoip]", "markdownify", "beautifulsoup4", "httpx"])
|
||||||
|
if not has_miku:
|
||||||
|
pkgs.append("miku_ai")
|
||||||
|
try:
|
||||||
|
cmd = [sys.executable, "-m", "pip", "install", "--break-system-packages", "-q"] + pkgs
|
||||||
|
subprocess.run(cmd, capture_output=True, encoding="utf-8", errors="replace", timeout=120)
|
||||||
|
# Verify
|
||||||
|
ok = True
|
||||||
|
try:
|
||||||
|
import importlib
|
||||||
|
if not has_camoufox:
|
||||||
|
importlib.import_module("camoufox")
|
||||||
|
if not has_miku:
|
||||||
|
importlib.import_module("miku_ai")
|
||||||
|
except ImportError:
|
||||||
|
ok = False
|
||||||
|
if ok:
|
||||||
|
print(f" ✅ WeChat Python packages installed ({', '.join(pkgs)})")
|
||||||
|
else:
|
||||||
|
print(f" [!] Some WeChat packages failed to install. Try: pip install {' '.join(pkgs)}")
|
||||||
|
except Exception:
|
||||||
|
print(f" [!] WeChat packages install failed. Try: pip install {' '.join(pkgs)}")
|
||||||
|
|
||||||
|
# Clone wechat-article-for-ai tool
|
||||||
|
tools_dir = os.path.expanduser("~/.agent-reach/tools")
|
||||||
|
wechat_dir = os.path.join(tools_dir, "wechat-article-for-ai")
|
||||||
|
if os.path.isfile(os.path.join(wechat_dir, "main.py")):
|
||||||
|
print(" ✅ wechat-article-for-ai tool already installed")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
os.makedirs(tools_dir, exist_ok=True)
|
||||||
|
subprocess.run(
|
||||||
|
["git", "clone", "--depth", "1",
|
||||||
|
"https://github.com/Panniantong/wechat-article-for-ai.git", wechat_dir],
|
||||||
|
capture_output=True, encoding="utf-8", errors="replace", timeout=60,
|
||||||
|
)
|
||||||
|
if os.path.isfile(os.path.join(wechat_dir, "main.py")):
|
||||||
|
print(" ✅ wechat-article-for-ai tool installed")
|
||||||
|
else:
|
||||||
|
print(" [!] wechat-article-for-ai clone failed. Try: git clone https://github.com/Panniantong/wechat-article-for-ai.git " + wechat_dir)
|
||||||
|
except Exception:
|
||||||
|
print(" [!] wechat-article-for-ai clone failed. Try: git clone https://github.com/Panniantong/wechat-article-for-ai.git " + wechat_dir)
|
||||||
|
|
||||||
|
|
||||||
|
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"),
|
||||||
|
]
|
||||||
|
|
||||||
|
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"),
|
||||||
|
]
|
||||||
|
|
||||||
|
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 search."""
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
print("Setting up mcporter (search 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, encoding="utf-8", errors="replace", timeout=120,
|
||||||
|
)
|
||||||
|
if shutil.which("mcporter"):
|
||||||
|
print(" ✅ mcporter installed")
|
||||||
|
else:
|
||||||
|
print(" [X] mcporter install failed. Retry: npm install -g mcporter (check network/timeout), or try: npx mcporter@latest list")
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [X] mcporter install failed: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Configure Exa MCP (free, no key needed)
|
||||||
|
try:
|
||||||
|
r = subprocess.run(
|
||||||
|
["mcporter", "config", "list"], capture_output=True, encoding="utf-8", errors="replace", timeout=5
|
||||||
|
)
|
||||||
|
if "exa" not in r.stdout:
|
||||||
|
subprocess.run(
|
||||||
|
["mcporter", "config", "add", "exa", "https://mcp.exa.ai/mcp"],
|
||||||
|
capture_output=True, encoding="utf-8", errors="replace", 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")
|
||||||
|
|
||||||
|
# NOTE: xhs-cli is now optional, installed via --channels=xiaohongshu
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
with open(cloud_file) as f:
|
||||||
|
content = f.read().lower()
|
||||||
|
if any(x in content for x in ["amazon", "google", "microsoft", "digitalocean", "linode", "vultr", "hetzner"]):
|
||||||
|
indicators += 2
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# systemd-detect-virt
|
||||||
|
try:
|
||||||
|
import subprocess
|
||||||
|
result = subprocess.run(["systemd-detect-virt"], capture_output=True, encoding="utf-8", errors="replace", timeout=3)
|
||||||
|
if result.returncode == 0 and result.stdout.strip() != "none":
|
||||||
|
indicators += 1
|
||||||
|
except Exception:
|
||||||
|
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 <key> <value>")
|
||||||
|
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("bilibili_proxy", value)
|
||||||
|
print(f"✅ Proxy configured for Bilibili!")
|
||||||
|
print(" Note: Reddit 已改为通过 rdt-cli 访问,无需代理。")
|
||||||
|
|
||||||
|
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, ct0 = _parse_twitter_cookie_input(value)
|
||||||
|
|
||||||
|
if auth_token and ct0:
|
||||||
|
config.set("twitter_auth_token", auth_token)
|
||||||
|
config.set("twitter_ct0", ct0)
|
||||||
|
|
||||||
|
# Sync credentials to twitter-cli env
|
||||||
|
print("✅ Twitter cookies configured!")
|
||||||
|
|
||||||
|
print("Testing Twitter access...", end=" ")
|
||||||
|
try:
|
||||||
|
import subprocess
|
||||||
|
twitter_bin = shutil.which("twitter")
|
||||||
|
if not twitter_bin:
|
||||||
|
print("[!] twitter-cli not installed. Run: pipx install twitter-cli")
|
||||||
|
else:
|
||||||
|
import os
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["TWITTER_AUTH_TOKEN"] = auth_token
|
||||||
|
env["TWITTER_CT0"] = ct0
|
||||||
|
result = subprocess.run(
|
||||||
|
[twitter_bin, "status"],
|
||||||
|
capture_output=True, encoding="utf-8", errors="replace", timeout=15,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
output = (result.stdout or "") + (result.stderr or "")
|
||||||
|
if "ok: true" in output:
|
||||||
|
print("✅ Twitter access works!")
|
||||||
|
else:
|
||||||
|
print("[!] Auth check failed (cookies might be wrong)")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[X] Failed: {e}")
|
||||||
|
else:
|
||||||
|
print("[X] 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 == "xhs-cookies":
|
||||||
|
_configure_xhs_cookies(value)
|
||||||
|
|
||||||
|
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 _parse_twitter_cookie_input(value: str):
|
||||||
|
"""Parse Twitter cookie input from either separate values or a cookie header."""
|
||||||
|
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]
|
||||||
|
|
||||||
|
return auth_token, ct0
|
||||||
|
|
||||||
|
|
||||||
|
def _configure_xhs_cookies(value):
|
||||||
|
"""Import cookies for xhs-cli, with legacy xiaohongshu-mcp support.
|
||||||
|
|
||||||
|
Accepts two formats:
|
||||||
|
1. Cookie-Editor JSON export (array of cookie objects)
|
||||||
|
2. Header String: "name1=value1; name2=value2; ..."
|
||||||
|
|
||||||
|
xhs-cli stores cookies as a name/value dict at ~/.xiaohongshu-cli/cookies.json.
|
||||||
|
The legacy Docker MCP stores Cookie-Editor style arrays at $COOKIES_PATH.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
|
||||||
|
value = value.strip()
|
||||||
|
if not value:
|
||||||
|
print("[X] Missing cookie value.")
|
||||||
|
print(" Usage: agent-reach configure xhs-cookies '<cookie JSON or header string>'")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Detect format and parse
|
||||||
|
cookies_json = None
|
||||||
|
cookies = []
|
||||||
|
|
||||||
|
# Try JSON format first (Cookie-Editor JSON export)
|
||||||
|
if value.startswith("["):
|
||||||
|
try:
|
||||||
|
parsed = json.loads(value)
|
||||||
|
if isinstance(parsed, list) and parsed:
|
||||||
|
# Validate it looks like cookie objects
|
||||||
|
first = parsed[0]
|
||||||
|
if isinstance(first, dict) and "name" in first and "value" in first:
|
||||||
|
cookies = parsed
|
||||||
|
cookies_json = json.dumps(parsed)
|
||||||
|
print(f" Parsed {len(parsed)} cookies from JSON format")
|
||||||
|
else:
|
||||||
|
print("[X] JSON array doesn't contain cookie objects (need name/value fields)")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
print("[X] Empty or invalid JSON array")
|
||||||
|
return
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"[X] Invalid JSON: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Header String format: "key1=val1; key2=val2; ..."
|
||||||
|
if cookies_json is None and "=" in value:
|
||||||
|
for part in value.split(";"):
|
||||||
|
part = part.strip()
|
||||||
|
if "=" not in part:
|
||||||
|
continue
|
||||||
|
name, val = part.split("=", 1)
|
||||||
|
name = name.strip()
|
||||||
|
val = val.strip()
|
||||||
|
if name:
|
||||||
|
cookies.append({
|
||||||
|
"name": name,
|
||||||
|
"value": val,
|
||||||
|
"domain": ".xiaohongshu.com",
|
||||||
|
"path": "/",
|
||||||
|
"expires": -1,
|
||||||
|
"size": len(name) + len(val),
|
||||||
|
"httpOnly": False,
|
||||||
|
"secure": False,
|
||||||
|
"session": True,
|
||||||
|
"sameSite": "Lax",
|
||||||
|
})
|
||||||
|
if cookies:
|
||||||
|
cookies_json = json.dumps(cookies)
|
||||||
|
print(f" Parsed {len(cookies)} cookies from Header String format")
|
||||||
|
else:
|
||||||
|
print("[X] Could not parse any cookies from input")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not cookies_json:
|
||||||
|
print("[X] Could not parse cookies. Accepted formats:")
|
||||||
|
print(' 1. JSON array: \'[{"name":"x","value":"y","domain":".xiaohongshu.com",...}]\'')
|
||||||
print(' 2. Header String: "key1=val1; key2=val2; ..."')
|
print(' 2. Header String: "key1=val1; key2=val2; ..."')
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user