# 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