Document env-based X auth flow

Update README, launch copy, and UI guidance to prefer popup-free AUTH_TOKEN/CT0 configuration, and keep X backend selection on the verified Bird or xAI paths.

Validation: uv run python -m unittest tests.test_env_project
This commit is contained in:
Jeffrey Sperling
2026-03-12 21:07:09 -07:00
parent dd9a3f1482
commit 3aaf31b08d
7 changed files with 93 additions and 87 deletions
+23 -25
View File
@@ -14,12 +14,11 @@ clawhub install last30days-official
**The AI world reinvents itself every month. This skill keeps you current.** /last30days researches your topic across Reddit, X, Bluesky, YouTube, TikTok, Instagram, Hacker News, Polymarket, and the web from the last 30 days, finds what the community is actually upvoting, sharing, betting on, and saying on camera, and writes you a grounded narrative with real citations. Whether it's Seedance 2.0 access, paper.design prompts, or the latest Nano Banana Pro techniques, you'll know what people who are paying attention already know. **The AI world reinvents itself every month. This skill keeps you current.** /last30days researches your topic across Reddit, X, Bluesky, YouTube, TikTok, Instagram, Hacker News, Polymarket, and the web from the last 30 days, finds what the community is actually upvoting, sharing, betting on, and saying on camera, and writes you a grounded narrative with real citations. Whether it's Seedance 2.0 access, paper.design prompts, or the latest Nano Banana Pro techniques, you'll know what people who are paying attention already know.
**New in v2.9.5 — Bluesky, Comparative Mode, ScrapeCreators X:** **New in v2.9.5 — Bluesky, Comparative Mode, and Config Improvements:**
- **Bluesky/AT Protocol** is now a social source. Opt-in via `BSKY_HANDLE` + `BSKY_APP_PASSWORD` (create at bsky.app/settings/app-passwords). Full pipeline: search, score, dedupe, render. - **Bluesky/AT Protocol** is now a social source. Opt-in via `BSKY_HANDLE` + `BSKY_APP_PASSWORD` (create at bsky.app/settings/app-passwords). Full pipeline: search, score, dedupe, render.
- **Comparative mode** - ask "X vs Y" (e.g., `/last30 cursor vs windsurf`) and get 3 parallel research passes with a side-by-side comparison: strengths, weaknesses, head-to-head table, and a data-driven verdict. - **Comparative mode** - ask "X vs Y" (e.g., `/last30 cursor vs windsurf`) and get 3 parallel research passes with a side-by-side comparison: strengths, weaknesses, head-to-head table, and a data-driven verdict.
- **ScrapeCreators X backend** - X/Twitter search now uses ScrapeCreators as an additional backend alongside Bird cookie auth. - **Per-project .env config** - drop a `.claude/last30days.env` in your project root for per-project API keys.
- **Per-project .env config** - drop a `.last30days.env` in your project root for per-project API keys.
- **SessionStart config check** - validates your config automatically when a Claude Code session starts. - **SessionStart config check** - validates your config automatically when a Claude Code session starts.
- **Expanded test coverage** - 455+ tests across all modules. - **Expanded test coverage** - 455+ tests across all modules.
@@ -70,32 +69,31 @@ git clone https://github.com/mvanhorn/last30days-skill.git ~/.claude/skills/last
# Add your API keys (optional if signed in to Codex) # Add your API keys (optional if signed in to Codex)
mkdir -p ~/.config/last30days mkdir -p ~/.config/last30days
cat > ~/.config/last30days/.env << 'EOF' cat > ~/.config/last30days/.env << 'EOF'
SCRAPECREATORS_API_KEY=... # Reddit + TikTok + Instagram (one key, all three) scrapecreators.com SCRAPECREATORS_API_KEY=... # Reddit + TikTok + Instagram (one key, all three) - scrapecreators.com
OPENAI_API_KEY=sk-... # optional legacy Reddit fallback if using `codex login` OPENAI_API_KEY=sk-... # optional - legacy Reddit fallback if using `codex login`
XAI_API_KEY=xai-... # optional — cookie auth is default for X search AUTH_TOKEN=... # recommended for X search - copy once from x.com cookies
BSKY_HANDLE=you.bsky.social # optional — Bluesky search (create app password below) CT0=... # recommended for X search - copy once from x.com cookies
BSKY_APP_PASSWORD=xxxx-xxxx-xxxx # optional — bsky.app/settings/app-passwords XAI_API_KEY=xai-... # optional - X fallback if you do not want cookie-based auth
BSKY_HANDLE=you.bsky.social # optional - Bluesky search (create app password below)
BSKY_APP_PASSWORD=xxxx-xxxx-xxxx # optional - bsky.app/settings/app-passwords
EOF EOF
chmod 600 ~/.config/last30days/.env chmod 600 ~/.config/last30days/.env
``` ```
If you're signed in to Codex (`codex login`), the skill will use your Codex credentials for the OpenAI Responses API and you can omit `OPENAI_API_KEY`. If you're not signed in, run `codex login` first. If you're signed in to Codex (`codex login`), the skill will use your Codex credentials for the OpenAI Responses API and you can omit `OPENAI_API_KEY`. If you're not signed in, run `codex login` first.
For project-specific overrides, create `.claude/last30days.env` in the repo root. It overrides the global `~/.config/last30days/.env`.
### X Search Authentication ### X Search Authentication
X search reads your existing browser cookies - no API keys or login commands needed. X search prefers explicit env auth. This keeps local runs headless and avoids browser-cookie and macOS Keychain prompts.
**Safari (recommended on Mac):** Just be logged into x.com. No setup needed. **Recommended setup:**
1. While logged into x.com once, open browser dev tools and copy the `auth_token` and `ct0` cookies for `x.com`.
2. Save them as `AUTH_TOKEN` and `CT0` in `~/.config/last30days/.env`, export them in your shell, or add them to `.claude/last30days.env` for a single project.
3. Re-run `/last30days`.
**Chrome:** Works, but macOS will prompt you to allow Keychain access the first time. Click "Allow" (or "Always Allow" to stop future prompts). **xAI fallback:** If you do not want to provide `AUTH_TOKEN` and `CT0`, set `XAI_API_KEY` and the skill will use xAI's `x_search` backend instead.
**Firefox:** Just be logged into x.com. No setup needed.
**Manual fallback:** If cookie auto-detection doesn't work, set these env vars (grab them from your browser's dev tools → Application → Cookies → x.com):
```bash
export AUTH_TOKEN=your_auth_token
export CT0=your_ct0_token
```
**Verify it's working:** **Verify it's working:**
```bash ```bash
@@ -930,13 +928,13 @@ This example shows /last30days discovering **emerging developer workflows** - re
## Requirements ## Requirements
- **OpenAI API key** - For Reddit research (uses web search via Responses API) - **OpenAI auth** - For Reddit research (uses web search via Responses API). Use `OPENAI_API_KEY` or `codex login`.
- **Node.js 22+** - For X search (bundled Twitter GraphQL client) - **Node.js 22+** - For X search (bundled Twitter GraphQL client)
- **X session** - Be logged into x.com in your browser, or set `AUTH_TOKEN`/`CT0` env vars - **Bundled X auth** - Set `AUTH_TOKEN` and `CT0` for popup-free local X search
- **xAI API key** (optional fallback) - If the bundled search can't authenticate, falls back to xAI's Grok API - **Alternate X backend** - Set `XAI_API_KEY` if bundled X auth is not configured
- **yt-dlp** (optional) - For YouTube search + transcript extraction. Install via `brew install yt-dlp` or `pip install yt-dlp`. When present, automatically searches YouTube and extracts video transcripts as an additional source. - **yt-dlp** (optional) - For YouTube search + transcript extraction. Install via `brew install yt-dlp` or `pip install yt-dlp`. When present, automatically searches YouTube and extracts video transcripts as an additional source.
At least one API key is required. X search works automatically if you're logged into x.com in your browser. YouTube search activates automatically when yt-dlp is in your PATH. At least one auth path is required. Reddit needs OpenAI auth. X needs either `AUTH_TOKEN` plus `CT0` or `XAI_API_KEY`. YouTube search activates automatically when yt-dlp is in your PATH.
## Troubleshooting ## Troubleshooting
@@ -1143,7 +1141,7 @@ Inspired by [Peter Steinberger](https://x.com/steipete)'s yt-dlp + [summarize](h
### Bundled X search (v2.1) ### Bundled X search (v2.1)
**X search is fully self-contained** - No external `bird` CLI or xAI API key needed. /last30days bundles a vendored subset of Bird's Twitter GraphQL client (MIT licensed, by Peter Steinberger). Just be logged into x.com in your browser and it auto-detects your session. Falls back to xAI API if bundled search can't authenticate. **X search is fully self-contained** - No external `bird` CLI install needed. /last30days bundles a vendored subset of Bird's Twitter GraphQL client (MIT licensed, by Peter Steinberger). With Node.js 22+ plus `AUTH_TOKEN` and `CT0`, it runs locally without browser-cookie prompts. Falls back to xAI API if bundled auth is not configured.
### Everything else (v2.1) ### Everything else (v2.1)
@@ -1190,7 +1188,7 @@ Thanks to the contributors who helped shape V2:
| `api.scrapecreators.com` | Search query (Reddit + TikTok + Instagram) | SCRAPECREATORS_API_KEY | | `api.scrapecreators.com` | Search query (Reddit + TikTok + Instagram) | SCRAPECREATORS_API_KEY |
| `api.openai.com` | Search query (legacy Reddit fallback) | OPENAI_API_KEY | | `api.openai.com` | Search query (legacy Reddit fallback) | OPENAI_API_KEY |
| `reddit.com` | Thread URLs for enrichment | None (public JSON) | | `reddit.com` | Thread URLs for enrichment | None (public JSON) |
| Twitter GraphQL / `api.x.ai` | Search query | Browser cookies or XAI_API_KEY | | Twitter GraphQL / `api.x.ai` | Search query | AUTH_TOKEN/CT0 or XAI_API_KEY |
| `youtube.com` (via yt-dlp) | Search query | None (public search) | | `youtube.com` (via yt-dlp) | Search query | None (public search) |
| `hn.algolia.com` | Search query | None (public API) | | `hn.algolia.com` | Search query | None (public API) |
| `gamma-api.polymarket.com` | Search query | None (public API) | | `gamma-api.polymarket.com` | Search query | None (public API) |
+11 -16
View File
@@ -9,7 +9,7 @@ User: /last30days "kanye west"
↓ ↓ (concurrent via ThreadPoolExecutor) ↓ ↓ (concurrent via ThreadPoolExecutor)
[REDDIT] [X/TWITTER] [REDDIT] [X/TWITTER]
↓ ↓ ↓ ↓
OpenAI Bird CLI or OpenAI Bundled Bird or
API xAI API API xAI API
↓ ↓ ↓ ↓
Parse Parse Parse Parse
@@ -95,11 +95,11 @@ No API key needed. This returns the actual thread data:
X search has **two backends** — the skill auto-detects which to use. X search has **two backends** — the skill auto-detects which to use.
### Priority: Bird CLI (free) → xAI API (paid) ### Priority: Bundled Bird (env auth) → xAI API (paid)
```python ```python
if bird_installed and bird_authenticated: if node_available and AUTH_TOKEN and CT0:
use Bird CLI # Free, uses your X login use bundled Bird # Free, popup-free, env-authenticated
elif XAI_API_KEY: elif XAI_API_KEY:
use xAI API # Paid, uses grok-4-1-fast use xAI API # Paid, uses grok-4-1-fast
else: else:
@@ -128,20 +128,15 @@ The prompt asks grok to return JSON with:
- `engagement`: `{ likes, reposts, replies, quotes }` - `engagement`: `{ likes, reposts, replies, quotes }`
- `why_relevant`, `relevance` score - `why_relevant`, `relevance` score
**Engagement data comes from grok's x_search tool** it has direct access to X's data. **Engagement data comes from grok's x_search tool** - it has direct access to X's data.
### Backend 2: Bird CLI (free alternative) ### Backend 2: Bundled Bird client (free alternative)
Bird is a CLI tool (`npm install -g @steipete/bird`) that uses your X login. The repo vendors a search-only subset of Bird's Twitter GraphQL client and shells out to it with Node.js. No global `bird` install is required. The Python wrapper passes `AUTH_TOKEN` and `CT0` via env, which keeps normal local runs headless and avoids browser-cookie prompts.
**Command:** **Bundled Bird returns raw X API data** - likes, reposts, replies are real engagement metrics from X's API, not estimates.
```bash
bird search "{topic} since:{from_date}" -n 30 --json
```
**Bird returns raw X API data** — likes, reposts, replies are real engagement metrics from X's API, not estimates. | Metric | Bundled Bird | xAI API |
| Metric | Bird CLI | xAI API |
|---|---|---| |---|---|---|
| Post text | Real | Real | | Post text | Real | Real |
| Likes/reposts | Real (X API) | Real (x_search tool) | | Likes/reposts | Real (X API) | Real (x_search tool) |
@@ -151,7 +146,7 @@ bird search "{topic} since:{from_date}" -n 30 --json
### Depth settings ### Depth settings
| Depth | xAI posts | Bird results | xAI timeout | Bird timeout | | Depth | xAI posts | Bundled Bird results | xAI timeout | Bird timeout |
|---|---|---|---|---| |---|---|---|---|---|
| `--quick` | 8-12 | 12 | 90s | 30s | | `--quick` | 8-12 | 12 | 90s | 30s |
| default | 20-30 | 30 | 120s | 45s | | default | 20-30 | 30 | 120s | 45s |
@@ -192,7 +187,7 @@ After both searches complete:
| `scripts/lib/openai_reddit.py` | Reddit search via OpenAI Responses API | | `scripts/lib/openai_reddit.py` | Reddit search via OpenAI Responses API |
| `scripts/lib/reddit_enrich.py` | Fetch real engagement data from Reddit JSON API | | `scripts/lib/reddit_enrich.py` | Fetch real engagement data from Reddit JSON API |
| `scripts/lib/xai_x.py` | X search via xAI API | | `scripts/lib/xai_x.py` | X search via xAI API |
| `scripts/lib/bird_x.py` | X search via Bird CLI (free) | | `scripts/lib/bird_x.py` | X search via bundled Bird client (free) |
| `scripts/lib/models.py` | Auto-select best available model | | `scripts/lib/models.py` | Auto-select best available model |
| `scripts/lib/env.py` | API key loading, source detection | | `scripts/lib/env.py` | API key loading, source detection |
| `scripts/lib/http.py` | HTTP transport with retries | | `scripts/lib/http.py` | HTTP transport with retries |
+18 -20
View File
@@ -13,7 +13,7 @@ YouTube transcripts are the second headline feature. Inspired by Peter Steinberg
**New in V2.1 — two headline features:** **New in V2.1 — two headline features:**
- **YouTube transcripts as a 4th source.** When yt-dlp is installed, /last30days automatically searches YouTube, grabs view counts, and extracts auto-generated transcripts from the top videos. A 20-minute review contains 10x the signal of a tweet — now the skill reads it. Inspired by @steipete's yt-dlp + summarize toolchain. - **YouTube transcripts as a 4th source.** When yt-dlp is installed, /last30days automatically searches YouTube, grabs view counts, and extracts auto-generated transcripts from the top videos. A 20-minute review contains 10x the signal of a tweet — now the skill reads it. Inspired by @steipete's yt-dlp + summarize toolchain.
- **X search is fully bundled.** No external `bird` CLI or xAI API key needed. Just Node.js 22+ and your browser cookies. Uses a vendored subset of Bird's Twitter GraphQL client (MIT licensed, originally by @steipete). - **X search is fully bundled.** No external `bird` CLI install needed. Add `AUTH_TOKEN` and `CT0` once, and the vendored Bird client runs locally without browser-cookie prompts. `XAI_API_KEY` remains an optional fallback.
--- ---
@@ -21,20 +21,18 @@ YouTube transcripts are the second headline feature. Inspired by Peter Steinberg
### X Search Authentication ### X Search Authentication
X search reads your existing browser cookies — no API keys or login commands needed. X search prefers explicit env auth. This keeps local runs headless and avoids browser-cookie and macOS Keychain prompts.
**Safari (recommended on Mac):** Just be logged into x.com. No setup needed. **Recommended setup:** While logged into x.com once, open browser dev tools and copy the `auth_token` and `ct0` cookies for `x.com`.
**Chrome:** Works, but macOS will prompt you to allow Keychain access the first time. Click "Allow" (or "Always Allow" to stop future prompts). Save them as `AUTH_TOKEN` and `CT0` in `~/.config/last30days/.env` or `.claude/last30days.env`:
**Firefox:** Just be logged into x.com. No setup needed.
**Manual fallback:** If cookie auto-detection doesn't work, set these env vars (grab them from your browser's dev tools → Application → Cookies → x.com):
```bash ```bash
export AUTH_TOKEN=your_auth_token AUTH_TOKEN=your_auth_token
export CT0=your_ct0_token CT0=your_ct0_token
``` ```
**xAI fallback:** If you do not want to provide `AUTH_TOKEN` and `CT0`, set `XAI_API_KEY` and use xAI's `x_search` backend instead.
**Verify it's working:** **Verify it's working:**
```bash ```bash
node ~/.claude/skills/last30days/scripts/lib/vendor/bird-search/bird-search.mjs --whoami node ~/.claude/skills/last30days/scripts/lib/vendor/bird-search/bird-search.mjs --whoami
@@ -44,8 +42,10 @@ node ~/.claude/skills/last30days/scripts/lib/vendor/bird-search/bird-search.mjs
## README: Install block env line ## README: Install block env line
``` ```bash
XAI_API_KEY=xai-... # optional — cookie auth is default for X search AUTH_TOKEN=... # recommended for X search
CT0=... # recommended for X search
XAI_API_KEY=xai-... # optional X fallback
``` ```
--- ---
@@ -60,15 +60,13 @@ XAI_API_KEY=xai-... # optional — cookie auth is default for X search
## GitHub issue #19 response (post AFTER publishing) ## GitHub issue #19 response (post AFTER publishing)
> Thanks for reporting this. Bird CLI was deprecated and the GitHub repo was deleted steipete was asked to take it down. > Thanks for reporting this. Bird CLI was deprecated and the GitHub repo was deleted. steipete was asked to take it down.
> >
> The good news: you don't need Bird anymore. v2.1 (just shipped) bundles X search directly — no external CLI, no `npm install`, no brew. Just Node.js 22+ and your browser cookies. > The good news: you don't need Bird anymore. v2.1 (just shipped) bundles X search directly. No external CLI, no `npm install`, no brew. Just Node.js 22+ plus `AUTH_TOKEN` and `CT0`, or `XAI_API_KEY` as fallback.
> >
> It also adds **YouTube as a 4th source** — when yt-dlp is installed, the skill automatically searches YouTube and extracts transcripts from the top videos. A 20-minute tutorial has 10x the signal of a tweet, and now the synthesis engine reads it. > It also adds **YouTube as a 4th source**. When yt-dlp is installed, the skill automatically searches YouTube and extracts transcripts from the top videos. A 20-minute tutorial has 10x the signal of a tweet, and now the synthesis engine reads it.
> >
> If you're on a Mac, Safari is the easiest path for X — just be logged into x.com. Chrome works too but macOS will prompt for Keychain access the first time. > The recommended setup is to copy `auth_token` and `ct0` from x.com once and store them as `AUTH_TOKEN` and `CT0` in your env. That avoids browser-cookie and Keychain prompts during normal runs.
>
> If cookie auto-detection doesn't work, you can set `AUTH_TOKEN` and `CT0` env vars manually (grab from browser dev tools → Application → Cookies → x.com).
> >
> The xAI API (`XAI_API_KEY`) also still works as a fallback. > The xAI API (`XAI_API_KEY`) also still works as a fallback.
@@ -82,7 +80,7 @@ XAI_API_KEY=xai-... # optional — cookie auth is default for X search
Two new features: Two new features:
→ YouTube transcripts as a 4th source (yt-dlp) → YouTube transcripts as a 4th source (yt-dlp)
→ X search fully bundled (no bird CLI needed) → X search fully bundled (no bird CLI install needed)
Research any topic across Reddit, X, YouTube & web in one command. Research any topic across Reddit, X, YouTube & web in one command.
@@ -100,7 +98,7 @@ When yt-dlp is installed, the skill searches YouTube, grabs view counts, and ext
### Thread version (post 2) ### Thread version (post 2)
2️⃣ X search is fully bundled 2️⃣ X search is fully bundled
Bird CLI was deprecated. Instead of requiring an external tool, v2.1 vendors a search-only subset. Just be logged into x.com in your browser. No npm install, no API keys. Bird CLI was deprecated. Instead of requiring an external tool, v2.1 vendors a search-only subset. Add `AUTH_TOKEN` and `CT0` once, then it runs locally with no npm install. `XAI_API_KEY` still works as fallback.
Both features inspired by @steipete's tooling. Both features inspired by @steipete's tooling.
+2 -2
View File
@@ -1621,8 +1621,8 @@ def main():
# Check available sources (accounting for Bird auto-detection) # Check available sources (accounting for Bird auto-detection)
available = env.get_available_sources(config) available = env.get_available_sources(config)
# Override available if Bird or ScrapeCreators provides X # Override available if Bird provides X
if x_source in ('bird', 'scrapecreators'): if x_source == 'bird':
if available == 'reddit': if available == 'reddit':
available = 'both' # Now have both Reddit + X available = 'both' # Now have both Reddit + X
elif available == 'reddit-web': elif available == 'reddit-web':
+5 -14
View File
@@ -346,7 +346,7 @@ def get_web_search_source(config: Dict[str, Any]) -> Optional[str]:
def get_missing_keys(config: Dict[str, Any]) -> str: def get_missing_keys(config: Dict[str, Any]) -> str:
"""Determine which sources are missing (accounting for Bird and ScrapeCreators). """Determine which sources are missing (accounting for Bird).
Returns: 'all', 'both', 'reddit', 'x', 'web', or 'none' Returns: 'all', 'both', 'reddit', 'x', 'web', or 'none'
""" """
@@ -358,8 +358,7 @@ def get_missing_keys(config: Dict[str, Any]) -> str:
from . import bird_x from . import bird_x
has_bird = bird_x.is_bird_installed() and bird_x.is_bird_authenticated() has_bird = bird_x.is_bird_installed() and bird_x.is_bird_authenticated()
has_sc_x = bool(config.get('SCRAPECREATORS_API_KEY')) has_x = has_xai or has_bird
has_x = has_xai or has_bird or has_sc_x
if has_reddit and has_x and has_web: if has_reddit and has_x and has_web:
return 'none' return 'none'
@@ -438,7 +437,9 @@ def validate_sources(requested: str, available: str, include_web: bool = False)
def get_x_source(config: Dict[str, Any]) -> Optional[str]: def get_x_source(config: Dict[str, Any]) -> Optional[str]:
"""Determine the best available X/Twitter source. """Determine the best available X/Twitter source.
Priority: Bird (free) → xAI (paid API) → ScrapeCreators (shared key) Priority: Bird (free) → xAI (paid API)
Keep X selection limited to documented, verified search backends.
Args: Args:
config: Configuration dict from get_config() config: Configuration dict from get_config()
@@ -446,7 +447,6 @@ def get_x_source(config: Dict[str, Any]) -> Optional[str]:
Returns: Returns:
'bird' if Bird is installed and authenticated, 'bird' if Bird is installed and authenticated,
'xai' if XAI_API_KEY is configured, 'xai' if XAI_API_KEY is configured,
'scrapecreators' if SCRAPECREATORS_API_KEY is configured,
None if no X source available. None if no X source available.
""" """
# Import here to avoid circular dependency # Import here to avoid circular dependency
@@ -462,10 +462,6 @@ def get_x_source(config: Dict[str, Any]) -> Optional[str]:
if config.get('XAI_API_KEY'): if config.get('XAI_API_KEY'):
return 'xai' return 'xai'
# Fall back to ScrapeCreators (same key as Reddit/TikTok/Instagram)
if config.get('SCRAPECREATORS_API_KEY'):
return 'scrapecreators'
return None return None
@@ -584,15 +580,11 @@ def get_x_source_status(config: Dict[str, Any]) -> Dict[str, Any]:
bird_status = bird_x.get_bird_status() bird_status = bird_x.get_bird_status()
xai_available = bool(config.get('XAI_API_KEY')) xai_available = bool(config.get('XAI_API_KEY'))
sc_available = bool(config.get('SCRAPECREATORS_API_KEY'))
# Determine active source # Determine active source
if bird_status["authenticated"]: if bird_status["authenticated"]:
source = 'bird' source = 'bird'
elif xai_available: elif xai_available:
source = 'xai' source = 'xai'
elif sc_available:
source = 'scrapecreators'
else: else:
source = None source = None
@@ -602,6 +594,5 @@ def get_x_source_status(config: Dict[str, Any]) -> Dict[str, Any]:
"bird_authenticated": bird_status["authenticated"], "bird_authenticated": bird_status["authenticated"],
"bird_username": bird_status["username"], "bird_username": bird_status["username"],
"xai_available": xai_available, "xai_available": xai_available,
"scrapecreators_available": sc_available,
"can_install_bird": bird_status["can_install"], "can_install_bird": bird_status["can_install"],
} }
+9 -10
View File
@@ -143,7 +143,7 @@ Just start with "last30" and talk to me like normal.
# Shorter promo for single missing key # Shorter promo for single missing key
PROMO_SINGLE_KEY = { PROMO_SINGLE_KEY = {
"reddit": "\n💡 You can unlock Reddit with an OpenAI API key or by running `codex login` — just ask me how.\n", "reddit": "\n💡 You can unlock Reddit with an OpenAI API key or by running `codex login` — just ask me how.\n",
"x": "\n💡 You can unlock X with an xAI API key — just ask me how.\n", "x": "\n💡 You can unlock X with AUTH_TOKEN/CT0 or XAI_API_KEY - just ask me how.\n",
} }
# Bird auth help (for local users with vendored Bird CLI) # Bird auth help (for local users with vendored Bird CLI)
@@ -151,16 +151,16 @@ BIRD_AUTH_HELP = f"""
{Colors.YELLOW}Bird authentication failed.{Colors.RESET} {Colors.YELLOW}Bird authentication failed.{Colors.RESET}
To fix this: To fix this:
1. Log into X (twitter.com) in Safari, Chrome, or Firefox 1. Add AUTH_TOKEN and CT0 to ~/.config/last30days/.env or .claude/last30days.env
2. Try again — Bird reads your browser cookies automatically. 2. Or set XAI_API_KEY for the xAI fallback backend
""" """
BIRD_AUTH_HELP_PLAIN = """ BIRD_AUTH_HELP_PLAIN = """
Bird authentication failed. Bird authentication failed.
To fix this: To fix this:
1. Log into X (twitter.com) in Safari, Chrome, or Firefox 1. Add AUTH_TOKEN and CT0 to ~/.config/last30days/.env or .claude/last30days.env
2. Try again — Bird reads your browser cookies automatically. 2. Or set XAI_API_KEY for the xAI fallback backend
""" """
# Spinner frames # Spinner frames
@@ -460,10 +460,9 @@ def show_diagnostic_banner(diag: dict):
label = f"Bird ({username})" if source == "bird" and username else source.upper() label = f"Bird ({username})" if source == "bird" and username else source.upper()
lines.append(f"{Colors.DIM}{Colors.RESET} {Colors.GREEN}✅ X/Twitter{Colors.RESET}{label} {Colors.DIM}{Colors.RESET}") lines.append(f"{Colors.DIM}{Colors.RESET} {Colors.GREEN}✅ X/Twitter{Colors.RESET}{label} {Colors.DIM}{Colors.RESET}")
else: else:
lines.append(f"{Colors.DIM}{Colors.RESET} {Colors.RED}❌ X/Twitter{Colors.RESET} — No Bird CLI or XAI_API_KEY {Colors.DIM}{Colors.RESET}") lines.append(f"{Colors.DIM}{Colors.RESET} {Colors.RED}❌ X/Twitter{Colors.RESET} — No X auth or fallback key {Colors.DIM}{Colors.RESET}")
if diag.get("bird_installed"): if diag.get("bird_installed"):
lines.append(f"{Colors.DIM}{Colors.RESET} └─ Bird installed but not authenticated {Colors.DIM}{Colors.RESET}") lines.append(f"{Colors.DIM}{Colors.RESET} └─ Add AUTH_TOKEN/CT0 or XAI_API_KEY {Colors.DIM}{Colors.RESET}")
lines.append(f"{Colors.DIM}{Colors.RESET} └─ Log into x.com in your browser, then retry {Colors.DIM}{Colors.RESET}")
else: else:
lines.append(f"{Colors.DIM}{Colors.RESET} └─ Needs Node.js 22+ (Bird is bundled) {Colors.DIM}{Colors.RESET}") lines.append(f"{Colors.DIM}{Colors.RESET} └─ Needs Node.js 22+ (Bird is bundled) {Colors.DIM}{Colors.RESET}")
@@ -507,9 +506,9 @@ def show_diagnostic_banner(diag: dict):
if has_x: if has_x:
lines.append("│ ✅ X/Twitter — available │") lines.append("│ ✅ X/Twitter — available │")
else: else:
lines.append("│ ❌ X/Twitter — No Bird CLI or XAI_API_KEY") lines.append("│ ❌ X/Twitter — No X auth or fallback key")
if diag.get("bird_installed"): if diag.get("bird_installed"):
lines.append("│ └─ Log into x.com in your browser, then retry") lines.append("│ └─ Add AUTH_TOKEN/CT0 or XAI_API_KEY ")
else: else:
lines.append("│ └─ Needs Node.js 22+ (Bird is bundled) │") lines.append("│ └─ Needs Node.js 22+ (Bird is bundled) │")
+25
View File
@@ -173,5 +173,30 @@ class TestFilePermissions(unittest.TestCase):
self.assertEqual(stderr.getvalue(), "") self.assertEqual(stderr.getvalue(), "")
class TestXSourceSelection(unittest.TestCase):
"""Tests for supported X backend selection."""
def test_get_x_source_ignores_scrapecreators_key(self):
config = {'SCRAPECREATORS_API_KEY': 'sc-key'}
with patch('lib.bird_x.is_bird_installed', return_value=False):
self.assertIsNone(env.get_x_source(config))
def test_get_x_source_status_ignores_scrapecreators_key(self):
config = {'SCRAPECREATORS_API_KEY': 'sc-key'}
bird_status = {
'installed': True,
'authenticated': False,
'username': None,
'can_install': False,
}
with patch('lib.bird_x.get_bird_status', return_value=bird_status):
status = env.get_x_source_status(config)
self.assertIsNone(status['source'])
self.assertFalse(status['xai_available'])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()