feat(twitter): migrate from xreach to bird CLI
- Replace xreach CLI with bird (@steipete/bird) as Twitter/X backend - bird uses AUTH_TOKEN/CT0 env vars (simpler than xreach's session.json) - Accept both 'bird' and 'birdx' binary names - Remove version detection logic (bird v0.8.0 is the baseline) - Write credentials.env to ~/.config/bird/ for easy sourcing - Keep xfetch session.json sync for backward compatibility - Update SKILL.md commands: bird search/read/user-tweets/thread - Update install/uninstall to use npm @steipete/bird - All 52 tests pass
This commit is contained in:
@@ -1,76 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Twitter/X — check if xreach CLI is available."""
|
||||
"""Twitter/X — check if bird CLI (@steipete/bird) is available."""
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from .base import Channel
|
||||
|
||||
# Minimum xreach-cli version with longform tweet and X Article support.
|
||||
# v0.3.2 added: extractTweetText() preferring note_tweet for long tweets (#aad6a16)
|
||||
# and X Article URL support (/article/ path, #2e05825).
|
||||
_MIN_XREACH_VERSION = (0, 3, 2)
|
||||
|
||||
|
||||
def _parse_version(ver_str: str) -> tuple[int, ...]:
|
||||
"""Parse a semver string like '0.3.2' into a tuple (0, 3, 2)."""
|
||||
try:
|
||||
return tuple(int(x) for x in ver_str.strip().split(".")[:3])
|
||||
except (ValueError, AttributeError):
|
||||
return (0, 0, 0)
|
||||
|
||||
|
||||
def _detect_xreach_version(xreach_path: str) -> str:
|
||||
"""Best-effort xreach version detection.
|
||||
|
||||
Some xreach-cli releases ship package.json@0.3.2 while `xreach --version`
|
||||
still prints 0.3.0 because the embedded dist version file was not updated.
|
||||
Prefer the newer of:
|
||||
1) `xreach --version`
|
||||
2) `npm list -g xreach-cli --json --depth=0`
|
||||
"""
|
||||
versions: list[str] = []
|
||||
|
||||
try:
|
||||
ver_result = subprocess.run(
|
||||
[xreach_path, "--version"], capture_output=True,
|
||||
encoding="utf-8", errors="replace", timeout=5
|
||||
)
|
||||
version_str = (ver_result.stdout or ver_result.stderr).strip()
|
||||
if version_str:
|
||||
versions.append(version_str)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
npm = shutil.which("npm")
|
||||
if npm:
|
||||
try:
|
||||
npm_result = subprocess.run(
|
||||
[npm, "list", "-g", "xreach-cli", "--json", "--depth=0"],
|
||||
capture_output=True, encoding="utf-8", errors="replace", timeout=10,
|
||||
)
|
||||
if npm_result.returncode == 0 and npm_result.stdout:
|
||||
data = json.loads(npm_result.stdout)
|
||||
npm_ver = (
|
||||
data.get("dependencies", {})
|
||||
.get("xreach-cli", {})
|
||||
.get("version", "")
|
||||
.strip()
|
||||
)
|
||||
if npm_ver:
|
||||
versions.append(npm_ver)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not versions:
|
||||
return ""
|
||||
return max(versions, key=_parse_version)
|
||||
|
||||
|
||||
class TwitterChannel(Channel):
|
||||
name = "twitter"
|
||||
description = "Twitter/X 推文"
|
||||
backends = ["xreach CLI"]
|
||||
backends = ["bird CLI"]
|
||||
tier = 1
|
||||
|
||||
def can_handle(self, url: str) -> bool:
|
||||
@@ -79,36 +18,33 @@ class TwitterChannel(Channel):
|
||||
return "x.com" in d or "twitter.com" in d
|
||||
|
||||
def check(self, config=None):
|
||||
xreach = shutil.which("xreach")
|
||||
if not xreach:
|
||||
bird = shutil.which("bird") or shutil.which("birdx")
|
||||
if not bird:
|
||||
return "warn", (
|
||||
"xreach CLI 未安装。搜索可通过 Exa 替代。安装:\n"
|
||||
" npm install -g xreach-cli"
|
||||
"bird CLI 未安装。搜索可通过 Exa 替代。安装:\n"
|
||||
" npm install -g @steipete/bird"
|
||||
)
|
||||
# Check version — longform tweet support requires >= 0.3.2
|
||||
try:
|
||||
version_str = _detect_xreach_version(xreach)
|
||||
version_tuple = _parse_version(version_str)
|
||||
if version_str and version_tuple < _MIN_XREACH_VERSION:
|
||||
min_str = ".".join(str(x) for x in _MIN_XREACH_VERSION)
|
||||
return "warn", (
|
||||
f"xreach CLI 版本过旧(当前 {version_str},需 >= {min_str})。"
|
||||
f"旧版本无法读取长文推文(note_tweet)和 X Article。升级:\n"
|
||||
f" npm install -g xreach-cli@latest"
|
||||
)
|
||||
except Exception:
|
||||
pass # version check failure is non-fatal; proceed to auth check
|
||||
|
||||
try:
|
||||
r = subprocess.run(
|
||||
[xreach, "auth", "check"], capture_output=True,
|
||||
[bird, "check"], capture_output=True,
|
||||
encoding="utf-8", errors="replace", timeout=10
|
||||
)
|
||||
output = (r.stdout or "") + (r.stderr or "")
|
||||
if r.returncode == 0:
|
||||
return "ok", "完整可用(读取、搜索推文,含长文/X Article)"
|
||||
# bird check returns 1 when auth is missing
|
||||
if "Missing credentials" in output or "missing" in output.lower():
|
||||
return "warn", (
|
||||
"bird CLI 已安装但未配置认证。设置环境变量:\n"
|
||||
" export AUTH_TOKEN=\"xxx\"\n"
|
||||
" export CT0=\"yyy\"\n"
|
||||
"或运行:\n"
|
||||
" agent-reach configure twitter-cookies \"auth_token=xxx; ct0=yyy\""
|
||||
)
|
||||
return "warn", (
|
||||
"xreach CLI 已安装但未配置 Cookie。运行:\n"
|
||||
"bird CLI 已安装但认证检查失败。运行:\n"
|
||||
" agent-reach configure twitter-cookies \"auth_token=xxx; ct0=yyy\""
|
||||
)
|
||||
except Exception:
|
||||
return "warn", "xreach CLI 已安装但连接失败"
|
||||
return "warn", "bird CLI 已安装但连接失败"
|
||||
|
||||
+31
-20
@@ -395,24 +395,24 @@ def _install_system_deps():
|
||||
except Exception:
|
||||
print(" [!] Node.js install failed. Try: apt install nodejs npm, or nvm install 22, or download from https://nodejs.org")
|
||||
|
||||
# ── xreach CLI (for Twitter search) ──
|
||||
if shutil.which("xreach"):
|
||||
print(" ✅ xreach CLI already installed")
|
||||
# ── 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", "xreach-cli"],
|
||||
["npm", "install", "-g", "@steipete/bird"],
|
||||
capture_output=True, encoding="utf-8", errors="replace", timeout=120,
|
||||
)
|
||||
if shutil.which("xreach"):
|
||||
print(" ✅ xreach CLI installed (Twitter search + timeline)")
|
||||
if shutil.which("bird") or shutil.which("birdx"):
|
||||
print(" ✅ bird CLI installed (Twitter search + timeline)")
|
||||
else:
|
||||
print(" -- xreach CLI install failed (optional — Twitter reading still works via Jina)")
|
||||
print(" -- bird CLI install failed (optional — Twitter reading still works via Jina)")
|
||||
except Exception:
|
||||
print(" -- xreach CLI install failed (optional — Twitter reading still works via Jina)")
|
||||
print(" -- bird CLI install failed (optional — Twitter reading still works via Jina)")
|
||||
else:
|
||||
print(" -- xreach CLI requires Node.js (optional — Twitter reading still works via Jina)")
|
||||
print(" -- bird CLI requires Node.js (optional — Twitter reading still works via Jina)")
|
||||
|
||||
# ── undici (proxy support for Node.js fetch) ──
|
||||
npm_cmd = shutil.which("npm")
|
||||
@@ -426,7 +426,7 @@ def _install_system_deps():
|
||||
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 — xreach may not work behind proxies)")
|
||||
print(" -- undici install failed (optional — bird may not work behind proxies)")
|
||||
|
||||
# ── yt-dlp JS runtime config (YouTube requires external JS runtime) ──
|
||||
if shutil.which("node"):
|
||||
@@ -626,7 +626,7 @@ def _install_system_deps_safe():
|
||||
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"),
|
||||
("xreach", ["xreach"], "xreach CLI (Twitter)", "npm install -g xreach-cli"),
|
||||
("bird", ["bird", "birdx"], "bird CLI (Twitter)", "npm install -g @steipete/bird"),
|
||||
]
|
||||
|
||||
missing = []
|
||||
@@ -679,7 +679,7 @@ def _install_system_deps_dryrun():
|
||||
checks = [
|
||||
("gh CLI", ["gh"], "apt install gh / brew install gh"),
|
||||
("Node.js", ["node"], "curl NodeSource setup | bash + apt install nodejs"),
|
||||
("xreach CLI", ["xreach"], "npm install -g xreach-cli"),
|
||||
("bird CLI", ["bird", "birdx"], "npm install -g @steipete/bird"),
|
||||
]
|
||||
|
||||
for label, binaries, method in checks:
|
||||
@@ -924,9 +924,10 @@ def _cmd_configure(args):
|
||||
config.set("twitter_auth_token", auth_token)
|
||||
config.set("twitter_ct0", ct0)
|
||||
|
||||
# Sync credentials to xreach's session.json so xreach auth check works
|
||||
# Sync credentials to bird CLI env
|
||||
try:
|
||||
import json
|
||||
# Legacy: sync to xfetch session.json for backward compat
|
||||
xfetch_dir = os.path.join(os.path.expanduser("~"), ".config", "xfetch")
|
||||
os.makedirs(xfetch_dir, exist_ok=True)
|
||||
session_path = os.path.join(xfetch_dir, "session.json")
|
||||
@@ -939,24 +940,34 @@ def _cmd_configure(args):
|
||||
with open(session_path, "w", encoding="utf-8") as sf:
|
||||
json.dump(session_data, sf, indent=2)
|
||||
os.chmod(session_path, 0o600)
|
||||
print("✅ Twitter cookies configured (synced to xreach)!")
|
||||
|
||||
# bird CLI: write shell-sourceable credentials.env
|
||||
bird_dir = os.path.join(os.path.expanduser("~"), ".config", "bird")
|
||||
os.makedirs(bird_dir, exist_ok=True)
|
||||
env_path = os.path.join(bird_dir, "credentials.env")
|
||||
with open(env_path, "w", encoding="utf-8") as f:
|
||||
f.write(f'AUTH_TOKEN="{auth_token}"\n')
|
||||
f.write(f'CT0="{ct0}"\n')
|
||||
os.chmod(env_path, 0o600)
|
||||
|
||||
print("✅ Twitter cookies configured (synced to bird)!")
|
||||
except Exception as e:
|
||||
print("✅ Twitter cookies configured!")
|
||||
print(f"[!] Could not sync to xreach session.json: {e}")
|
||||
print(f"[!] Could not sync to bird credentials: {e}")
|
||||
|
||||
print("Testing Twitter access...", end=" ")
|
||||
try:
|
||||
import subprocess
|
||||
xreach = shutil.which("xreach")
|
||||
if not xreach:
|
||||
print("[!] xreach CLI not installed. Run: npm install -g xreach-cli")
|
||||
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(
|
||||
[xreach, "search", "test", "-n", "1"],
|
||||
[bird, "search", "test", "-n", "1"],
|
||||
capture_output=True, encoding="utf-8", errors="replace", timeout=15,
|
||||
env=env,
|
||||
)
|
||||
@@ -1259,7 +1270,7 @@ def _cmd_uninstall(args):
|
||||
print()
|
||||
print("Optional: remove tools installed by Agent Reach:")
|
||||
print(" npm uninstall -g mcporter")
|
||||
print(" npm uninstall -g xreach-cli")
|
||||
print(" npm uninstall -g @steipete/bird")
|
||||
print(" npm uninstall -g undici")
|
||||
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class Config:
|
||||
FEATURE_REQUIREMENTS = {
|
||||
"exa_search": ["exa_api_key"],
|
||||
"reddit_proxy": ["reddit_proxy"],
|
||||
"twitter_xreach": ["twitter_auth_token", "twitter_ct0"],
|
||||
"twitter_xreach": ["twitter_auth_token", "twitter_ct0"], # legacy key name; used by bird CLI
|
||||
"groq_whisper": ["groq_api_key"],
|
||||
"github_token": ["github_token"],
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ def extract_all(browser: str = "chrome") -> Dict[str, dict]:
|
||||
|
||||
|
||||
def _sync_xfetch_session(auth_token: str, ct0: str) -> None:
|
||||
"""Sync Twitter credentials to ~/.config/xfetch/session.json for xreach CLI."""
|
||||
"""Sync Twitter credentials to ~/.config/xfetch/session.json (legacy xreach compat)."""
|
||||
import json
|
||||
import os
|
||||
|
||||
@@ -138,6 +138,31 @@ def _sync_xfetch_session(auth_token: str, ct0: str) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def _sync_bird_env(auth_token: str, ct0: str) -> None:
|
||||
"""Write Twitter credentials to ~/.config/bird/credentials.env for bird CLI.
|
||||
|
||||
bird reads AUTH_TOKEN and CT0 from environment variables. This writes a
|
||||
shell-sourceable file so users can `source ~/.config/bird/credentials.env`.
|
||||
"""
|
||||
import os
|
||||
|
||||
try:
|
||||
bird_dir = os.path.join(os.path.expanduser("~"), ".config", "bird")
|
||||
os.makedirs(bird_dir, exist_ok=True)
|
||||
env_path = os.path.join(bird_dir, "credentials.env")
|
||||
with open(env_path, "w", encoding="utf-8") as f:
|
||||
f.write(f'AUTH_TOKEN="{auth_token}"\n')
|
||||
f.write(f'CT0="{ct0}"\n')
|
||||
os.chmod(env_path, 0o600)
|
||||
except Exception:
|
||||
# Non-fatal: agent-reach config is the source of truth, bird env sync is best-effort
|
||||
pass
|
||||
|
||||
|
||||
# Alias for callers expecting the name _sync_bird_credentials
|
||||
_sync_bird_credentials = _sync_bird_env
|
||||
|
||||
|
||||
def configure_from_browser(browser: str, config) -> List[Tuple[str, bool, str]]:
|
||||
"""
|
||||
Extract cookies and configure all found platforms.
|
||||
@@ -162,7 +187,8 @@ def configure_from_browser(browser: str, config) -> List[Tuple[str, bool, str]]:
|
||||
if "auth_token" in tc and "ct0" in tc:
|
||||
config.set("twitter_auth_token", tc["auth_token"])
|
||||
config.set("twitter_ct0", tc["ct0"])
|
||||
# Sync credentials to xreach's session.json so `xreach auth check` works
|
||||
# Sync credentials to bird CLI env and legacy xfetch session.json
|
||||
_sync_bird_env(tc["auth_token"], tc["ct0"])
|
||||
_sync_xfetch_session(tc["auth_token"], tc["ct0"])
|
||||
results_list.append(("Twitter/X", True, "auth_token + ct0"))
|
||||
else:
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
AgentReach — installer, doctor, and configuration tool.
|
||||
|
||||
Agent Reach helps AI agents install and configure upstream platform tools
|
||||
(xreach CLI, yt-dlp, mcporter, gh CLI, etc.). After installation, agents
|
||||
(bird CLI, yt-dlp, mcporter, gh CLI, etc.). After installation, agents
|
||||
call the upstream tools directly — no wrapper layer needed.
|
||||
|
||||
Usage:
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
# Twitter 高级功能配置指南(xreach CLI)
|
||||
# Twitter 高级功能配置指南(bird CLI)
|
||||
|
||||
Twitter 基础阅读通过 Jina Reader 免费可用,无需配置。
|
||||
|
||||
高级功能需要 xreach CLI:
|
||||
高级功能需要 bird CLI(@steipete/bird):
|
||||
|
||||
- 搜索推文(`xreach search`)
|
||||
- 读取完整推文和对话链(`xreach tweet`、`xreach thread`)
|
||||
- 用户时间线(`xreach tweets`)
|
||||
- 搜索推文(`bird search`)
|
||||
- 读取完整推文和对话链(`bird read`、`bird thread`)
|
||||
- 用户时间线(`bird user-tweets`)
|
||||
|
||||
xreach 是免费开源工具(npm 包 xreach-cli),但需要你的 Twitter 账号 cookie。
|
||||
bird 是免费开源工具(npm 包 @steipete/bird),但需要你的 Twitter 账号 cookie。
|
||||
|
||||
## 快速配置
|
||||
|
||||
1. 检查 xreach 是否安装:
|
||||
1. 检查 bird 是否安装:
|
||||
|
||||
```bash
|
||||
which xreach && echo "installed" || echo "not installed"
|
||||
which bird && echo "installed" || echo "not installed"
|
||||
```
|
||||
|
||||
2. 安装 xreach:
|
||||
2. 安装 bird:
|
||||
|
||||
```bash
|
||||
npm install -g xreach-cli
|
||||
npm install -g @steipete/bird
|
||||
```
|
||||
|
||||
> 备选包:`npm install -g @connormartin/bird`
|
||||
|
||||
3. 测试是否配置好:
|
||||
|
||||
```bash
|
||||
AUTH_TOKEN="xxx" CT0="yyy" xreach search "test" -n 1
|
||||
AUTH_TOKEN="xxx" CT0="yyy" bird search "test" -n 1
|
||||
```
|
||||
|
||||
## 获取 Cookie(Cookie-Editor 方式,推荐)
|
||||
@@ -47,7 +49,7 @@ agent-reach configure twitter-cookies "粘贴的 cookie JSON"
|
||||
|
||||
如果你已经知道 `auth_token` 和 `ct0`:
|
||||
|
||||
1. 安装 xreach(如果没装):`npm install -g xreach-cli`
|
||||
1. 安装 bird(如果没装):`npm install -g @steipete/bird`
|
||||
|
||||
2. 设置环境变量:
|
||||
|
||||
@@ -59,19 +61,21 @@ export CT0="你的ct0"
|
||||
3. 测试:
|
||||
|
||||
```bash
|
||||
xreach search "test" --auth-token "$AUTH_TOKEN" --ct0 "$CT0" -n 1
|
||||
bird search "test" -n 1
|
||||
```
|
||||
|
||||
## 代理配置
|
||||
|
||||
> xreach CLI 内置代理支持,通过 `--proxy` 参数传入:
|
||||
> bird CLI 支持通过环境变量设置代理:
|
||||
|
||||
```bash
|
||||
xreach search "test" --auth-token "$AUTH_TOKEN" --ct0 "$CT0" --proxy "http://user:pass@host:port"
|
||||
export HTTP_PROXY="http://user:pass@host:port"
|
||||
export HTTPS_PROXY="http://user:pass@host:port"
|
||||
bird search "test" -n 1
|
||||
```
|
||||
|
||||
也支持代理轮换文件:
|
||||
也可以使用全局代理工具:
|
||||
|
||||
```bash
|
||||
xreach search "test" --auth-token "$AUTH_TOKEN" --ct0 "$CT0" --proxy-file proxies.txt
|
||||
proxychains bird search "test" -n 1
|
||||
```
|
||||
|
||||
@@ -5,7 +5,7 @@ Agent Reach MCP Server — expose doctor/status as MCP tool.
|
||||
Run: python -m agent_reach.integrations.mcp_server
|
||||
|
||||
Agent Reach is an installer + doctor tool. For actual reading/searching,
|
||||
agents should call upstream tools directly (xreach, yt-dlp, mcporter, etc.).
|
||||
agents should call upstream tools directly (bird, yt-dlp, mcporter, etc.).
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
@@ -39,13 +39,13 @@ mcporter call 'exa.web_search_exa(query: "query", numResults: 5)'
|
||||
mcporter call 'exa.get_code_context_exa(query: "code question", tokensNum: 3000)'
|
||||
```
|
||||
|
||||
## Twitter/X (xreach)
|
||||
## Twitter/X (bird)
|
||||
|
||||
```bash
|
||||
xreach search "query" -n 10 --json # search
|
||||
xreach tweet URL_OR_ID --json # read tweet (supports /status/ and /article/ URLs)
|
||||
xreach tweets @username -n 20 --json # user timeline
|
||||
xreach thread URL_OR_ID --json # full thread
|
||||
bird search "query" -n 10 # search
|
||||
bird read URL_OR_ID # read tweet (supports /status/ and /article/ URLs)
|
||||
bird user-tweets @username -n 20 # user timeline
|
||||
bird thread URL_OR_ID # full thread
|
||||
```
|
||||
|
||||
## YouTube (yt-dlp)
|
||||
|
||||
Reference in New Issue
Block a user