Files
last30days-skill/docs/plans/2026-02-07-feat-bundle-bird-x-search-plan.md
T
Matt Van Horn 31313c69ac v2.1: Bundle Bird X search - no external CLI needed
Vendor Bird's Twitter GraphQL search client directly into /last30days,
eliminating the dependency on `npm install -g @steipete/bird`. X search
now works out of the box with just Node.js 22+ and browser cookies.

- Add vendored bird-search.mjs wrapper (search-only subset of Bird v0.8.0)
- Vendor @steipete/sweet-cookie for browser cookie extraction
- Update bird_x.py to call vendored Node.js module instead of `bird` binary
- Update README.md and SKILL.md for v2.1 (remove Bird CLI install steps)
- Include Bird's MIT LICENSE in vendor directory

The fallback chain is: vendored search -> xAI API key -> web-only mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 17:16:04 -08:00

9.3 KiB

title, type, date
title type date
feat: Bundle Bird X search client to eliminate npm dependency feat 2026-02-07

feat: Bundle Bird X search client to eliminate npm dependency

Overview

Replace the subprocess.run(["bird", "search", ...]) dependency in bird_x.py with a vendored Node.js module that calls Twitter's GraphQL search API directly. This eliminates the need for users to npm install -g @steipete/bird and protects against the package being removed from npm.

Bird is MIT-licensed. We have the full compiled package archived at vendor/steipete-bird-0.8.0.tgz and forked to github.com/mvanhorn/bird-cli-archive.

Problem Statement

@steipete deleted Bird's GitHub repo on 2026-02-07. The npm package still works today, but if he unpublishes it from npm:

  • New users can't npm install -g @steipete/bird
  • The bird binary disappears from PATH on fresh installs
  • bird_x.py returns 0 X results for everyone without an xAI API key
  • /last30days V2's headline feature ("free X search") stops working for new users

Proposed Solution

Vendor Bird's search-only subset as a Node.js module inside /last30days, called from Python via subprocess.run(["node", ...]).

This is the minimal-change approach:

  • Keep Python as the orchestrator (bird_x.py stays mostly the same)
  • Replace subprocess.run(["bird", "search", ...]) with subprocess.run(["node", "vendor/bird-search.mjs", ...])
  • Extract only the search-related code from Bird (not posting, bookmarks, lists, etc.)
  • Cookie auth stays the same (environment variables or browser extraction)

Why not rewrite in pure Python?

Bird's search client uses Twitter's internal GraphQL API with:

  • Rotating QueryIDs (hardcoded + runtime refresh from x.com)
  • Specific request header construction (bearer token, csrf, client UUIDs)
  • Cursor-based pagination with Twitter-specific response parsing
  • The @steipete/sweet-cookie dependency for browser cookie extraction

Porting all of this to Python is ~1000 lines of fragile reverse-engineering. Vendoring the working JS code is faster, safer, and easier to maintain since the archive includes source maps for debugging.

Technical Approach

What we need from Bird

Only 8 files from dist/lib/ (out of 30+):

  1. twitter-client-base.js - HTTP client, auth headers, rate limiting
  2. twitter-client-search.js - Search mixin (the core feature)
  3. twitter-client-utils.js - Tweet parsing, cursor extraction
  4. twitter-client-constants.js - API endpoints, QueryIDs
  5. twitter-client-types.js - TypeScript type stubs
  6. cookies.js - Cookie resolution (env vars, browser extraction)
  7. runtime-query-ids.js - QueryID refresh from x.com
  8. paginate-cursor.js - Cursor pagination helper

Plus:

  • features.json - GraphQL feature flags
  • query-ids.json - Hardcoded QueryID fallbacks

What we DON'T need

Posting, bookmarks, lists, timelines, engagement, follow, media, news, user lookup, user tweets - all the non-search mixins. This cuts the vendored code roughly in half.

Architecture

scripts/
  lib/
    bird_x.py              # MODIFIED - calls node instead of bird binary
    vendor/
      bird-search/
        bird-search.mjs    # NEW - thin CLI wrapper, ~40 lines
        lib/                # VENDORED - subset of Bird's dist/lib/
          twitter-client-base.js
          twitter-client-search.js
          twitter-client-utils.js
          twitter-client-constants.js
          twitter-client-types.js
          cookies.js
          runtime-query-ids.js
          paginate-cursor.js
          features.json
          query-ids.json
        node_modules/       # VENDORED - sweet-cookie only
          @steipete/
            sweet-cookie/
        package.json        # Minimal, points to bird-search.mjs
        LICENSE             # Bird's MIT license (required by MIT terms)

Implementation

1. Create bird-search.mjs wrapper (~40 lines)

A minimal Node.js script that:

  • Accepts: node bird-search.mjs <query> --count <n> --json
  • Creates a TwitterClient with search mixin only
  • Resolves cookies (env vars first, then browser extraction)
  • Calls client.search(query, count)
  • Outputs JSON to stdout
  • Exits with code 0 on success, 1 on error

