Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,25 @@ We use the following tools to maintain code quality:
Run all checks before submitting a PR:

```bash
# Linting
# Linting and import sorting
ruff check agent_reach tests
ruff format agent_reach tests

# Type checking
mypy agent_reach

# Tests
pytest
# Unit tests
pytest tests/

# Clean-environment CLI smoke test
bash test.sh

# Package build
python -m build
```

`test.sh` intentionally verifies the supported installer/doctor/version/API contract. Agent Reach is not a `read`/`search` wrapper CLI; after installation, agents should call the upstream tools directly as documented in the generated skill.

## Adding New Channels

Agent Reach uses a unified channel interface. To add a new platform:
Expand Down
3 changes: 2 additions & 1 deletion agent_reach/channels/twitter.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# -*- coding: utf-8 -*-
"""Twitter/X — check if twitter-cli or bird CLI is available."""

from .base import Channel
from agent_reach.probe import probe_command

from .base import Channel


class TwitterChannel(Channel):
name = "twitter"
Expand Down
1 change: 1 addition & 0 deletions agent_reach/channels/v2ex.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import json
import urllib.request
from typing import Any

from .base import Channel

_UA = "agent-reach/1.0"
Expand Down
1 change: 1 addition & 0 deletions agent_reach/channels/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"""Web — any URL via Jina Reader. Always available."""

import urllib.request

from .base import Channel

_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
Expand Down
2 changes: 2 additions & 0 deletions agent_reach/channels/xiaoyuzhou.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
"""Xiaoyuzhou Podcast (小宇宙播客) — transcribe podcasts via Groq Whisper API."""

import os

from agent_reach.config import Config
from agent_reach.probe import probe_command

from .base import Channel


Expand Down
67 changes: 37 additions & 30 deletions agent_reach/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
agent-reach setup
"""

import sys
import argparse
import json
import os
import sys
import time

from agent_reach import __version__
Expand Down Expand Up @@ -171,6 +171,7 @@ def main():
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

Expand Down Expand Up @@ -223,9 +224,9 @@ def _cmd_install(args):
env = _detect_environment()

if env == "server":
print(f"Environment: Server/VPS (auto-detected)")
print("Environment: Server/VPS (auto-detected)")
else:
print(f"Environment: Local computer (auto-detected)")
print("Environment: Local computer (auto-detected)")

server_skipped_opencli_channels = set()
if env == "server" and requested_channels:
Expand All @@ -236,11 +237,11 @@ def _cmd_install(args):
# Apply explicit flags
if args.proxy:
if dry_run:
print(f"[dry-run] Would save network proxy")
print("[dry-run] Would save network proxy")
else:
config.set("proxy", args.proxy)
config.set("bilibili_proxy", args.proxy) # legacy key
print(f"✅ 代理已保存(Agent 访问受限网络时使用)")
print("✅ 代理已保存(Agent 访问受限网络时使用)")

# ── Install core system dependencies (lightweight, always) ──
print()
Expand Down Expand Up @@ -289,15 +290,15 @@ def _cmd_install(args):
print(" it only happens once during install. Enter your password or click 'Allow'.)")
try:
from agent_reach.cookie_extract import configure_from_browser
results = configure_from_browser("chrome", config)
browser_results = configure_from_browser("chrome", config)
found = False
for platform, success, message in results:
for platform, success, message in browser_results:
if success:
print(f" ✅ {platform}: {message}")
found = True
if not found:
results = configure_from_browser("firefox", config)
for platform, success, message in results:
browser_results = configure_from_browser("firefox", config)
for platform, success, message in browser_results:
if success:
print(f" ✅ {platform}: {message}")
found = True
Expand All @@ -321,13 +322,13 @@ def _cmd_install(args):
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)
channel_results = check_all(config)
ok = sum(1 for r in channel_results.values() if r["status"] == "ok")
total = len(channel_results)

# Final status
print()
print(format_report(results))
print(format_report(channel_results))
print()

# ── Install agent skill ──
Expand All @@ -354,9 +355,9 @@ def _cmd_install(args):

def _install_skill(force: bool = True):
"""Install Agent Reach as an agent skill (OpenClaw / Claude Code / .agents)."""
import importlib.resources
import os
import shutil
import importlib.resources

def _is_english_locale(value: str) -> bool:
normalized = value.strip().lower()
Expand Down Expand Up @@ -531,9 +532,9 @@ def _cmd_format(args):

def _install_system_deps():
"""Install system-level dependencies: gh CLI, Node.js (for mcporter)."""
import platform
import shutil
import subprocess
import platform
import tempfile

print("Checking system dependencies...")
Expand Down Expand Up @@ -659,6 +660,7 @@ def _install_system_deps():
def _install_xiaoyuzhou_deps():
"""Install Xiaoyuzhou podcast transcription script."""
import shutil

from agent_reach.config import Config

config = Config()
Expand Down Expand Up @@ -1019,6 +1021,7 @@ def _detect_environment():
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()
Expand Down Expand Up @@ -1121,15 +1124,15 @@ def _cmd_configure(args):

elif args.key == "github-token":
config.set("github_token", value)
print(f"✅ GitHub token configured!")
print("✅ GitHub token configured!")

elif args.key == "groq-key":
config.set("groq_api_key", value)
print(f"✅ Groq key configured!")
print("✅ Groq key configured!")

elif args.key == "openai-key":
config.set("openai_api_key", value)
print(f"✅ OpenAI key configured!")
print("✅ OpenAI key configured!")


def _cmd_transcribe(args):
Expand Down Expand Up @@ -1476,18 +1479,22 @@ def _cmd_uninstall(args):
def _cmd_doctor(args=None):
from agent_reach.config import Config
from agent_reach.doctor import check_all, format_report
rich_module: object | None
try:
from rich import print as rprint
import rich as rich_module
except ImportError:
rprint = print
rich_module = None
config = Config()
results = check_all(config)

if args is not None and getattr(args, "json", False):
print(json.dumps(results, ensure_ascii=False, indent=2))
return

rprint(format_report(results))
if rich_module is not None:
getattr(rich_module, "print")(format_report(results))
else:
print(format_report(results))

# Auto-install skill if not already present (fixes #154)
_install_skill(force=False)
Expand Down Expand Up @@ -1545,7 +1552,7 @@ def _cmd_setup():
print(" 获取: https://github.com/settings/tokens (无需任何权限)")
current = config.get("github_token")
if current:
print(f" 当前状态: ✅ 已配置")
print(" 当前状态: ✅ 已配置")
else:
key = input(" GITHUB_TOKEN (回车跳过): ").strip()
if key:
Expand All @@ -1566,7 +1573,7 @@ def _cmd_setup():
print(" 免费额度,注册: https://console.groq.com")
current = config.get("groq_api_key")
if current:
print(f" 当前状态: ✅ 已配置")
print(" 当前状态: ✅ 已配置")
else:
key = input(" GROQ_API_KEY (回车跳过): ").strip()
if key:
Expand Down Expand Up @@ -1697,10 +1704,10 @@ def parse(v):
except ValueError:
return None

r, l = parse(remote), parse(local)
if r is None or l is None:
remote_version, local_version = parse(remote), parse(local)
if remote_version is None or local_version is None:
return remote != local # unparseable — fall back to old behavior
return r > l
return remote_version > local_version


def _cmd_check_update():
Expand Down Expand Up @@ -1733,7 +1740,7 @@ def _cmd_check_update():
print()
print(_UPDATE_INSTRUCTIONS)
return "update_available"
print(f"✅ 已是最新版本")
print("✅ 已是最新版本")
return "up_to_date"

release_err = _classify_github_response_error(resp)
Expand Down Expand Up @@ -1770,9 +1777,9 @@ def _cmd_watch():

Only outputs problems. If everything is fine, outputs a single line.
"""
from agent_reach import __version__
from agent_reach.config import Config
from agent_reach.doctor import check_all
from agent_reach import __version__

