docs: Add Bird CLI implementation plan
12-task implementation plan for integrating Bird CLI as free X/Twitter search alternative. Includes detection, interactive install, search dispatch, and main flow integration. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,929 @@
|
|||||||
|
# Bird CLI Integration Implementation Plan
|
||||||
|
|
||||||
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||||
|
|
||||||
|
**Goal:** Add Bird CLI as a free, zero-config alternative to xAI for X/Twitter searches with interactive installation.
|
||||||
|
|
||||||
|
**Architecture:** New `bird_x.py` module handles Bird detection, installation prompts, and search. Modified `env.py` determines X source priority (Bird → xAI → WebSearch). Main script prompts for Bird install if not found.
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3, subprocess for Bird CLI calls, existing lib modules for normalization/scoring.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Create bird_x.py - Detection Functions
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `scripts/lib/bird_x.py`
|
||||||
|
|
||||||
|
**Step 1: Create the module with detection functions**
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Bird CLI client for X (Twitter) search."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
def _log(msg: str):
|
||||||
|
"""Log to stderr."""
|
||||||
|
sys.stderr.write(f"[Bird] {msg}\n")
|
||||||
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def is_bird_installed() -> bool:
|
||||||
|
"""Check if Bird CLI is installed."""
|
||||||
|
return shutil.which("bird") is not None
|
||||||
|
|
||||||
|
|
||||||
|
def is_bird_authenticated() -> Optional[str]:
|
||||||
|
"""Check if Bird is authenticated by running 'bird whoami'.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Username if authenticated, None otherwise.
|
||||||
|
"""
|
||||||
|
if not is_bird_installed():
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["bird", "whoami"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
if result.returncode == 0 and result.stdout.strip():
|
||||||
|
# Output is typically the username
|
||||||
|
return result.stdout.strip().split('\n')[0]
|
||||||
|
return None
|
||||||
|
except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def check_npm_available() -> bool:
|
||||||
|
"""Check if npm is available for installation."""
|
||||||
|
return shutil.which("npm") is not None
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Verify the module loads**
|
||||||
|
|
||||||
|
Run: `cd /Users/mvanhorn/last30days-skill-private && python3 -c "from scripts.lib import bird_x; print('OK')"`
|
||||||
|
Expected: `OK`
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add scripts/lib/bird_x.py
|
||||||
|
git commit -m "feat(bird): add detection functions for Bird CLI"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Add Bird Installation Functions
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `scripts/lib/bird_x.py`
|
||||||
|
|
||||||
|
**Step 1: Add installation function**
|
||||||
|
|
||||||
|
Add after `check_npm_available()`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def install_bird() -> Tuple[bool, str]:
|
||||||
|
"""Install Bird CLI via npm.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (success, message).
|
||||||
|
"""
|
||||||
|
if not check_npm_available():
|
||||||
|
return False, "npm not found. Install Node.js first, or install Bird manually: https://github.com/steipete/bird"
|
||||||
|
|
||||||
|
try:
|
||||||
|
_log("Installing Bird CLI...")
|
||||||
|
result = subprocess.run(
|
||||||
|
["npm", "install", "-g", "@steipete/bird"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=120,
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
return True, "Bird CLI installed successfully!"
|
||||||
|
else:
|
||||||
|
error = result.stderr.strip() or result.stdout.strip() or "Unknown error"
|
||||||
|
return False, f"Installation failed: {error}"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return False, "Installation timed out"
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"Installation error: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_bird_status() -> Dict[str, Any]:
|
||||||
|
"""Get comprehensive Bird status.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys: installed, authenticated, username, can_install
|
||||||
|
"""
|
||||||
|
installed = is_bird_installed()
|
||||||
|
username = is_bird_authenticated() if installed else None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"installed": installed,
|
||||||
|
"authenticated": username is not None,
|
||||||
|
"username": username,
|
||||||
|
"can_install": check_npm_available(),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Verify functions work**
|
||||||
|
|
||||||
|
Run: `cd /Users/mvanhorn/last30days-skill-private && python3 -c "from scripts.lib import bird_x; print(bird_x.get_bird_status())"`
|
||||||
|
Expected: Dict with installed/authenticated status
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add scripts/lib/bird_x.py
|
||||||
|
git commit -m "feat(bird): add installation and status functions"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Add Bird Search Function
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `scripts/lib/bird_x.py`
|
||||||
|
|
||||||
|
**Step 1: Add depth config and search function**
|
||||||
|
|
||||||
|
Add after imports at top:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Depth configurations: number of results to request
|
||||||
|
DEPTH_CONFIG = {
|
||||||
|
"quick": 12,
|
||||||
|
"default": 30,
|
||||||
|
"deep": 60,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add after `get_bird_status()`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def search_x(
|
||||||
|
topic: str,
|
||||||
|
from_date: str,
|
||||||
|
to_date: str,
|
||||||
|
depth: str = "default",
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Search X using Bird CLI.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
topic: Search topic
|
||||||
|
from_date: Start date (YYYY-MM-DD)
|
||||||
|
to_date: End date (YYYY-MM-DD)
|
||||||
|
depth: Research depth - "quick", "default", or "deep"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Raw Bird JSON response or error dict.
|
||||||
|
"""
|
||||||
|
count = DEPTH_CONFIG.get(depth, DEPTH_CONFIG["default"])
|
||||||
|
|
||||||
|
# Build command
|
||||||
|
cmd = [
|
||||||
|
"bird", "search",
|
||||||
|
topic,
|
||||||
|
"--since", from_date,
|
||||||
|
"-n", str(count),
|
||||||
|
"--json",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Adjust timeout based on depth
|
||||||
|
timeout = 30 if depth == "quick" else 45 if depth == "default" else 60
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
error = result.stderr.strip() or "Bird search failed"
|
||||||
|
return {"error": error, "items": []}
|
||||||
|
|
||||||
|
# Parse JSON output
|
||||||
|
output = result.stdout.strip()
|
||||||
|
if not output:
|
||||||
|
return {"items": []}
|
||||||
|
|
||||||
|
return json.loads(output)
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return {"error": "Search timed out", "items": []}
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
return {"error": f"Invalid JSON response: {e}", "items": []}
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e), "items": []}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Verify search function signature**
|
||||||
|
|
||||||
|
Run: `cd /Users/mvanhorn/last30days-skill-private && python3 -c "from scripts.lib import bird_x; import inspect; print(inspect.signature(bird_x.search_x))"`
|
||||||
|
Expected: `(topic: str, from_date: str, to_date: str, depth: str = 'default') -> Dict[str, Any]`
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add scripts/lib/bird_x.py
|
||||||
|
git commit -m "feat(bird): add search_x function"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Add Bird Response Parser
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `scripts/lib/bird_x.py`
|
||||||
|
|
||||||
|
**Step 1: Add parse function**
|
||||||
|
|
||||||
|
Add at end of file:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def parse_bird_response(response: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
|
"""Parse Bird response to match xai_x output format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response: Raw Bird JSON response
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of normalized item dicts matching xai_x.parse_x_response() format.
|
||||||
|
"""
|
||||||
|
items = []
|
||||||
|
|
||||||
|
# Check for errors
|
||||||
|
if "error" in response and response["error"]:
|
||||||
|
_log(f"Bird error: {response['error']}")
|
||||||
|
return items
|
||||||
|
|
||||||
|
# Bird returns a list of tweets directly or under a key
|
||||||
|
raw_items = response if isinstance(response, list) else response.get("items", response.get("tweets", []))
|
||||||
|
|
||||||
|
if not isinstance(raw_items, list):
|
||||||
|
return items
|
||||||
|
|
||||||
|
for i, tweet in enumerate(raw_items):
|
||||||
|
if not isinstance(tweet, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Extract URL - Bird uses permanent_url or we construct from id
|
||||||
|
url = tweet.get("permanent_url") or tweet.get("url", "")
|
||||||
|
if not url and tweet.get("id"):
|
||||||
|
screen_name = tweet.get("user", {}).get("screen_name", "")
|
||||||
|
if screen_name:
|
||||||
|
url = f"https://x.com/{screen_name}/status/{tweet['id']}"
|
||||||
|
|
||||||
|
if not url:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Parse date from created_at (e.g., "Wed Jan 15 14:30:00 +0000 2026")
|
||||||
|
date = None
|
||||||
|
created_at = tweet.get("created_at", "")
|
||||||
|
if created_at:
|
||||||
|
try:
|
||||||
|
# Try ISO format first
|
||||||
|
if "T" in created_at:
|
||||||
|
dt = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
|
||||||
|
else:
|
||||||
|
# Twitter format: "Wed Jan 15 14:30:00 +0000 2026"
|
||||||
|
dt = datetime.strptime(created_at, "%a %b %d %H:%M:%S %z %Y")
|
||||||
|
date = dt.strftime("%Y-%m-%d")
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Extract user info
|
||||||
|
user = tweet.get("user", {})
|
||||||
|
author_handle = user.get("screen_name", "") or tweet.get("author_handle", "")
|
||||||
|
|
||||||
|
# Build engagement dict
|
||||||
|
engagement = {
|
||||||
|
"likes": tweet.get("like_count") or tweet.get("favorite_count"),
|
||||||
|
"reposts": tweet.get("retweet_count"),
|
||||||
|
"replies": tweet.get("reply_count"),
|
||||||
|
"quotes": tweet.get("quote_count"),
|
||||||
|
}
|
||||||
|
# Convert to int where possible
|
||||||
|
for key in engagement:
|
||||||
|
if engagement[key] is not None:
|
||||||
|
try:
|
||||||
|
engagement[key] = int(engagement[key])
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
engagement[key] = None
|
||||||
|
|
||||||
|
# Build normalized item
|
||||||
|
item = {
|
||||||
|
"id": f"X{i+1}",
|
||||||
|
"text": str(tweet.get("text", tweet.get("full_text", ""))).strip()[:500],
|
||||||
|
"url": url,
|
||||||
|
"author_handle": author_handle.lstrip("@"),
|
||||||
|
"date": date,
|
||||||
|
"engagement": engagement if any(v is not None for v in engagement.values()) else None,
|
||||||
|
"why_relevant": "", # Bird doesn't provide relevance explanations
|
||||||
|
"relevance": 0.7, # Default relevance, let score.py re-rank
|
||||||
|
}
|
||||||
|
|
||||||
|
items.append(item)
|
||||||
|
|
||||||
|
return items
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Verify parser handles empty input**
|
||||||
|
|
||||||
|
Run: `cd /Users/mvanhorn/last30days-skill-private && python3 -c "from scripts.lib import bird_x; print(bird_x.parse_bird_response({}))"`
|
||||||
|
Expected: `[]`
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add scripts/lib/bird_x.py
|
||||||
|
git commit -m "feat(bird): add response parser matching xai_x format"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Add UI Functions for Bird Prompts
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `scripts/lib/ui.py`
|
||||||
|
|
||||||
|
**Step 1: Add Bird-related messages and prompts**
|
||||||
|
|
||||||
|
Add after `PROMO_SINGLE_KEY_PLAIN` dict (around line 128):
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Bird CLI prompts
|
||||||
|
BIRD_INSTALL_PROMPT = f"""
|
||||||
|
{Colors.CYAN}{Colors.BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{Colors.RESET}
|
||||||
|
{Colors.CYAN}🐦 FREE X/TWITTER SEARCH AVAILABLE{Colors.RESET}
|
||||||
|
|
||||||
|
Bird CLI provides free X search using your browser session (no API key needed).
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
BIRD_INSTALL_PROMPT_PLAIN = """
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
🐦 FREE X/TWITTER SEARCH AVAILABLE
|
||||||
|
|
||||||
|
Bird CLI provides free X search using your browser session (no API key needed).
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
BIRD_AUTH_HELP = f"""
|
||||||
|
{Colors.YELLOW}Bird authentication failed.{Colors.RESET}
|
||||||
|
|
||||||
|
To fix this:
|
||||||
|
1. Log into X (twitter.com) in Safari, Chrome, or Firefox
|
||||||
|
2. Run: {Colors.BOLD}bird check{Colors.RESET} to verify credentials
|
||||||
|
3. Try again
|
||||||
|
|
||||||
|
For manual setup, see: https://github.com/steipete/bird#authentication
|
||||||
|
"""
|
||||||
|
|
||||||
|
BIRD_AUTH_HELP_PLAIN = """
|
||||||
|
Bird authentication failed.
|
||||||
|
|
||||||
|
To fix this:
|
||||||
|
1. Log into X (twitter.com) in Safari, Chrome, or Firefox
|
||||||
|
2. Run: bird check to verify credentials
|
||||||
|
3. Try again
|
||||||
|
|
||||||
|
For manual setup, see: https://github.com/steipete/bird#authentication
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Add prompt functions to ProgressDisplay class**
|
||||||
|
|
||||||
|
Add these methods to the `ProgressDisplay` class (after `show_promo` method, around line 310):
|
||||||
|
|
||||||
|
```python
|
||||||
|
def prompt_bird_install(self) -> bool:
|
||||||
|
"""Prompt user to install Bird CLI.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if user wants to install, False otherwise.
|
||||||
|
"""
|
||||||
|
if IS_TTY:
|
||||||
|
sys.stderr.write(BIRD_INSTALL_PROMPT)
|
||||||
|
else:
|
||||||
|
sys.stderr.write(BIRD_INSTALL_PROMPT_PLAIN)
|
||||||
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = input("Install Bird CLI now? (y/n): ").strip().lower()
|
||||||
|
return response in ('y', 'yes')
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def show_bird_install_success(self, username: str):
|
||||||
|
"""Show Bird installation success message."""
|
||||||
|
msg = f"{Colors.GREEN}✓ Bird installed and authenticated as @{username}{Colors.RESET}\n" if IS_TTY else f"✓ Bird installed and authenticated as @{username}\n"
|
||||||
|
sys.stderr.write(msg)
|
||||||
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
def show_bird_install_failed(self, error: str):
|
||||||
|
"""Show Bird installation failure message."""
|
||||||
|
msg = f"{Colors.RED}✗ Bird installation failed: {error}{Colors.RESET}\n" if IS_TTY else f"✗ Bird installation failed: {error}\n"
|
||||||
|
sys.stderr.write(msg)
|
||||||
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
def show_bird_auth_help(self):
|
||||||
|
"""Show Bird authentication help."""
|
||||||
|
if IS_TTY:
|
||||||
|
sys.stderr.write(BIRD_AUTH_HELP)
|
||||||
|
else:
|
||||||
|
sys.stderr.write(BIRD_AUTH_HELP_PLAIN)
|
||||||
|
sys.stderr.flush()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Verify new methods exist**
|
||||||
|
|
||||||
|
Run: `cd /Users/mvanhorn/last30days-skill-private && python3 -c "from scripts.lib.ui import ProgressDisplay; p = ProgressDisplay('test', show_banner=False); print(hasattr(p, 'prompt_bird_install'))"`
|
||||||
|
Expected: `True`
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add scripts/lib/ui.py
|
||||||
|
git commit -m "feat(ui): add Bird CLI install prompts and auth help"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Update env.py with X Source Detection
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `scripts/lib/env.py`
|
||||||
|
|
||||||
|
**Step 1: Add get_x_source function**
|
||||||
|
|
||||||
|
Add at end of file:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_x_source(config: Dict[str, Any]) -> Optional[str]:
|
||||||
|
"""Determine the best available X/Twitter source.
|
||||||
|
|
||||||
|
Priority: Bird (free) → xAI (paid API)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Configuration dict from get_config()
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
'bird' if Bird is installed and authenticated,
|
||||||
|
'xai' if XAI_API_KEY is configured,
|
||||||
|
None if no X source available.
|
||||||
|
"""
|
||||||
|
# Import here to avoid circular dependency
|
||||||
|
from . import bird_x
|
||||||
|
|
||||||
|
# Check Bird first (free option)
|
||||||
|
if bird_x.is_bird_installed():
|
||||||
|
username = bird_x.is_bird_authenticated()
|
||||||
|
if username:
|
||||||
|
return 'bird'
|
||||||
|
|
||||||
|
# Fall back to xAI if key exists
|
||||||
|
if config.get('XAI_API_KEY'):
|
||||||
|
return 'xai'
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_x_source_status(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Get detailed X source status for UI decisions.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys: source, bird_installed, bird_authenticated,
|
||||||
|
bird_username, xai_available, can_install_bird
|
||||||
|
"""
|
||||||
|
from . import bird_x
|
||||||
|
|
||||||
|
bird_status = bird_x.get_bird_status()
|
||||||
|
xai_available = bool(config.get('XAI_API_KEY'))
|
||||||
|
|
||||||
|
# Determine active source
|
||||||
|
if bird_status["authenticated"]:
|
||||||
|
source = 'bird'
|
||||||
|
elif xai_available:
|
||||||
|
source = 'xai'
|
||||||
|
else:
|
||||||
|
source = None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"source": source,
|
||||||
|
"bird_installed": bird_status["installed"],
|
||||||
|
"bird_authenticated": bird_status["authenticated"],
|
||||||
|
"bird_username": bird_status["username"],
|
||||||
|
"xai_available": xai_available,
|
||||||
|
"can_install_bird": bird_status["can_install"],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Verify function works**
|
||||||
|
|
||||||
|
Run: `cd /Users/mvanhorn/last30days-skill-private && python3 -c "from scripts.lib import env; print(env.get_x_source_status(env.get_config()))"`
|
||||||
|
Expected: Dict with source status
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add scripts/lib/env.py
|
||||||
|
git commit -m "feat(env): add X source detection with Bird priority"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: Update __init__.py to Export bird_x
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `scripts/lib/__init__.py`
|
||||||
|
|
||||||
|
**Step 1: Add bird_x to imports**
|
||||||
|
|
||||||
|
Replace file contents with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# last30days library modules
|
||||||
|
from . import bird_x
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Verify import works**
|
||||||
|
|
||||||
|
Run: `cd /Users/mvanhorn/last30days-skill-private && python3 -c "from scripts.lib import bird_x; print('OK')"`
|
||||||
|
Expected: `OK`
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add scripts/lib/__init__.py
|
||||||
|
git commit -m "feat(lib): export bird_x module"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 8: Integrate Bird into Main Script - Part 1 (Setup Phase)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `scripts/last30days.py`
|
||||||
|
|
||||||
|
**Step 1: Add bird_x import**
|
||||||
|
|
||||||
|
Add `bird_x` to the imports from lib (around line 36):
|
||||||
|
|
||||||
|
```python
|
||||||
|
from lib import (
|
||||||
|
bird_x,
|
||||||
|
dates,
|
||||||
|
dedupe,
|
||||||
|
env,
|
||||||
|
http,
|
||||||
|
models,
|
||||||
|
normalize,
|
||||||
|
openai_reddit,
|
||||||
|
reddit_enrich,
|
||||||
|
render,
|
||||||
|
schema,
|
||||||
|
score,
|
||||||
|
ui,
|
||||||
|
websearch,
|
||||||
|
xai_x,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Add Bird setup function**
|
||||||
|
|
||||||
|
Add after the imports, before `load_fixture`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def setup_bird_if_needed(progress: ui.ProgressDisplay) -> Optional[str]:
|
||||||
|
"""Check Bird status and offer installation if needed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
'bird' if Bird is ready to use,
|
||||||
|
'declined' if user declined install,
|
||||||
|
None if Bird not available and couldn't be installed.
|
||||||
|
"""
|
||||||
|
status = bird_x.get_bird_status()
|
||||||
|
|
||||||
|
# Already working
|
||||||
|
if status["authenticated"]:
|
||||||
|
return 'bird'
|
||||||
|
|
||||||
|
# Installed but not authenticated
|
||||||
|
if status["installed"]:
|
||||||
|
progress.show_bird_auth_help()
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Not installed - offer to install if npm available
|
||||||
|
if status["can_install"]:
|
||||||
|
if progress.prompt_bird_install():
|
||||||
|
success, message = bird_x.install_bird()
|
||||||
|
if success:
|
||||||
|
# Check if auth works now
|
||||||
|
username = bird_x.is_bird_authenticated()
|
||||||
|
if username:
|
||||||
|
progress.show_bird_install_success(username)
|
||||||
|
return 'bird'
|
||||||
|
else:
|
||||||
|
progress.show_bird_auth_help()
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
progress.show_bird_install_failed(message)
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return 'declined'
|
||||||
|
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Verify script still loads**
|
||||||
|
|
||||||
|
Run: `cd /Users/mvanhorn/last30days-skill-private && python3 -c "import scripts.last30days; print('OK')"`
|
||||||
|
Expected: `OK`
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add scripts/last30days.py
|
||||||
|
git commit -m "feat(main): add Bird setup function"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 9: Integrate Bird into Main Script - Part 2 (Search Dispatch)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `scripts/last30days.py`
|
||||||
|
|
||||||
|
**Step 1: Modify _search_x function to support Bird**
|
||||||
|
|
||||||
|
Replace the `_search_x` function (around line 119-159) with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _search_x(
|
||||||
|
topic: str,
|
||||||
|
config: dict,
|
||||||
|
selected_models: dict,
|
||||||
|
from_date: str,
|
||||||
|
to_date: str,
|
||||||
|
depth: str,
|
||||||
|
mock: bool,
|
||||||
|
x_source: str = "xai",
|
||||||
|
) -> tuple:
|
||||||
|
"""Search X via Bird CLI or xAI (runs in thread).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x_source: 'bird' or 'xai' - which backend to use
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (x_items, raw_response, error)
|
||||||
|
"""
|
||||||
|
raw_response = None
|
||||||
|
x_error = None
|
||||||
|
|
||||||
|
if mock:
|
||||||
|
raw_response = load_fixture("xai_sample.json")
|
||||||
|
x_items = xai_x.parse_x_response(raw_response or {})
|
||||||
|
return x_items, raw_response, x_error
|
||||||
|
|
||||||
|
# Use Bird if specified
|
||||||
|
if x_source == "bird":
|
||||||
|
try:
|
||||||
|
raw_response = bird_x.search_x(
|
||||||
|
topic,
|
||||||
|
from_date,
|
||||||
|
to_date,
|
||||||
|
depth=depth,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raw_response = {"error": str(e)}
|
||||||
|
x_error = f"{type(e).__name__}: {e}"
|
||||||
|
|
||||||
|
x_items = bird_x.parse_bird_response(raw_response or {})
|
||||||
|
|
||||||
|
# Check for error in response
|
||||||
|
if raw_response and raw_response.get("error") and not x_error:
|
||||||
|
x_error = raw_response["error"]
|
||||||
|
|
||||||
|
return x_items, raw_response, x_error
|
||||||
|
|
||||||
|
# Use xAI (original behavior)
|
||||||
|
try:
|
||||||
|
raw_response = xai_x.search_x(
|
||||||
|
config["XAI_API_KEY"],
|
||||||
|
selected_models["xai"],
|
||||||
|
topic,
|
||||||
|
from_date,
|
||||||
|
to_date,
|
||||||
|
depth=depth,
|
||||||
|
)
|
||||||
|
except http.HTTPError as e:
|
||||||
|
raw_response = {"error": str(e)}
|
||||||
|
x_error = f"API error: {e}"
|
||||||
|
except Exception as e:
|
||||||
|
raw_response = {"error": str(e)}
|
||||||
|
x_error = f"{type(e).__name__}: {e}"
|
||||||
|
|
||||||
|
x_items = xai_x.parse_x_response(raw_response or {})
|
||||||
|
|
||||||
|
return x_items, raw_response, x_error
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Update run_research to accept x_source parameter**
|
||||||
|
|
||||||
|
Find the `run_research` function signature (around line 161) and add `x_source` parameter:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def run_research(
|
||||||
|
topic: str,
|
||||||
|
sources: str,
|
||||||
|
config: dict,
|
||||||
|
selected_models: dict,
|
||||||
|
from_date: str,
|
||||||
|
to_date: str,
|
||||||
|
depth: str = "default",
|
||||||
|
mock: bool = False,
|
||||||
|
progress: ui.ProgressDisplay = None,
|
||||||
|
x_source: str = "xai",
|
||||||
|
) -> tuple:
|
||||||
|
```
|
||||||
|
|
||||||
|
Then update the `_search_x` call inside (around line 218-222) to pass `x_source`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
x_future = executor.submit(
|
||||||
|
_search_x, topic, config, selected_models,
|
||||||
|
from_date, to_date, depth, mock, x_source
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Verify script syntax is valid**
|
||||||
|
|
||||||
|
Run: `cd /Users/mvanhorn/last30days-skill-private && python3 -m py_compile scripts/last30days.py && echo "OK"`
|
||||||
|
Expected: `OK`
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add scripts/last30days.py
|
||||||
|
git commit -m "feat(main): dispatch X search to Bird or xAI"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 10: Integrate Bird into Main Script - Part 3 (Main Function)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `scripts/last30days.py`
|
||||||
|
|
||||||
|
**Step 1: Update main() to check Bird before research**
|
||||||
|
|
||||||
|
In the `main()` function, after loading config and before checking available sources (around line 345-355), add Bird setup:
|
||||||
|
|
||||||
|
Find this section:
|
||||||
|
```python
|
||||||
|
# Load config
|
||||||
|
config = env.get_config()
|
||||||
|
|
||||||
|
# Check available sources
|
||||||
|
available = env.get_available_sources(config)
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with:
|
||||||
|
```python
|
||||||
|
# Load config
|
||||||
|
config = env.get_config()
|
||||||
|
|
||||||
|
# Initialize progress display early for Bird prompts
|
||||||
|
progress = ui.ProgressDisplay(args.topic, show_banner=True)
|
||||||
|
|
||||||
|
# Check Bird availability and offer install if needed
|
||||||
|
x_source_status = env.get_x_source_status(config)
|
||||||
|
x_source = x_source_status["source"]
|
||||||
|
|
||||||
|
# If no X source and Bird can be installed, offer it
|
||||||
|
if x_source is None and x_source_status["can_install_bird"]:
|
||||||
|
bird_result = setup_bird_if_needed(progress)
|
||||||
|
if bird_result == 'bird':
|
||||||
|
x_source = 'bird'
|
||||||
|
# Refresh status
|
||||||
|
x_source_status = env.get_x_source_status(config)
|
||||||
|
|
||||||
|
# Check available sources (now accounting for Bird)
|
||||||
|
available = env.get_available_sources(config)
|
||||||
|
|
||||||
|
# Override available if Bird is ready
|
||||||
|
if x_source == 'bird':
|
||||||
|
if available == 'reddit':
|
||||||
|
available = 'both' # Now have both Reddit + X (via Bird)
|
||||||
|
elif available == 'web':
|
||||||
|
available = 'x' # Now have X via Bird
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Remove duplicate progress initialization**
|
||||||
|
|
||||||
|
Find and remove the later `progress = ui.ProgressDisplay(...)` line (around line 371) since we now create it earlier.
|
||||||
|
|
||||||
|
**Step 3: Pass x_source to run_research**
|
||||||
|
|
||||||
|
Find the `run_research` call (around line 413) and add `x_source` parameter:
|
||||||
|
|
||||||
|
```python
|
||||||
|
reddit_items, x_items, web_needed, raw_openai, raw_xai, raw_reddit_enriched, reddit_error, x_error = run_research(
|
||||||
|
args.topic,
|
||||||
|
sources,
|
||||||
|
config,
|
||||||
|
selected_models,
|
||||||
|
from_date,
|
||||||
|
to_date,
|
||||||
|
depth,
|
||||||
|
args.mock,
|
||||||
|
progress,
|
||||||
|
x_source=x_source or "xai",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4: Verify script runs with --help**
|
||||||
|
|
||||||
|
Run: `cd /Users/mvanhorn/last30days-skill-private && python3 scripts/last30days.py --help`
|
||||||
|
Expected: Help text displays without errors
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add scripts/last30days.py
|
||||||
|
git commit -m "feat(main): integrate Bird setup into main flow"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 11: Test End-to-End with Mock Mode
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- None (testing only)
|
||||||
|
|
||||||
|
**Step 1: Test mock mode still works**
|
||||||
|
|
||||||
|
Run: `cd /Users/mvanhorn/last30days-skill-private && python3 scripts/last30days.py "Claude Code" --mock --emit=compact 2>&1 | head -20`
|
||||||
|
Expected: Output showing research results without errors
|
||||||
|
|
||||||
|
**Step 2: Test Bird detection (informational)**
|
||||||
|
|
||||||
|
Run: `cd /Users/mvanhorn/last30days-skill-private && python3 -c "from scripts.lib import env; import json; print(json.dumps(env.get_x_source_status(env.get_config()), indent=2))"`
|
||||||
|
Expected: JSON showing current Bird/xAI status
|
||||||
|
|
||||||
|
**Step 3: Commit any fixes if needed, then final commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -A
|
||||||
|
git commit -m "feat(bird): complete Bird CLI integration
|
||||||
|
|
||||||
|
- Add bird_x.py module for Bird CLI detection, install, and search
|
||||||
|
- Add UI prompts for interactive Bird installation
|
||||||
|
- Update env.py with X source priority (Bird > xAI)
|
||||||
|
- Integrate Bird into main research flow
|
||||||
|
- Bird uses browser cookies (free, no API key needed)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 12: Push to Private Repo
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- None (git only)
|
||||||
|
|
||||||
|
**Step 1: Push all changes**
|
||||||
|
|
||||||
|
Run: `cd /Users/mvanhorn/last30days-skill-private && git push origin main`
|
||||||
|
Expected: Changes pushed to private repo
|
||||||
|
|
||||||
|
**Step 2: Verify commit history**
|
||||||
|
|
||||||
|
Run: `cd /Users/mvanhorn/last30days-skill-private && git log --oneline -10`
|
||||||
|
Expected: Shows Bird integration commits
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
After completing all tasks, the skill will:
|
||||||
|
|
||||||
|
1. Check if Bird CLI is installed on startup
|
||||||
|
2. If not installed but npm available, prompt user to install
|
||||||
|
3. If installed, verify authentication via `bird whoami`
|
||||||
|
4. If authenticated, use Bird for all X searches (free)
|
||||||
|
5. If not, fall back to xAI (if key exists) or WebSearch
|
||||||
|
6. Output format identical regardless of backend used
|
||||||
Reference in New Issue
Block a user