This replaces the full bird CLI binary. Same interface, fraction of the code.

2. Modify bird_x.py - change subprocess target

# BEFORE (current)
cmd = ["bird", "search", query, "-n", str(count), "--json"]

# AFTER (vendored)
bird_search = Path(__file__).parent / "vendor" / "bird-search" / "bird-search.mjs"
cmd = ["node", bird_search, query, "--count", str(count), "--json"]

Same subprocess pattern. Same JSON output format. Minimal diff.

3. Update auth check functions

# BEFORE
def is_bird_installed() -> bool:
    return shutil.which("bird") is not None

# AFTER
def is_bird_installed() -> bool:
    bird_search = Path(__file__).parent / "vendor" / "bird-search" / "bird-search.mjs"
    return bird_search.exists() and shutil.which("node") is not None

is_bird_authenticated() changes from bird whoami to a quick Node.js cookie check or environment variable check.

install_bird() becomes a no-op (already vendored) or removes itself entirely.

@steipete/sweet-cookie is the only runtime dependency. It handles browser cookie extraction on macOS/Linux. Options:

Option A (recommended): Vendor sweet-cookie into vendor/bird-search/node_modules/. It's small (one file). This makes the skill fully self-contained with zero npm installs.

Option B: Fall back to environment variables only (no browser cookie extraction). Users would need to manually set AUTH_TOKEN and CT0 env vars. Simpler but worse UX.

Recommend Option A - vendor it.

5. Update user-facing docs

  • README.md - Remove "Install Bird CLI" section, replace with "Requires Node.js 22+"
  • SKILL.md - Remove Bird CLI installation instructions
  • Keep the fallback chain: vendored Bird search -> xAI API key -> web-only

Acceptance Criteria

  • bird_x.py calls vendored Node.js module instead of bird binary
  • search_x() returns identical JSON format (no downstream changes needed)
  • search_handles() works with vendored module
  • Cookie auth works via environment variables (AUTH_TOKEN, CT0)
  • Cookie auth works via browser extraction (sweet-cookie)
  • is_bird_installed() checks for vendored module + Node.js
  • install_bird() removed or returns success immediately
  • No npm install -g @steipete/bird required anywhere
  • Bird's MIT LICENSE included in vendor directory
  • README updated to remove Bird CLI install steps
  • SKILL.md updated to remove Bird CLI references
  • Works on macOS (primary) and Linux
  • Fallback to xAI API key still works if vendored search fails

Files Changed

File Action Description
scripts/lib/bird_x.py MODIFY Replace ["bird", ...] subprocess calls with ["node", "vendor/bird-search/bird-search.mjs", ...]
scripts/lib/vendor/bird-search/bird-search.mjs CREATE Thin Node.js wrapper that imports Bird's search client and outputs JSON
scripts/lib/vendor/bird-search/lib/*.js VENDOR 8 files from Bird's dist/lib/ (search subset only)
scripts/lib/vendor/bird-search/lib/features.json VENDOR GraphQL feature flags
scripts/lib/vendor/bird-search/lib/query-ids.json VENDOR Hardcoded QueryID fallbacks
scripts/lib/vendor/bird-search/node_modules/ VENDOR sweet-cookie package
scripts/lib/vendor/bird-search/package.json CREATE Minimal package.json for module resolution
scripts/lib/vendor/bird-search/LICENSE COPY Bird's MIT license
README.md MODIFY Remove Bird CLI install section, add Node.js 22+ requirement
SKILL.md MODIFY Remove Bird CLI references

Dependencies & Risks

Node.js 22+ required - Users who had Bird CLI already have Node.js. This is not a new dependency, just a version requirement. Claude Code environments typically have Node.js.

Twitter API changes - The GraphQL QueryIDs may rotate. Bird includes a runtime refresh mechanism (runtime-query-ids.js) that fetches new IDs from x.com. This is vendored and will continue working.

sweet-cookie platform support - Browser cookie extraction only works on macOS (Safari, Chrome, Firefox) and Linux (Chrome, Firefox). Windows users need manual env vars. This matches Bird CLI's existing behavior.

Legal - Bird is MIT licensed. MIT requires including the license notice in copies. We include LICENSE in the vendor directory. Using Twitter's internal API is the same legal gray area Bird always operated in - user accepted this when they used Bird.

What This Does NOT Change

  • Python remains the orchestrator - bird_x.py still does query construction, retry logic, response parsing
  • The fallback chain stays: vendored search -> xAI API -> web-only
  • Cookie auth mechanism is identical (env vars or browser extraction)
  • JSON output format is identical - no changes needed in score.py or format.py
  • xai_x.py is completely untouched

References

  • Bird CLI archive: github.com/mvanhorn/bird-cli-archive
  • Local vendor tarball: vendor/steipete-bird-0.8.0.tgz
  • Bird search implementation: bird-cli-archive/dist/lib/twitter-client-search.js
  • Current bird_x.py: scripts/lib/bird_x.py
  • Fallback chain: scripts/lib/env.py:get_x_source()