config = Config()
issues = []
Expand Down Expand Up @@ -1811,8 +1818,8 @@ def _cmd_watch():
print(f"Agent Reach: 全部正常 ({ok}/{total} 渠道可用,v{__version__} 已是最新)")
return

print(f"Agent Reach 监控报告")
print(f"=" * 40)
print("Agent Reach 监控报告")
print("=" * 40)
print(f"版本: v{__version__} | 渠道: {ok}/{total}")

if issues:
Expand Down
4 changes: 2 additions & 2 deletions agent_reach/cookie_extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
agent-reach configure --from-browser chrome
"""

from typing import Dict, List, Tuple
from typing import Any, Dict, List, Tuple

# Platform cookie specs: (platform_name, domain_pattern, needed_cookies)
PLATFORM_SPECS = [
PLATFORM_SPECS: List[Dict[str, Any]] = [
{
"name": "Twitter/X",
"domains": [".x.com", ".twitter.com"],
Expand Down
12 changes: 8 additions & 4 deletions agent_reach/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
"""

from typing import Dict
from agent_reach.config import Config

from agent_reach.channels import get_all_channels
from agent_reach.config import Config


def check_all(config: Config) -> Dict[str, dict]:
Expand Down Expand Up @@ -47,9 +48,13 @@ def _name_msg(r: dict, escape) -> str:
def format_report(results: Dict[str, dict]) -> str:
"""Format results as a readable text report (with Rich markup)."""
try:
from rich.markup import escape
from rich.markup import escape as rich_escape

def escape(text: str) -> str:
return rich_escape(text)
except ImportError:
escape = lambda x: x
def escape(text: str) -> str:
return text

lines = []
lines.append("[bold cyan]Agent Reach 状态[/bold cyan]")
Expand Down Expand Up @@ -107,7 +112,6 @@ def format_report(results: Dict[str, dict]) -> str:
)

# Security check: config file permissions (Unix only)
import os
import stat
import sys

Expand Down
2 changes: 1 addition & 1 deletion agent_reach/integrations/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
try:
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
from mcp.types import TextContent, Tool
HAS_MCP = True
except ImportError:
HAS_MCP = False
Expand Down
1 change: 1 addition & 0 deletions agent_reach/probe.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ def probe_command(
# failures (timeout/error) are worth a second attempt
if last.status in ("missing", "broken"):
return last
assert last is not None
return last


Expand Down
Loading