Agent-Reach/agent_eyes/cli.py
Panniantong 62b82c5a52 feat: deterministic install & configure commands
New commands:
- agent-eyes install --env=<local|server> --search=<yes|no> [--proxy=URL] [--exa-key=KEY]
  One-shot installer with explicit flags. No ambiguity.

- agent-eyes configure <key> <value>
  Set exa-key/proxy/github-token/groq-key with auto-testing.
  e.g. 'agent-eyes configure exa-key xxx' → saves + tests API

Rewrote install.md as strict decision tree:
1. Ask 3 questions → get flags
2. pip install
3. Run ONE install command with flags
4. Configure keys with configure command (auto-tests each)
5. Verify with doctor

Inspired by oh-my-opencode's deterministic installer pattern.
2026-02-24 06:16:52 +01:00

392 lines
13 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""
Agent Eyes CLI — command-line interface.
Usage:
agent-eyes read <url>
agent-eyes search <query>
agent-eyes search-reddit <query> [--sub <subreddit>]
agent-eyes search-github <query> [--lang <language>]
agent-eyes search-twitter <query>
agent-eyes setup
agent-eyes doctor
agent-eyes version
"""
import sys
import asyncio
import argparse
import json
import os
from agent_eyes 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-eyes",
description="👁️ Give your AI Agent eyes to see the entire internet",
)
parser.add_argument("-v", "--verbose", action="store_true", help="Show debug logs")
sub = parser.add_subparsers(dest="command", help="Available commands")
# ── read ──
p_read = sub.add_parser("read", help="Read content from a URL")
p_read.add_argument("url", help="URL to read")
p_read.add_argument("--json", dest="as_json", action="store_true", help="Output as JSON")
# ── search ──
p_search = sub.add_parser("search", help="Search the web (Exa)")
p_search.add_argument("query", nargs="+", help="Search query")
p_search.add_argument("-n", "--num", type=int, default=5, help="Number of results")
# ── search-reddit ──
p_sr = sub.add_parser("search-reddit", help="Search Reddit")
p_sr.add_argument("query", nargs="+", help="Search query")
p_sr.add_argument("--sub", help="Subreddit filter")
p_sr.add_argument("-n", "--num", type=int, default=10, help="Number of results")
# ── search-github ──
p_sg = sub.add_parser("search-github", help="Search GitHub")
p_sg.add_argument("query", nargs="+", help="Search query")
p_sg.add_argument("--lang", help="Language filter")
p_sg.add_argument("-n", "--num", type=int, default=5, help="Number of results")
# ── search-twitter ──
p_st = sub.add_parser("search-twitter", help="Search Twitter")
p_st.add_argument("query", nargs="+", help="Search query")
p_st.add_argument("-n", "--num", type=int, default=10, help="Number of results")
# ── 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"], default="local",
help="Environment: local computer or server/VPS")
p_install.add_argument("--search", choices=["yes", "no"], default="yes",
help="Enable web search (needs free Exa API key)")
p_install.add_argument("--proxy", default="",
help="Residential proxy for Reddit/Bilibili (http://user:pass@ip:port)")
p_install.add_argument("--exa-key", default="",
help="Exa API key (get free at https://exa.ai)")
# ── configure ──
p_conf = sub.add_parser("configure", help="Set a config value")
p_conf.add_argument("key", choices=["exa-key", "proxy", "github-token", "groq-key"],
help="What to configure")
p_conf.add_argument("value", help="The value to set")
# ── doctor ──
sub.add_parser("doctor", help="Check platform availability")
# ── 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 Eyes v{__version__}")
sys.exit(0)
if args.command == "doctor":
_cmd_doctor()
elif args.command == "setup":
_cmd_setup()
elif args.command == "install":
_cmd_install(args)
elif args.command == "configure":
_cmd_configure(args)
elif args.command == "read":
asyncio.run(_cmd_read(args))
elif args.command.startswith("search"):
asyncio.run(_cmd_search(args))
# ── Command handlers ────────────────────────────────
def _cmd_install(args):
"""One-shot deterministic installer."""
from agent_eyes.config import Config
from agent_eyes.doctor import check_all, format_report
config = Config()
print()
print("👁️ Agent Eyes Installer")
print("=" * 40)
# Apply flags
if args.exa_key:
config.set("exa_api_key", args.exa_key)
print(f"✅ Exa search key configured")
if args.proxy:
config.set("reddit_proxy", args.proxy)
config.set("bilibili_proxy", args.proxy)
print(f"✅ Proxy configured for Reddit + Bilibili")
# Environment-specific advice
if args.env == "server":
print(f"📡 Environment: Server/VPS")
if not args.proxy:
print(f"⚠️ Reddit and Bilibili block server IPs.")
print(f" To unlock: agent-eyes configure proxy http://user:pass@ip:port")
print(f" Recommend: https://www.webshare.io ($1/month)")
else:
print(f"💻 Environment: Local computer")
# Test zero-config features
print()
print("Testing channels...")
results = check_all(config)
ok = sum(1 for r in results.values() if r["status"] == "ok")
total = len(results)
print(f"{ok}/{total} channels active")
# What's missing
if args.search == "yes" and not args.exa_key:
print()
print("🔍 Search not yet configured. Run:")
print(" agent-eyes configure exa-key YOUR_KEY")
print(" (Get free key: https://exa.ai)")
# Final status
print()
print(format_report(results))
print()
print("✅ Installation complete!")
if ok < total:
print(f" Run `agent-eyes configure` to unlock remaining channels.")
def _cmd_configure(args):
"""Set a config value and test it."""
from agent_eyes.config import Config
import subprocess
config = Config()
key_map = {
"exa-key": "exa_api_key",
"proxy": ("reddit_proxy", "bilibili_proxy"),
"github-token": "github_token",
"groq-key": "groq_api_key",
}
config_key = key_map.get(args.key)
if isinstance(config_key, tuple):
for k in config_key:
config.set(k, args.value)
else:
config.set(config_key, args.value)
print(f"{args.key} configured!")
# Auto-test
if args.key == "exa-key":
print("Testing search...", end=" ")
try:
import asyncio
from agent_eyes.core import AgentEyes
eyes = AgentEyes(config)
results = asyncio.run(eyes.search("test", num_results=1))
if results:
print("✅ Search works!")
else:
print("⚠️ No results, but API connected.")
except Exception as e:
print(f"❌ Failed: {e}")
elif args.key == "proxy":
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"},
proxies={"http": args.value, "https": args.value},
timeout=10,
)
if resp.status_code == 200:
print("✅ Reddit accessible!")
else:
print(f"❌ Reddit returned {resp.status_code}")
except Exception as e:
print(f"❌ Failed: {e}")
def _cmd_doctor():
from agent_eyes.config import Config
from agent_eyes.doctor import check_all, format_report
config = Config()
results = check_all(config)
print(format_report(results))
def _cmd_setup():
from agent_eyes.config import Config
config = Config()
print()
print("👁️ Agent Eyes 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-eyes 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-eyes doctor 查看完整状态")
print()
async def _cmd_read(args):
from agent_eyes.core import AgentEyes
eyes = AgentEyes()
try:
result = await eyes.read(args.url)
if args.as_json:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
print(f"\n📖 {result.get('title', 'Untitled')}")
print(f"🔗 {result.get('url', '')}")
if result.get("author"):
print(f"👤 {result['author']}")
print(f"\n{result.get('content', '')}")
except Exception as e:
print(f"❌ Error: {e}", file=sys.stderr)
sys.exit(1)
async def _cmd_search(args):
from agent_eyes.core import AgentEyes
eyes = AgentEyes()
query = " ".join(args.query)
num = args.num
try:
if args.command == "search":
results = await eyes.search(query, num_results=num)
elif args.command == "search-reddit":
results = await eyes.search_reddit(query, subreddit=getattr(args, "sub", None), limit=num)
elif args.command == "search-github":
results = await eyes.search_github(query, language=getattr(args, "lang", None), limit=num)
elif args.command == "search-twitter":
results = await eyes.search_twitter(query, limit=num)
else:
print(f"Unknown command: {args.command}", file=sys.stderr)
sys.exit(1)
if not results:
print("No results found.")
return
for i, r in enumerate(results, 1):
title = r.get("title") or r.get("name") or r.get("text", "")[:60]
url = r.get("url", "")
snippet = r.get("snippet") or r.get("description") or r.get("text", "")
print(f"\n{i}. {title}")
print(f" 🔗 {url}")
if snippet:
print(f" {snippet[:200]}")
# Extra info for GitHub
if "stars" in r:
print(f"{r['stars']} 🍴 {r.get('forks', 0)} 📝 {r.get('language', '')}")
except ValueError as e:
print(f"⚠️ {e}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"❌ Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()