Merge pull request #193 from Panniantong/feat/twitter-bird-migration
feat(twitter): migrate from xreach to bird CLI
This commit is contained in:
@@ -56,7 +56,7 @@ AI Agent 已经能帮你写代码、改文档、管项目——但你让它去
|
|||||||
|---|---|
|
|---|---|
|
||||||
| 💰 **完全免费** | 所有工具开源、所有 API 免费。唯一可能花钱的是服务器代理($1/月),本地电脑不需要 |
|
| 💰 **完全免费** | 所有工具开源、所有 API 免费。唯一可能花钱的是服务器代理($1/月),本地电脑不需要 |
|
||||||
| 🔒 **隐私安全** | Cookie 只存在你本地,不上传不外传。代码完全开源,随时可审查 |
|
| 🔒 **隐私安全** | Cookie 只存在你本地,不上传不外传。代码完全开源,随时可审查 |
|
||||||
| 🔄 **持续更新** | 底层工具(yt-dlp、xreach、Jina Reader 等)定期追踪更新到最新版,你不用自己盯 |
|
| 🔄 **持续更新** | 底层工具(yt-dlp、bird、Jina Reader 等)定期追踪更新到最新版,你不用自己盯 |
|
||||||
| 🤖 **兼容所有 Agent** | Claude Code、OpenClaw、Cursor、Windsurf……任何能跑命令行的 Agent 都能用 |
|
| 🤖 **兼容所有 Agent** | Claude Code、OpenClaw、Cursor、Windsurf……任何能跑命令行的 Agent 都能用 |
|
||||||
| 🩺 **自带诊断** | `agent-reach doctor` 一条命令告诉你哪个通、哪个不通、怎么修 |
|
| 🩺 **自带诊断** | `agent-reach doctor` 一条命令告诉你哪个通、哪个不通、怎么修 |
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ AI Agent 已经能帮你写代码、改文档、管项目——但你让它去
|
|||||||
|
|
||||||
> ⚠️ **OpenClaw 用户请先确认 exec 权限已开启**
|
> ⚠️ **OpenClaw 用户请先确认 exec 权限已开启**
|
||||||
>
|
>
|
||||||
> Agent Reach 依赖 Agent 执行 shell 命令(`pip install`、`mcporter`、`xreach` 等)。如果你的 OpenClaw 使用了默认的 `messaging` 工具配置,Agent 将无法执行命令。**安装前请先开启 exec 权限**:
|
> Agent Reach 依赖 Agent 执行 shell 命令(`pip install`、`mcporter`、`bird` 等)。如果你的 OpenClaw 使用了默认的 `messaging` 工具配置,Agent 将无法执行命令。**安装前请先开启 exec 权限**:
|
||||||
>
|
>
|
||||||
> ```bash
|
> ```bash
|
||||||
> openclaw config set tools.profile "coding"
|
> openclaw config set tools.profile "coding"
|
||||||
@@ -126,7 +126,7 @@ AI Agent 已经能帮你写代码、改文档、管项目——但你让它去
|
|||||||
<summary>它会做什么?(点击展开)</summary>
|
<summary>它会做什么?(点击展开)</summary>
|
||||||
|
|
||||||
1. **安装 CLI 工具** — `pip install` 装好 `agent-reach` 命令行
|
1. **安装 CLI 工具** — `pip install` 装好 `agent-reach` 命令行
|
||||||
2. **安装系统依赖** — 自动检测并安装 Node.js、gh CLI、mcporter、xreach 等
|
2. **安装系统依赖** — 自动检测并安装 Node.js、gh CLI、mcporter、bird 等
|
||||||
3. **配置搜索引擎** — 通过 MCP 接入 Exa(免费,无需 API Key)
|
3. **配置搜索引擎** — 通过 MCP 接入 Exa(免费,无需 API Key)
|
||||||
4. **检测环境** — 判断是本地电脑还是服务器,给出对应的配置建议
|
4. **检测环境** — 判断是本地电脑还是服务器,给出对应的配置建议
|
||||||
5. **注册 SKILL.md** — 在 Agent 的 skills 目录安装使用指南,以后 Agent 遇到"搜推特"、"看视频"这类需求,会自动知道该调哪个上游工具
|
5. **注册 SKILL.md** — 在 Agent 的 skills 目录安装使用指南,以后 Agent 遇到"搜推特"、"看视频"这类需求,会自动知道该调哪个上游工具
|
||||||
@@ -143,7 +143,7 @@ AI Agent 已经能帮你写代码、改文档、管项目——但你让它去
|
|||||||
- "帮我看看这个链接" → `curl https://r.jina.ai/URL` 读任意网页
|
- "帮我看看这个链接" → `curl https://r.jina.ai/URL` 读任意网页
|
||||||
- "这个 GitHub 仓库是做什么的" → `gh repo view owner/repo`
|
- "这个 GitHub 仓库是做什么的" → `gh repo view owner/repo`
|
||||||
- "这个视频讲了什么" → `yt-dlp --dump-json URL` 提取字幕
|
- "这个视频讲了什么" → `yt-dlp --dump-json URL` 提取字幕
|
||||||
- "帮我看看这条推文" → `xreach tweet URL --json`
|
- "帮我看看这条推文" → `bird read URL`
|
||||||
- "订阅这个 RSS" → `feedparser` 解析
|
- "订阅这个 RSS" → `feedparser` 解析
|
||||||
- "搜一下 GitHub 上有什么 LLM 框架" → `gh search repos "LLM framework"`
|
- "搜一下 GitHub 上有什么 LLM 框架" → `gh search repos "LLM framework"`
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ AI Agent 已经能帮你写代码、改文档、管项目——但你让它去
|
|||||||
|
|
||||||
Agent Reach 做的事情很简单:**帮你把这些选型和配置的活儿做完了。**
|
Agent Reach 做的事情很简单:**帮你把这些选型和配置的活儿做完了。**
|
||||||
|
|
||||||
安装完成后,Agent 直接调用上游工具(xreach CLI、yt-dlp、mcporter、gh CLI 等),不需要经过 Agent Reach 的包装层。
|
安装完成后,Agent 直接调用上游工具(bird CLI、yt-dlp、mcporter、gh CLI 等),不需要经过 Agent Reach 的包装层。
|
||||||
|
|
||||||
### 🔌 每个渠道都是可插拔的
|
### 🔌 每个渠道都是可插拔的
|
||||||
|
|
||||||
@@ -168,7 +168,7 @@ Agent Reach 做的事情很简单:**帮你把这些选型和配置的活儿做
|
|||||||
```
|
```
|
||||||
channels/
|
channels/
|
||||||
├── web.py → Jina Reader ← 可以换成 Firecrawl、Crawl4AI……
|
├── web.py → Jina Reader ← 可以换成 Firecrawl、Crawl4AI……
|
||||||
├── twitter.py → xreach ← 可以换成 Nitter、官方 API……
|
├── twitter.py → bird ← 可以换成 Nitter、官方 API……
|
||||||
├── youtube.py → yt-dlp ← 可以换成 YouTube API、Whisper……
|
├── youtube.py → yt-dlp ← 可以换成 YouTube API、Whisper……
|
||||||
├── github.py → gh CLI ← 可以换成 REST API、PyGithub……
|
├── github.py → gh CLI ← 可以换成 REST API、PyGithub……
|
||||||
├── bilibili.py → yt-dlp ← 可以换成 bilibili-api……
|
├── bilibili.py → yt-dlp ← 可以换成 bilibili-api……
|
||||||
@@ -189,7 +189,7 @@ channels/
|
|||||||
| 场景 | 选型 | 为什么选它 |
|
| 场景 | 选型 | 为什么选它 |
|
||||||
|------|------|-----------|
|
|------|------|-----------|
|
||||||
| 读网页 | [Jina Reader](https://github.com/jina-ai/reader) | 9.8K Star,免费,不需要 API Key |
|
| 读网页 | [Jina Reader](https://github.com/jina-ai/reader) | 9.8K Star,免费,不需要 API Key |
|
||||||
| 读推特 | [xreach](https://www.npmjs.com/package/xreach-cli) | Cookie 登录,免费。官方 API 按量付费(读一条 $0.005) |
|
| 读推特 | [bird](https://www.npmjs.com/package/@steipete/bird) | Cookie 登录,免费。官方 API 按量付费(读一条 $0.005) |
|
||||||
| 视频字幕 + 搜索 | [yt-dlp](https://github.com/yt-dlp/yt-dlp) | 148K Star,YouTube + B站 + 1800 站通吃 |
|
| 视频字幕 + 搜索 | [yt-dlp](https://github.com/yt-dlp/yt-dlp) | 148K Star,YouTube + B站 + 1800 站通吃 |
|
||||||
| 搜全网 | [Exa](https://exa.ai) via [mcporter](https://github.com/steipete/mcporter) | AI 语义搜索,MCP 接入免 Key |
|
| 搜全网 | [Exa](https://exa.ai) via [mcporter](https://github.com/steipete/mcporter) | AI 语义搜索,MCP 接入免 Key |
|
||||||
| GitHub | [gh CLI](https://cli.github.com) | 官方工具,认证后完整 API 能力 |
|
| GitHub | [gh CLI](https://cli.github.com) | 官方工具,认证后完整 API 能力 |
|
||||||
@@ -282,13 +282,13 @@ Star 一下,下次需要的时候能找到。⭐
|
|||||||
<details>
|
<details>
|
||||||
<summary><strong>AI Agent 怎么搜索 Twitter / X?不想付 API 费用</strong></summary>
|
<summary><strong>AI Agent 怎么搜索 Twitter / X?不想付 API 费用</strong></summary>
|
||||||
|
|
||||||
Agent Reach 使用 [xreach CLI](https://www.npmjs.com/package/xreach-cli) 通过 Cookie 认证访问 Twitter,完全免费。安装 Agent Reach 后,用 Cookie-Editor 导出你的 Twitter Cookie,运行 `agent-reach configure twitter-cookies "your_cookies"` 即可。之后 Agent 就可以用 `xreach search "关键词" --json` 搜索推文了。
|
Agent Reach 使用 [bird CLI](https://www.npmjs.com/package/@steipete/bird) 通过 Cookie 认证访问 Twitter,完全免费。安装 Agent Reach 后,用 Cookie-Editor 导出你的 Twitter Cookie,运行 `agent-reach configure twitter-cookies "your_cookies"` 即可。之后 Agent 就可以用 `bird search "关键词"` 搜索推文了。
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong>How to search Twitter/X with AI agent for free (no API)?</strong></summary>
|
<summary><strong>How to search Twitter/X with AI agent for free (no API)?</strong></summary>
|
||||||
|
|
||||||
Agent Reach uses the xreach CLI with cookie auth — zero API fees. After installing, export your Twitter cookies with the Cookie-Editor extension, run `agent-reach configure twitter-cookies "your_cookies"`, then your agent can search with `xreach search "query" --json`.
|
Agent Reach uses the bird CLI with cookie auth — zero API fees. After installing, export your Twitter cookies with the Cookie-Editor extension, run `agent-reach configure twitter-cookies "your_cookies"`, then your agent can search with `bird search "query"`.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -326,14 +326,14 @@ Yes! Agent Reach is an installer + configuration tool — any AI coding agent th
|
|||||||
<details>
|
<details>
|
||||||
<summary><strong>Is this free? Any API costs?</strong></summary>
|
<summary><strong>Is this free? Any API costs?</strong></summary>
|
||||||
|
|
||||||
100% free. All backends are open-source tools (xreach CLI, yt-dlp, Jina Reader, Exa, etc.) that don't require paid API keys. The only optional cost is a residential proxy (~$1/month) if you need Reddit/Bilibili access from a server.
|
100% free. All backends are open-source tools (bird CLI, yt-dlp, Jina Reader, Exa, etc.) that don't require paid API keys. The only optional cost is a residential proxy (~$1/month) if you need Reddit/Bilibili access from a server.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 致谢
|
## 致谢
|
||||||
|
|
||||||
[Jina Reader](https://github.com/jina-ai/reader) · [yt-dlp](https://github.com/yt-dlp/yt-dlp) · [xreach](https://www.npmjs.com/package/xreach-cli) · [Exa](https://exa.ai) · [mcporter](https://github.com/steipete/mcporter) · [feedparser](https://github.com/kurtmckee/feedparser) · [xiaohongshu-mcp](https://github.com/xpzouying/xiaohongshu-mcp) · [douyin-mcp-server](https://github.com/yzfly/douyin-mcp-server) · [linkedin-scraper-mcp](https://github.com/stickerdaniel/linkedin-mcp-server)
|
[Jina Reader](https://github.com/jina-ai/reader) · [yt-dlp](https://github.com/yt-dlp/yt-dlp) · [bird](https://www.npmjs.com/package/@steipete/bird) · [Exa](https://exa.ai) · [mcporter](https://github.com/steipete/mcporter) · [feedparser](https://github.com/kurtmckee/feedparser) · [xiaohongshu-mcp](https://github.com/xpzouying/xiaohongshu-mcp) · [douyin-mcp-server](https://github.com/yzfly/douyin-mcp-server) · [linkedin-scraper-mcp](https://github.com/stickerdaniel/linkedin-mcp-server)
|
||||||
|
|
||||||
## 联系
|
## 联系
|
||||||
|
|
||||||
|
|||||||
@@ -1,76 +1,15 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- 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 shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from .base import Channel
|
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):
|
class TwitterChannel(Channel):
|
||||||
name = "twitter"
|
name = "twitter"
|
||||||
description = "Twitter/X 推文"
|
description = "Twitter/X 推文"
|
||||||
backends = ["xreach CLI"]
|
backends = ["bird CLI"]
|
||||||
tier = 1
|
tier = 1
|
||||||
|
|
||||||
def can_handle(self, url: str) -> bool:
|
def can_handle(self, url: str) -> bool:
|
||||||
@@ -79,36 +18,33 @@ class TwitterChannel(Channel):
|
|||||||
return "x.com" in d or "twitter.com" in d
|
return "x.com" in d or "twitter.com" in d
|
||||||
|
|
||||||
def check(self, config=None):
|
def check(self, config=None):
|
||||||
xreach = shutil.which("xreach")
|
bird = shutil.which("bird") or shutil.which("birdx")
|
||||||
if not xreach:
|
if not bird:
|
||||||
return "warn", (
|
return "warn", (
|
||||||
"xreach CLI 未安装。搜索可通过 Exa 替代。安装:\n"
|
"bird CLI 未安装。搜索可通过 Exa 替代。安装:\n"
|
||||||
" npm install -g xreach-cli"
|
" 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:
|
try:
|
||||||
r = subprocess.run(
|
r = subprocess.run(
|
||||||
[xreach, "auth", "check"], capture_output=True,
|
[bird, "check"], capture_output=True,
|
||||||
encoding="utf-8", errors="replace", timeout=10
|
encoding="utf-8", errors="replace", timeout=10
|
||||||
)
|
)
|
||||||
|
output = (r.stdout or "") + (r.stderr or "")
|
||||||
if r.returncode == 0:
|
if r.returncode == 0:
|
||||||
return "ok", "完整可用(读取、搜索推文,含长文/X Article)"
|
return "ok", "完整可用(读取、搜索推文,含长文/X Article)"
|
||||||
|
# bird check returns 1 when auth is missing
|
||||||
|
if "Missing credentials" in output or "missing" in output.lower():
|
||||||
return "warn", (
|
return "warn", (
|
||||||
"xreach CLI 已安装但未配置 Cookie。运行:\n"
|
"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", (
|
||||||
|
"bird CLI 已安装但认证检查失败。运行:\n"
|
||||||
" agent-reach configure twitter-cookies \"auth_token=xxx; ct0=yyy\""
|
" agent-reach configure twitter-cookies \"auth_token=xxx; ct0=yyy\""
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
return "warn", "xreach CLI 已安装但连接失败"
|
return "warn", "bird CLI 已安装但连接失败"
|
||||||
|
|||||||
+31
-20
@@ -395,24 +395,24 @@ def _install_system_deps():
|
|||||||
except Exception:
|
except Exception:
|
||||||
print(" [!] Node.js install failed. Try: apt install nodejs npm, or nvm install 22, or download from https://nodejs.org")
|
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) ──
|
# ── bird CLI (for Twitter search) ──
|
||||||
if shutil.which("xreach"):
|
if shutil.which("bird") or shutil.which("birdx"):
|
||||||
print(" ✅ xreach CLI already installed")
|
print(" ✅ bird CLI already installed")
|
||||||
else:
|
else:
|
||||||
if shutil.which("npm"):
|
if shutil.which("npm"):
|
||||||
try:
|
try:
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["npm", "install", "-g", "xreach-cli"],
|
["npm", "install", "-g", "@steipete/bird"],
|
||||||
capture_output=True, encoding="utf-8", errors="replace", timeout=120,
|
capture_output=True, encoding="utf-8", errors="replace", timeout=120,
|
||||||
)
|
)
|
||||||
if shutil.which("xreach"):
|
if shutil.which("bird") or shutil.which("birdx"):
|
||||||
print(" ✅ xreach CLI installed (Twitter search + timeline)")
|
print(" ✅ bird CLI installed (Twitter search + timeline)")
|
||||||
else:
|
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:
|
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:
|
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) ──
|
# ── undici (proxy support for Node.js fetch) ──
|
||||||
npm_cmd = shutil.which("npm")
|
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)
|
subprocess.run([npm_cmd, "install", "-g", "undici"], capture_output=True, encoding="utf-8", errors="replace", timeout=60)
|
||||||
print(" ✅ undici installed (Node.js proxy support)")
|
print(" ✅ undici installed (Node.js proxy support)")
|
||||||
except Exception:
|
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) ──
|
# ── yt-dlp JS runtime config (YouTube requires external JS runtime) ──
|
||||||
if shutil.which("node"):
|
if shutil.which("node"):
|
||||||
@@ -626,7 +626,7 @@ def _install_system_deps_safe():
|
|||||||
deps = [
|
deps = [
|
||||||
("gh", ["gh"], "GitHub CLI", "https://cli.github.com — or: apt install gh / brew install gh"),
|
("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"),
|
("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 = []
|
missing = []
|
||||||
@@ -679,7 +679,7 @@ def _install_system_deps_dryrun():
|
|||||||
checks = [
|
checks = [
|
||||||
("gh CLI", ["gh"], "apt install gh / brew install gh"),
|
("gh CLI", ["gh"], "apt install gh / brew install gh"),
|
||||||
("Node.js", ["node"], "curl NodeSource setup | bash + apt install nodejs"),
|
("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:
|
for label, binaries, method in checks:
|
||||||
@@ -924,9 +924,10 @@ def _cmd_configure(args):
|
|||||||
config.set("twitter_auth_token", auth_token)
|
config.set("twitter_auth_token", auth_token)
|
||||||
config.set("twitter_ct0", ct0)
|
config.set("twitter_ct0", ct0)
|
||||||
|
|
||||||
# Sync credentials to xreach's session.json so xreach auth check works
|
# Sync credentials to bird CLI env
|
||||||
try:
|
try:
|
||||||
import json
|
import json
|
||||||
|
# Legacy: sync to xfetch session.json for backward compat
|
||||||
xfetch_dir = os.path.join(os.path.expanduser("~"), ".config", "xfetch")
|
xfetch_dir = os.path.join(os.path.expanduser("~"), ".config", "xfetch")
|
||||||
os.makedirs(xfetch_dir, exist_ok=True)
|
os.makedirs(xfetch_dir, exist_ok=True)
|
||||||
session_path = os.path.join(xfetch_dir, "session.json")
|
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:
|
with open(session_path, "w", encoding="utf-8") as sf:
|
||||||
json.dump(session_data, sf, indent=2)
|
json.dump(session_data, sf, indent=2)
|
||||||
os.chmod(session_path, 0o600)
|
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:
|
except Exception as e:
|
||||||
print("✅ Twitter cookies configured!")
|
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=" ")
|
print("Testing Twitter access...", end=" ")
|
||||||
try:
|
try:
|
||||||
import subprocess
|
import subprocess
|
||||||
xreach = shutil.which("xreach")
|
bird = shutil.which("bird") or shutil.which("birdx")
|
||||||
if not xreach:
|
if not bird:
|
||||||
print("[!] xreach CLI not installed. Run: npm install -g xreach-cli")
|
print("[!] bird CLI not installed. Run: npm install -g @steipete/bird")
|
||||||
else:
|
else:
|
||||||
import os
|
import os
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env["AUTH_TOKEN"] = auth_token
|
env["AUTH_TOKEN"] = auth_token
|
||||||
env["CT0"] = ct0
|
env["CT0"] = ct0
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[xreach, "search", "test", "-n", "1"],
|
[bird, "search", "test", "-n", "1"],
|
||||||
capture_output=True, encoding="utf-8", errors="replace", timeout=15,
|
capture_output=True, encoding="utf-8", errors="replace", timeout=15,
|
||||||
env=env,
|
env=env,
|
||||||
)
|
)
|
||||||
@@ -1259,7 +1270,7 @@ def _cmd_uninstall(args):
|
|||||||
print()
|
print()
|
||||||
print("Optional: remove tools installed by Agent Reach:")
|
print("Optional: remove tools installed by Agent Reach:")
|
||||||
print(" npm uninstall -g mcporter")
|
print(" npm uninstall -g mcporter")
|
||||||
print(" npm uninstall -g xreach-cli")
|
print(" npm uninstall -g @steipete/bird")
|
||||||
print(" npm uninstall -g undici")
|
print(" npm uninstall -g undici")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class Config:
|
|||||||
FEATURE_REQUIREMENTS = {
|
FEATURE_REQUIREMENTS = {
|
||||||
"exa_search": ["exa_api_key"],
|
"exa_search": ["exa_api_key"],
|
||||||
"reddit_proxy": ["reddit_proxy"],
|
"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"],
|
"groq_whisper": ["groq_api_key"],
|
||||||
"github_token": ["github_token"],
|
"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:
|
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 json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -138,6 +138,31 @@ def _sync_xfetch_session(auth_token: str, ct0: str) -> None:
|
|||||||
pass
|
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]]:
|
def configure_from_browser(browser: str, config) -> List[Tuple[str, bool, str]]:
|
||||||
"""
|
"""
|
||||||
Extract cookies and configure all found platforms.
|
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:
|
if "auth_token" in tc and "ct0" in tc:
|
||||||
config.set("twitter_auth_token", tc["auth_token"])
|
config.set("twitter_auth_token", tc["auth_token"])
|
||||||
config.set("twitter_ct0", tc["ct0"])
|
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"])
|
_sync_xfetch_session(tc["auth_token"], tc["ct0"])
|
||||||
results_list.append(("Twitter/X", True, "auth_token + ct0"))
|
results_list.append(("Twitter/X", True, "auth_token + ct0"))
|
||||||
else:
|
else:
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
AgentReach — installer, doctor, and configuration tool.
|
AgentReach — installer, doctor, and configuration tool.
|
||||||
|
|
||||||
Agent Reach helps AI agents install and configure upstream platform tools
|
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.
|
call the upstream tools directly — no wrapper layer needed.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|||||||
@@ -1,33 +1,35 @@
|
|||||||
# Twitter 高级功能配置指南(xreach CLI)
|
# Twitter 高级功能配置指南(bird CLI)
|
||||||
|
|
||||||
Twitter 基础阅读通过 Jina Reader 免费可用,无需配置。
|
Twitter 基础阅读通过 Jina Reader 免费可用,无需配置。
|
||||||
|
|
||||||
高级功能需要 xreach CLI:
|
高级功能需要 bird CLI(@steipete/bird):
|
||||||
|
|
||||||
- 搜索推文(`xreach search`)
|
- 搜索推文(`bird search`)
|
||||||
- 读取完整推文和对话链(`xreach tweet`、`xreach thread`)
|
- 读取完整推文和对话链(`bird read`、`bird thread`)
|
||||||
- 用户时间线(`xreach tweets`)
|
- 用户时间线(`bird user-tweets`)
|
||||||
|
|
||||||
xreach 是免费开源工具(npm 包 xreach-cli),但需要你的 Twitter 账号 cookie。
|
bird 是免费开源工具(npm 包 @steipete/bird),但需要你的 Twitter 账号 cookie。
|
||||||
|
|
||||||
## 快速配置
|
## 快速配置
|
||||||
|
|
||||||
1. 检查 xreach 是否安装:
|
1. 检查 bird 是否安装:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
which xreach && echo "installed" || echo "not installed"
|
which bird && echo "installed" || echo "not installed"
|
||||||
```
|
```
|
||||||
|
|
||||||
2. 安装 xreach:
|
2. 安装 bird:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -g xreach-cli
|
npm install -g @steipete/bird
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> 备选包:`npm install -g @connormartin/bird`
|
||||||
|
|
||||||
3. 测试是否配置好:
|
3. 测试是否配置好:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
AUTH_TOKEN="xxx" CT0="yyy" xreach search "test" -n 1
|
AUTH_TOKEN="xxx" CT0="yyy" bird search "test" -n 1
|
||||||
```
|
```
|
||||||
|
|
||||||
## 获取 Cookie(Cookie-Editor 方式,推荐)
|
## 获取 Cookie(Cookie-Editor 方式,推荐)
|
||||||
@@ -47,7 +49,7 @@ agent-reach configure twitter-cookies "粘贴的 cookie JSON"
|
|||||||
|
|
||||||
如果你已经知道 `auth_token` 和 `ct0`:
|
如果你已经知道 `auth_token` 和 `ct0`:
|
||||||
|
|
||||||
1. 安装 xreach(如果没装):`npm install -g xreach-cli`
|
1. 安装 bird(如果没装):`npm install -g @steipete/bird`
|
||||||
|
|
||||||
2. 设置环境变量:
|
2. 设置环境变量:
|
||||||
|
|
||||||
@@ -59,19 +61,21 @@ export CT0="你的ct0"
|
|||||||
3. 测试:
|
3. 测试:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
xreach search "test" --auth-token "$AUTH_TOKEN" --ct0 "$CT0" -n 1
|
bird search "test" -n 1
|
||||||
```
|
```
|
||||||
|
|
||||||
## 代理配置
|
## 代理配置
|
||||||
|
|
||||||
> xreach CLI 内置代理支持,通过 `--proxy` 参数传入:
|
> bird CLI 支持通过环境变量设置代理:
|
||||||
|
|
||||||
```bash
|
```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
|
```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
|
Run: python -m agent_reach.integrations.mcp_server
|
||||||
|
|
||||||
Agent Reach is an installer + doctor tool. For actual reading/searching,
|
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
|
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)'
|
mcporter call 'exa.get_code_context_exa(query: "code question", tokensNum: 3000)'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Twitter/X (xreach)
|
## Twitter/X (bird)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
xreach search "query" -n 10 --json # search
|
bird search "query" -n 10 # search
|
||||||
xreach tweet URL_OR_ID --json # read tweet (supports /status/ and /article/ URLs)
|
bird read URL_OR_ID # read tweet (supports /status/ and /article/ URLs)
|
||||||
xreach tweets @username -n 20 --json # user timeline
|
bird user-tweets @username -n 20 # user timeline
|
||||||
xreach thread URL_OR_ID --json # full thread
|
bird thread URL_OR_ID # full thread
|
||||||
```
|
```
|
||||||
|
|
||||||
## YouTube (yt-dlp)
|
## YouTube (yt-dlp)
|
||||||
|
|||||||
+10
-10
@@ -51,7 +51,7 @@ Update Agent Reach: https://raw.githubusercontent.com/Panniantong/agent-reach/ma
|
|||||||
|---|---|
|
|---|---|
|
||||||
| 💰 **Completely free** | All tools are open source, all APIs are free. The only possible cost is a server proxy ($1/month) — local computers don't need one |
|
| 💰 **Completely free** | All tools are open source, all APIs are free. The only possible cost is a server proxy ($1/month) — local computers don't need one |
|
||||||
| 🔒 **Privacy safe** | Cookies stay local. Never uploaded. Fully open source — audit anytime |
|
| 🔒 **Privacy safe** | Cookies stay local. Never uploaded. Fully open source — audit anytime |
|
||||||
| 🔄 **Kept up to date** | Upstream tools (yt-dlp, xreach, Jina Reader, etc.) are tracked and updated regularly |
|
| 🔄 **Kept up to date** | Upstream tools (yt-dlp, bird, Jina Reader, etc.) are tracked and updated regularly |
|
||||||
| 🤖 **Works with any Agent** | Claude Code, OpenClaw, Cursor, Windsurf… any Agent that can run commands |
|
| 🤖 **Works with any Agent** | Claude Code, OpenClaw, Cursor, Windsurf… any Agent that can run commands |
|
||||||
| 🩺 **Built-in diagnostics** | `agent-reach doctor` — one command shows what works, what doesn't, and how to fix it |
|
| 🩺 **Built-in diagnostics** | `agent-reach doctor` — one command shows what works, what doesn't, and how to fix it |
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ Update Agent Reach: https://raw.githubusercontent.com/Panniantong/agent-reach/ma
|
|||||||
| Platform | Capabilities | Setup | Notes |
|
| Platform | Capabilities | Setup | Notes |
|
||||||
|----------|-------------|:-----:|-------|
|
|----------|-------------|:-----:|-------|
|
||||||
| 🌐 **Web** | Read | Zero config | Any URL → clean Markdown ([Jina Reader](https://github.com/jina-ai/reader) ⭐9.8K) |
|
| 🌐 **Web** | Read | Zero config | Any URL → clean Markdown ([Jina Reader](https://github.com/jina-ai/reader) ⭐9.8K) |
|
||||||
| 🐦 **Twitter/X** | Read · Search | Zero config / Cookie | Single tweets readable out of the box. Cookie unlocks search, timeline, posting ([xreach](https://www.npmjs.com/package/xreach-cli)) |
|
| 🐦 **Twitter/X** | Read · Search | Zero config / Cookie | Single tweets readable out of the box. Cookie unlocks search, timeline, posting ([bird](https://www.npmjs.com/package/@steipete/bird)) |
|
||||||
| 📕 **XiaoHongShu** | Read · Search · **Post · Comment · Like** | mcporter | Via [xiaohongshu-mcp](https://github.com/user/xiaohongshu-mcp) internal API, install and go |
|
| 📕 **XiaoHongShu** | Read · Search · **Post · Comment · Like** | mcporter | Via [xiaohongshu-mcp](https://github.com/user/xiaohongshu-mcp) internal API, install and go |
|
||||||
| 🎵 **Douyin** | Video parsing · Watermark-free download | mcporter | Via [douyin-mcp-server](https://github.com/yzfly/douyin-mcp-server), no login needed |
|
| 🎵 **Douyin** | Video parsing · Watermark-free download | mcporter | Via [douyin-mcp-server](https://github.com/yzfly/douyin-mcp-server), no login needed |
|
||||||
| 💼 **LinkedIn** | Jina Reader (public pages) | Full profiles, companies, job search | Tell your Agent "help me set up LinkedIn" |
|
| 💼 **LinkedIn** | Jina Reader (public pages) | Full profiles, companies, job search | Tell your Agent "help me set up LinkedIn" |
|
||||||
@@ -126,7 +126,7 @@ No configuration needed — just tell your Agent:
|
|||||||
- "Read this link" → `curl https://r.jina.ai/URL` for any web page
|
- "Read this link" → `curl https://r.jina.ai/URL` for any web page
|
||||||
- "What's this GitHub repo about?" → `gh repo view owner/repo`
|
- "What's this GitHub repo about?" → `gh repo view owner/repo`
|
||||||
- "What does this video cover?" → `yt-dlp --dump-json URL` for subtitles
|
- "What does this video cover?" → `yt-dlp --dump-json URL` for subtitles
|
||||||
- "Read this tweet" → `xreach tweet URL --json`
|
- "Read this tweet" → `bird read URL`
|
||||||
- "Subscribe to this RSS" → `feedparser` to parse feeds
|
- "Subscribe to this RSS" → `feedparser` to parse feeds
|
||||||
- "Search GitHub for LLM frameworks" → `gh search repos "LLM framework"`
|
- "Search GitHub for LLM frameworks" → `gh search repos "LLM framework"`
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ Every time you spin up a new Agent, you spend time finding tools, installing dep
|
|||||||
|
|
||||||
Agent Reach does one simple thing: **it makes those tool selection and configuration decisions for you.**
|
Agent Reach does one simple thing: **it makes those tool selection and configuration decisions for you.**
|
||||||
|
|
||||||
After installation, your Agent calls the upstream tools directly (xreach CLI, yt-dlp, mcporter, gh CLI, etc.) — no wrapper layer in between.
|
After installation, your Agent calls the upstream tools directly (bird CLI, yt-dlp, mcporter, gh CLI, etc.) — no wrapper layer in between.
|
||||||
|
|
||||||
### 🔌 Every Channel is Pluggable
|
### 🔌 Every Channel is Pluggable
|
||||||
|
|
||||||
@@ -195,7 +195,7 @@ Each platform maps to an upstream tool. **Don't like one? Swap it out.**
|
|||||||
```
|
```
|
||||||
channels/
|
channels/
|
||||||
├── web.py → Jina Reader ← swap to Firecrawl, Crawl4AI…
|
├── web.py → Jina Reader ← swap to Firecrawl, Crawl4AI…
|
||||||
├── twitter.py → xreach ← swap to Nitter, official API…
|
├── twitter.py → bird CLI ← swap to Nitter, official API…
|
||||||
├── youtube.py → yt-dlp ← swap to YouTube API, Whisper…
|
├── youtube.py → yt-dlp ← swap to YouTube API, Whisper…
|
||||||
├── github.py → gh CLI ← swap to REST API, PyGithub…
|
├── github.py → gh CLI ← swap to REST API, PyGithub…
|
||||||
├── bilibili.py → yt-dlp ← swap to bilibili-api…
|
├── bilibili.py → yt-dlp ← swap to bilibili-api…
|
||||||
@@ -215,7 +215,7 @@ Each channel file only checks whether its upstream tool is installed and working
|
|||||||
| Scenario | Tool | Why |
|
| Scenario | Tool | Why |
|
||||||
|----------|------|-----|
|
|----------|------|-----|
|
||||||
| Read web pages | [Jina Reader](https://github.com/jina-ai/reader) | 9.8K stars, free, no API key needed |
|
| Read web pages | [Jina Reader](https://github.com/jina-ai/reader) | 9.8K stars, free, no API key needed |
|
||||||
| Read tweets | [xreach](https://www.npmjs.com/package/xreach-cli) | Cookie auth, free. Official API is pay-per-use ($0.005/post read) |
|
| Read tweets | [bird](https://www.npmjs.com/package/@steipete/bird) | Cookie auth, free. Official API is pay-per-use ($0.005/post read) |
|
||||||
| Video subtitles + search | [yt-dlp](https://github.com/yt-dlp/yt-dlp) | 148K stars, YouTube + Bilibili + 1800 sites |
|
| Video subtitles + search | [yt-dlp](https://github.com/yt-dlp/yt-dlp) | 148K stars, YouTube + Bilibili + 1800 sites |
|
||||||
| Search the web | [Exa](https://exa.ai) via [mcporter](https://github.com/nicepkg/mcporter) | AI semantic search, MCP integration, no API key |
|
| Search the web | [Exa](https://exa.ai) via [mcporter](https://github.com/nicepkg/mcporter) | AI semantic search, MCP integration, no API key |
|
||||||
| GitHub | [gh CLI](https://cli.github.com) | Official tool, full API after auth |
|
| GitHub | [gh CLI](https://cli.github.com) | Official tool, full API after auth |
|
||||||
@@ -248,7 +248,7 @@ This project was entirely vibe-coded 🎸 There might be rough edges here and th
|
|||||||
<details>
|
<details>
|
||||||
<summary><strong>How to search Twitter/X with AI agent without paying for API?</strong></summary>
|
<summary><strong>How to search Twitter/X with AI agent without paying for API?</strong></summary>
|
||||||
|
|
||||||
Agent Reach uses the [xreach CLI](https://www.npmjs.com/package/xreach-cli) with cookie-based authentication — completely free, no Twitter API subscription needed. After installing Agent Reach, export your Twitter cookies using the Cookie-Editor Chrome extension, run `agent-reach configure twitter-cookies "your_cookies"`, and your agent can search with `xreach search "query" --json`.
|
Agent Reach uses the [bird CLI](https://www.npmjs.com/package/@steipete/bird) with cookie-based authentication — completely free, no Twitter API subscription needed. After installing Agent Reach, export your Twitter cookies using the Cookie-Editor Chrome extension, run `agent-reach configure twitter-cookies "your_cookies"`, and your agent can search with `bird search "query" -n 10`.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -272,13 +272,13 @@ Yes! Agent Reach is an installer + configuration tool. Any AI coding agent that
|
|||||||
<details>
|
<details>
|
||||||
<summary><strong>Is Agent Reach free? Any API costs?</strong></summary>
|
<summary><strong>Is Agent Reach free? Any API costs?</strong></summary>
|
||||||
|
|
||||||
100% free and open source. All backends (xreach CLI, yt-dlp, Jina Reader, Exa) are free tools that don't require paid API keys. The only optional cost is a residential proxy (~$1/month) if you need Reddit/Bilibili access from a server.
|
100% free and open source. All backends (bird CLI, yt-dlp, Jina Reader, Exa) are free tools that don't require paid API keys. The only optional cost is a residential proxy (~$1/month) if you need Reddit/Bilibili access from a server.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong>Free alternative to Twitter API for web scraping?</strong></summary>
|
<summary><strong>Free alternative to Twitter API for web scraping?</strong></summary>
|
||||||
|
|
||||||
Agent Reach uses xreach CLI which accesses Twitter via cookie auth — same as your browser session. No API fees, no rate limit tiers, no developer account needed. Supports search, read tweets, read profiles, and timelines.
|
Agent Reach uses bird CLI which accesses Twitter via cookie auth — same as your browser session. No API fees, no rate limit tiers, no developer account needed. Supports search, read tweets, read profiles, and timelines.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -297,7 +297,7 @@ Install douyin-mcp-server, then your agent can use `mcporter call 'douyin.parse_
|
|||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
[Jina Reader](https://github.com/jina-ai/reader) · [yt-dlp](https://github.com/yt-dlp/yt-dlp) · [xreach](https://www.npmjs.com/package/xreach-cli) · [Exa](https://exa.ai) · [feedparser](https://github.com/kurtmckee/feedparser) · [douyin-mcp-server](https://github.com/yzfly/douyin-mcp-server) · [linkedin-scraper-mcp](https://github.com/stickerdaniel/linkedin-mcp-server)
|
[Jina Reader](https://github.com/jina-ai/reader) · [yt-dlp](https://github.com/yt-dlp/yt-dlp) · [bird](https://www.npmjs.com/package/@steipete/bird) · [Exa](https://exa.ai) · [feedparser](https://github.com/kurtmckee/feedparser) · [douyin-mcp-server](https://github.com/yzfly/douyin-mcp-server) · [linkedin-scraper-mcp](https://github.com/stickerdaniel/linkedin-mcp-server)
|
||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
|
|
||||||
|
|||||||
+7
-8
@@ -20,7 +20,7 @@ Copy this to your AI Agent:
|
|||||||
|
|
||||||
### Goal
|
### Goal
|
||||||
|
|
||||||
Install Agent Reach and all upstream tools so your user's agent has full internet access. After installation, you'll use the upstream tools directly (xreach CLI, yt-dlp, mcporter, gh CLI, etc.) — Agent Reach is just the installer and health checker.
|
Install Agent Reach and all upstream tools so your user's agent has full internet access. After installation, you'll use the upstream tools directly (bird CLI, yt-dlp, mcporter, gh CLI, etc.) — Agent Reach is just the installer and health checker.
|
||||||
|
|
||||||
### ⚠️ Boundaries
|
### ⚠️ Boundaries
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ pip install https://github.com/Panniantong/agent-reach/archive/main.zip
|
|||||||
agent-reach install --env=auto
|
agent-reach install --env=auto
|
||||||
```
|
```
|
||||||
|
|
||||||
This auto-installs system dependencies (gh CLI, Node.js, mcporter, xreach), configures Exa search, detects environment, and tests all channels.
|
This auto-installs system dependencies (gh CLI, Node.js, mcporter, bird), configures Exa search, detects environment, and tests all channels.
|
||||||
|
|
||||||
> 💡 **macOS / Homebrew Python 提示 `externally-managed-environment`?**
|
> 💡 **macOS / Homebrew Python 提示 `externally-managed-environment`?**
|
||||||
> 这是 PEP 668 保护,不是 Agent Reach 本身的问题。优先用 `pipx install ...`,或先创建 `venv` 再安装。
|
> 这是 PEP 668 保护,不是 Agent Reach 本身的问题。优先用 `pipx install ...`,或先创建 `venv` 再安装。
|
||||||
@@ -129,13 +129,12 @@ agent-reach configure twitter-cookies "PASTED_STRING"
|
|||||||
|
|
||||||
> **代理说明(中国大陆等需要翻墙的网络环境):**
|
> **代理说明(中国大陆等需要翻墙的网络环境):**
|
||||||
>
|
>
|
||||||
> xreach CLI 使用 Node.js 原生 `fetch()`,默认不走系统代理(`HTTP_PROXY`/`HTTPS_PROXY`)。
|
> bird CLI 使用 Node.js,在需要代理的网络环境下可通过环境变量配置代理。
|
||||||
> Agent Reach 已内置解决方案:安装时会自动安装 `undici`(`npm install -g undici`),
|
> Agent Reach 安装时会自动安装 `undici`(`npm install -g undici`)以提供代理支持。
|
||||||
> 运行 xreach 时自动注入 `EnvHttpProxyAgent`,让 fetch 走用户配置的代理。
|
|
||||||
>
|
>
|
||||||
> **你(Agent)需要做的:**
|
> **你(Agent)需要做的:**
|
||||||
> 1. 确认 `undici` 已全局安装:`npm list -g undici`,没有就装:`npm install -g undici`
|
> 1. 确认用户配了代理:`agent-reach configure proxy http://user:pass@ip:port`
|
||||||
> 2. 确认用户配了代理:`agent-reach configure proxy http://user:pass@ip:port`
|
> 2. 设置环境变量:`export HTTP_PROXY="..." HTTPS_PROXY="..."`
|
||||||
> 3. Agent Reach 会自动处理剩下的,不需要用户做额外操作
|
> 3. Agent Reach 会自动处理剩下的,不需要用户做额外操作
|
||||||
>
|
>
|
||||||
> 如果用户报告 "fetch failed",参考 [troubleshooting.md](troubleshooting.md)
|
> 如果用户报告 "fetch failed",参考 [troubleshooting.md](troubleshooting.md)
|
||||||
@@ -325,7 +324,7 @@ After installation, use upstream tools directly. See SKILL.md for the full comma
|
|||||||
|
|
||||||
| Platform | Upstream Tool | Example |
|
| Platform | Upstream Tool | Example |
|
||||||
|----------|--------------|---------|
|
|----------|--------------|---------|
|
||||||
| Twitter/X | `xreach` | `xreach search "query" --json` |
|
| Twitter/X | `bird` | `bird search "query" -n 10` |
|
||||||
| YouTube | `yt-dlp` | `yt-dlp --dump-json URL` |
|
| YouTube | `yt-dlp` | `yt-dlp --dump-json URL` |
|
||||||
| Bilibili | `yt-dlp` | `yt-dlp --dump-json URL` |
|
| Bilibili | `yt-dlp` | `yt-dlp --dump-json URL` |
|
||||||
| Reddit | `curl` | `curl -s "https://reddit.com/r/xxx.json"` |
|
| Reddit | `curl` | `curl -s "https://reddit.com/r/xxx.json"` |
|
||||||
|
|||||||
+14
-15
@@ -1,44 +1,43 @@
|
|||||||
# 常见问题排查
|
# 常见问题排查
|
||||||
|
|
||||||
## Twitter/X: xreach CLI "fetch failed"
|
## Twitter/X: bird CLI 连接失败
|
||||||
|
|
||||||
**症状:** `xreach search` 或其他命令返回 "fetch failed"
|
**症状:** `bird search` 或其他命令返回错误
|
||||||
|
|
||||||
**原因:** xreach CLI 使用 Node.js 的 `undici` 库发请求。如果你的网络环境需要代理才能访问 x.com,需要明确传入代理参数。
|
**原因:** bird CLI 需要 AUTH_TOKEN 和 CT0 环境变量才能访问 Twitter API。如果你的网络环境需要代理才能访问 x.com,需要配置代理。
|
||||||
|
|
||||||
**解决方案:**
|
**解决方案:**
|
||||||
|
|
||||||
### 方案 1:使用 --proxy 参数
|
### 方案 1:设置环境变量代理
|
||||||
|
|
||||||
```bash
|
```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
|
||||||
```
|
```
|
||||||
|
|
||||||
### 方案 2:使用全局代理工具
|
### 方案 2:使用全局代理工具
|
||||||
|
|
||||||
让代理工具接管所有网络流量,这样 xreach 的请求也会走代理:
|
让代理工具接管所有网络流量,这样 bird 的请求也会走代理:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS — ClashX / Surge 开启"增强模式"
|
# macOS — ClashX / Surge 开启"增强模式"
|
||||||
# Linux — proxychains 或 tun2socks
|
# Linux — proxychains 或 tun2socks
|
||||||
proxychains xreach search "test" -n 1
|
proxychains bird search "test" -n 1
|
||||||
```
|
```
|
||||||
|
|
||||||
### 方案 3:不用 xreach,用 Exa 搜索替代
|
### 方案 3:不用 bird,用 Exa 搜索替代
|
||||||
|
|
||||||
xreach 不可用时,可以直接用 Exa 搜索 Twitter 内容:
|
bird 不可用时,可以直接用 Exa 搜索 Twitter 内容:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mcporter call 'exa.web_search_exa(query: "site:x.com 搜索词", numResults: 5)'
|
mcporter call 'exa.web_search_exa(query: "site:x.com 搜索词", numResults: 5)'
|
||||||
```
|
```
|
||||||
|
|
||||||
### 方案 4:设置 HTTP_PROXY 环境变量
|
### 方案 4:检查认证
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export HTTP_PROXY="http://127.0.0.1:7890"
|
bird check
|
||||||
export HTTPS_PROXY="http://127.0.0.1:7890"
|
|
||||||
|
|
||||||
xreach search "test"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> ⚠️ 注意:Node.js 原生 fetch 不一定读取这些环境变量,推荐用方案 1 的 --proxy 参数。
|
> 如果返回 "Missing credentials",需要设置 AUTH_TOKEN 和 CT0 环境变量。
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from unittest.mock import patch, Mock
|
from unittest.mock import patch, Mock
|
||||||
|
|
||||||
from agent_reach.channels.twitter import _detect_xreach_version, TwitterChannel
|
from agent_reach.channels.twitter import TwitterChannel
|
||||||
|
|
||||||
|
|
||||||
def _cp(stdout="", stderr="", returncode=0):
|
def _cp(stdout="", stderr="", returncode=0):
|
||||||
@@ -13,27 +13,58 @@ def _cp(stdout="", stderr="", returncode=0):
|
|||||||
return m
|
return m
|
||||||
|
|
||||||
|
|
||||||
def test_detect_xreach_version_prefers_npm_when_cli_version_is_stale():
|
def test_check_bird_found_and_auth_ok():
|
||||||
with patch("shutil.which", return_value="/opt/homebrew/bin/npm"), patch(
|
"""bird found + bird check returns 0 → ok."""
|
||||||
"subprocess.run",
|
|
||||||
side_effect=[
|
|
||||||
_cp(stdout="0.3.0\n"),
|
|
||||||
_cp(stdout='{"dependencies":{"xreach-cli":{"version":"0.3.2"}}}'),
|
|
||||||
],
|
|
||||||
):
|
|
||||||
assert _detect_xreach_version("/opt/homebrew/bin/xreach") == "0.3.2"
|
|
||||||
|
|
||||||
|
|
||||||
def test_twitter_channel_does_not_false_warn_when_npm_has_newer_xreach():
|
|
||||||
channel = TwitterChannel()
|
channel = TwitterChannel()
|
||||||
with patch("shutil.which", side_effect=lambda name: "/opt/homebrew/bin/xreach" if name == "xreach" else "/opt/homebrew/bin/npm"), patch(
|
with patch("shutil.which", side_effect=lambda name: "/usr/local/bin/bird" if name == "bird" else None), patch(
|
||||||
"subprocess.run",
|
"subprocess.run",
|
||||||
side_effect=[
|
return_value=_cp(stdout="Authenticated as @user\n", returncode=0),
|
||||||
_cp(stdout="0.3.0\n"),
|
|
||||||
_cp(stdout='{"dependencies":{"xreach-cli":{"version":"0.3.2"}}}'),
|
|
||||||
_cp(stdout="authenticated\n", returncode=0),
|
|
||||||
],
|
|
||||||
):
|
):
|
||||||
status, message = channel.check()
|
status, message = channel.check()
|
||||||
assert status == "ok"
|
assert status == "ok"
|
||||||
assert "完整可用" in message
|
assert "完整可用" in message
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_bird_found_auth_missing():
|
||||||
|
"""bird found + bird check returns 1 with 'Missing credentials' → warn about auth."""
|
||||||
|
channel = TwitterChannel()
|
||||||
|
with patch("shutil.which", side_effect=lambda name: "/usr/local/bin/bird" if name == "bird" else None), patch(
|
||||||
|
"subprocess.run",
|
||||||
|
return_value=_cp(stderr="Missing credentials: AUTH_TOKEN and CT0 required\n", returncode=1),
|
||||||
|
):
|
||||||
|
status, message = channel.check()
|
||||||
|
assert status == "warn"
|
||||||
|
assert "未配置认证" in message
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_bird_not_found():
|
||||||
|
"""bird not found → warn with install hint for @steipete/bird."""
|
||||||
|
channel = TwitterChannel()
|
||||||
|
with patch("shutil.which", return_value=None):
|
||||||
|
status, message = channel.check()
|
||||||
|
assert status == "warn"
|
||||||
|
assert "@steipete/bird" in message
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_birdx_binary_accepted():
|
||||||
|
"""birdx symlink is accepted as an alternative binary name."""
|
||||||
|
channel = TwitterChannel()
|
||||||
|
with patch("shutil.which", side_effect=lambda name: "/usr/local/bin/birdx" if name == "birdx" else None), patch(
|
||||||
|
"subprocess.run",
|
||||||
|
return_value=_cp(stdout="Authenticated as @user\n", returncode=0),
|
||||||
|
):
|
||||||
|
status, message = channel.check()
|
||||||
|
assert status == "ok"
|
||||||
|
assert "完整可用" in message
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_bird_auth_failure_generic():
|
||||||
|
"""bird check returns 1 without 'Missing credentials' → generic auth failure warn."""
|
||||||
|
channel = TwitterChannel()
|
||||||
|
with patch("shutil.which", side_effect=lambda name: "/usr/local/bin/bird" if name == "bird" else None), patch(
|
||||||
|
"subprocess.run",
|
||||||
|
return_value=_cp(stderr="Error: token expired\n", returncode=1),
|
||||||
|
):
|
||||||
|
status, message = channel.check()
|
||||||
|
assert status == "warn"
|
||||||
|
assert "认证检查失败" in message
|
||||||
|
|||||||
Reference in New Issue
Block a user