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:
Panniantong
2026-03-23 08:51:22 +01:00
parent c1af1cad14
commit 7ae0cd8c0a
13 changed files with 199 additions and 193 deletions
+19 -83
View File
@@ -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
View File
@@ -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")
+1 -1
View File
@@ -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"],
}
+28 -2
View File
@@ -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
View File
@@ -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:
+21 -17
View File
@@ -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
```
## 获取 CookieCookie-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
```
+1 -1
View File
@@ -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
+5 -5
View File
@@ -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)