diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..fb97658 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,21 @@ +# last30days Skill + +Claude Code skill for researching any topic across Reddit, X, YouTube, and web. +Python scripts with multi-source search aggregation. + +## Structure +- `scripts/last30days.py` — main research engine +- `scripts/lib/` — search, enrichment, rendering modules +- `scripts/lib/vendor/bird-search/` — vendored X search client +- `SKILL.md` — skill definition (deployed to ~/.claude/skills/last30days/) + +## Commands +```bash +python3 scripts/last30days.py "test query" --emit=compact # Run research +bash scripts/sync.sh # Deploy to ~/.claude, ~/.agents, ~/.codex +``` + +## Rules +- `lib/__init__.py` must be bare package marker (comment only, NO eager imports) +- After edits: run `bash scripts/sync.sh` to deploy +- Git remotes: origin=private, upstream=public diff --git a/SKILL.md b/SKILL.md index f6e8fba..8faf843 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,6 +1,6 @@ --- name: last30days -version: "2.9.2" +version: "2.9.1" description: "Research a topic from the last 30 days. Also triggered by 'last30'. Sources: Reddit, X, YouTube, TikTok, Instagram, Hacker News, Polymarket, web. Become an expert and write copy-paste-ready prompts." argument-hint: 'last30 AI video tools, last30 best project management tools' allowed-tools: Bash, Read, Write, AskUserQuestion, WebSearch @@ -30,7 +30,7 @@ metadata: - prompts --- -# last30days v2.9.4: Research Any Topic from the Last 30 Days +# last30days v2.9.1: Research Any Topic from the Last 30 Days Research ANY topic across Reddit, X, YouTube, TikTok, Hacker News, Polymarket, and the web. Surface what people are actually discussing, recommending, betting on, and debating right now. @@ -123,7 +123,7 @@ If `--agent` appears in ARGUMENTS (e.g., `/last30days plaud granola --agent`): 5. **Skip** the follow-up invitation ("I'm now an expert on X...") 6. **Output** the complete research report and stop - do not wait for further input -Agent mode saves raw research data to `~/Documents/Last30Days/` automatically via `--save-dir` (handled by the script, no extra tool calls). +Agent mode still saves the research briefing to `~/Documents/Last30Days/` using the same logic as interactive mode (see "Save Research to Documents" section). Agent mode report format: @@ -167,7 +167,7 @@ if [ -z "${SKILL_ROOT:-}" ]; then exit 1 fi -python3 "${SKILL_ROOT}/scripts/last30days.py" "$ARGUMENTS" --emit=compact --no-native-web --save-dir=~/Documents/Last30Days # Add --x-handle=HANDLE if RESOLVED_HANDLE is set +python3 "${SKILL_ROOT}/scripts/last30days.py" "$ARGUMENTS" --emit=compact --no-native-web # Add --x-handle=HANDLE if RESOLVED_HANDLE is set ``` Use a **timeout of 300000** (5 minutes) on the Bash call. The script typically takes 1-3 minutes. @@ -498,9 +498,57 @@ For `/last30days war in Iran` (NEWS): --- +## Save Research to Documents + +After displaying the invitation, save the complete research briefing to `~/Documents/Last30Days/`. This happens automatically on every run. + +**Generate the filename** from TOPIC: +- Lowercase, replace spaces/special chars with hyphens, remove consecutive hyphens, trim to 60 chars +- Example: "Claude Code Best Practices" → `claude-code-best-practices.md` +- If file already exists, append today's date: `{slug}-YYYY-MM-DD.md` + +**End your invitation with a single `📎` footer line:** + +``` +📎 ~/Documents/Last30Days/{slug}.md +``` + +**Then immediately save using a background Bash command** (`run_in_background: true`): + +```bash +mkdir -p ~/Documents/Last30Days && cat > ~/Documents/Last30Days/{slug}.md << 'RESEARCH_EOF' +# {TOPIC} + +> Researched {date} | Query type: {QUERY_TYPE} | Target tool: {TARGET_TOOL or "general"} + +## What I learned + +{The full synthesis section you just displayed - all topics, patterns, and citations} + +## Stats + +{The full stats box with source counts and engagement - copy exactly as displayed} + +## Follow-up suggestions + +{The 2-3 specific suggestions from the invitation block} + +--- +*Generated by [last30days](https://github.com/mvanhorn/last30days-skill) v2.9.1* +RESEARCH_EOF +``` + +**CRITICAL RULES:** +1. NEVER use the `Write` tool — it displays "Wrote N lines..." which ruins the experience +2. ALWAYS use `run_in_background: true` so the Bash call is nearly invisible +3. The `📎` line is part of your text message, not a separate tool call +4. The invitation + `📎` line must be the LAST visible thing on screen + +--- + ## WAIT FOR USER'S RESPONSE -**STOP and wait** for the user to respond. Do NOT call any tools after displaying the invitation. The research script already saved raw data to `~/Documents/Last30Days/` via `--save-dir`. +**STOP and wait** for the user to respond. --- diff --git a/docs/how-search-works.md b/docs/how-search-works.md new file mode 100644 index 0000000..9b3c0fe --- /dev/null +++ b/docs/how-search-works.md @@ -0,0 +1,200 @@ +# How Reddit & X Search Work in last30days + +## Architecture Overview + +``` +User: /last30days "kanye west" + ↓ + ┌─────┴─────┐ + ↓ ↓ (concurrent via ThreadPoolExecutor) + [REDDIT] [X/TWITTER] + ↓ ↓ + OpenAI Bird CLI or + API xAI API + ↓ ↓ + Parse Parse + ↓ ↓ + Enrich ───┘ + (fetch ↓ + actual [MERGE] + upvotes) ↓ + ↓ [NORMALIZE → FILTER → SCORE → DEDUPE] + └───────────↓ + [OUTPUT to SKILL.md agent] +``` + +Both searches run **in parallel** using Python's `ThreadPoolExecutor(max_workers=2)`. + +--- + +## Reddit Search + +### How it works + +Reddit search uses the **OpenAI Responses API** with the `web_search` tool, domain-filtered to `reddit.com` only. + +**API Call:** +``` +POST https://api.openai.com/v1/responses +Authorization: Bearer {OPENAI_API_KEY} +``` + +**Payload:** +```json +{ + "model": "gpt-5.2", + "tools": [{ + "type": "web_search", + "filters": { "allowed_domains": ["reddit.com"] } + }], + "input": "Search Reddit for threads about {topic}..." +} +``` + +The prompt asks the model to: +1. Extract core subject (strip noise words like "best", "tips", "top") +2. Search 3 patterns: `"{topic} site:reddit.com"`, `"reddit {topic}"`, `"{topic} reddit"` +3. Return JSON with `title`, `url`, `subreddit`, `date`, `relevance` +4. URLs must contain `/r/` AND `/comments/` (real threads only) + +**Model fallback chain:** `gpt-5.2 → gpt-5.1 → gpt-5 → gpt-4.1 → gpt-4o → gpt-4o-mini` +Triggers on HTTP 400/403 with access error keywords. + +### Enrichment (the secret sauce) + +After search, each thread gets **enriched** by hitting Reddit's free JSON API: + +``` +GET https://reddit.com/r/{sub}/comments/{id}/{slug}/.json +``` + +No API key needed. This returns the actual thread data: + +| Data Point | Source | +|---|---| +| Upvotes (score) | Reddit JSON API | +| Comment count | Reddit JSON API | +| Upvote ratio | Reddit JSON API | +| Top 10 comments (text + score) | Reddit JSON API | +| 7 key comment insights | Extracted via heuristics | +| Actual post date | `created_utc` timestamp | + +**This is why Reddit results have real engagement metrics** — the enrichment step fetches actual upvote/comment data, not AI estimates. + +### Depth settings + +| Depth | Threads requested | Timeout | +|---|---|---| +| `--quick` | 15-25 | 90s | +| default | 30-50 | 120s | +| `--deep` | 70-100 | 180s | + +--- + +## X/Twitter Search + +X search has **two backends** — the skill auto-detects which to use. + +### Priority: Bird CLI (free) → xAI API (paid) + +```python +if bird_installed and bird_authenticated: + use Bird CLI # Free, uses your X login +elif XAI_API_KEY: + use xAI API # Paid, uses grok-4-1-fast +else: + skip X entirely # No X results +``` + +### Backend 1: xAI API + +**API Call:** +``` +POST https://api.x.ai/v1/responses +Authorization: Bearer {XAI_API_KEY} +``` + +**Payload:** +```json +{ + "model": "grok-4-1-fast", + "tools": [{ "type": "x_search" }], + "input": "Search X for posts about {topic} from {from_date} to {to_date}..." +} +``` + +The prompt asks grok to return JSON with: +- `text`, `url`, `author_handle`, `date` +- `engagement`: `{ likes, reposts, replies, quotes }` +- `why_relevant`, `relevance` score + +**Engagement data comes from grok's x_search tool** — it has direct access to X's data. + +### Backend 2: Bird CLI (free alternative) + +Bird is a CLI tool (`npm install -g @steipete/bird`) that uses your X login. + +**Command:** +```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 | Bird CLI | xAI API | +|---|---|---| +| Post text | Real | Real | +| Likes/reposts | Real (X API) | Real (x_search tool) | +| Replies/quotes | Real | Real | +| Author handle | Real | Real | +| Relevance score | Default 0.7 (re-ranked by score.py) | AI-assessed 0.0-1.0 | + +### Depth settings + +| Depth | xAI posts | Bird results | xAI timeout | Bird timeout | +|---|---|---|---|---| +| `--quick` | 8-12 | 12 | 90s | 30s | +| default | 20-30 | 30 | 120s | 45s | +| `--deep` | 40-60 | 60 | 180s | 60s | + +--- + +## Post-Processing (both sources) + +After both searches complete: + +1. **Normalize** — consistent formatting, timezone handling +2. **Date filter** — hard filter to requested date range +3. **Score** — relevance scoring (engagement-weighted) +4. **Sort** — highest scores first +5. **Deduplicate** — remove duplicate URLs +6. **Fallback** — if all items filtered out, keep top 3 by relevance + +--- + +## Error Handling + +| Layer | Strategy | +|---|---| +| HTTP requests | 3 retries with exponential backoff (1s → 2s → 3s) | +| Model access errors | Automatic fallback to next model in chain | +| Reddit enrichment | Per-item try/catch; keeps unenriched item on failure | +| X source detection | Silent fallback from Bird → xAI → skip | +| Overall pipeline | Errors stored as `reddit_error`/`x_error`, shown to user | + +--- + +## Key Files + +| File | Purpose | +|---|---| +| `scripts/last30days.py` | Main orchestrator, concurrent execution | +| `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/xai_x.py` | X search via xAI API | +| `scripts/lib/bird_x.py` | X search via Bird CLI (free) | +| `scripts/lib/models.py` | Auto-select best available model | +| `scripts/lib/env.py` | API key loading, source detection | +| `scripts/lib/http.py` | HTTP transport with retries | +| `scripts/lib/score.py` | Relevance scoring | +| `scripts/lib/dedupe.py` | URL-based deduplication | diff --git a/docs/plans/2026-02-06-feat-automated-v1-v2-test-harness-plan.md b/docs/plans/2026-02-06-feat-automated-v1-v2-test-harness-plan.md new file mode 100644 index 0000000..c91f873 --- /dev/null +++ b/docs/plans/2026-02-06-feat-automated-v1-v2-test-harness-plan.md @@ -0,0 +1,281 @@ +--- +title: "feat: Automated v1 vs v2 test harness using claude --print" +type: feat +date: 2026-02-06 +--- + +# feat: Automated V1 vs V2 Test Harness + +## Overview + +Build a bash script that swaps SKILL.md between v1 (upstream) and v2 (current), runs `claude --print "/last30days [query]"` for all 17 test queries on each version, captures output to files, then generates a comparison doc with analysis. + +## How It Works + +``` +┌─────────────────────────────────────────────────┐ +│ test-v1-vs-v2.sh │ +│ │ +│ 1. Save current SKILL.md as .v2 backup │ +│ 2. Install v1 SKILL.md from upstream │ +│ 3. Loop 17 queries → claude --print → v1/*.txt │ +│ 4. Restore v2 SKILL.md │ +│ 5. Loop 17 queries → claude --print → v2/*.txt │ +│ 6. Generate comparison doc │ +└─────────────────────────────────────────────────┘ +``` + +Each `claude --print` call: +- Invokes the /last30days skill exactly as a user would +- Runs the Python script (real API calls to OpenAI + xAI) +- Runs WebSearch +- Applies SKILL.md presentation instructions +- Returns the full formatted output +- Exits (no interactive session) + +## Implementation + +### File: `scripts/test-v1-vs-v2.sh` + +```bash +#!/bin/bash +set -euo pipefail + +# === Config === +SKILL_DIR="$HOME/.claude/skills/last30days" +REPO_DIR="/Users/mvanhorn/last30days-skill-private" +OUT_DIR="$REPO_DIR/docs/test-results/v1-vs-v2-$(date +%Y%m%d-%H%M%S)" +V1_DIR="$OUT_DIR/v1" +V2_DIR="$OUT_DIR/v2" + +mkdir -p "$V1_DIR" "$V2_DIR" + +# All 17 test queries (from README + plans) +declare -a QUERIES=( + "prompting techniques for chatgpt for legal questions" + "best clawdbot use cases" + "how to best setup clawdbot" + "prompting tips for nano banana pro for ios designs" + "top claude code skills" + "using ChatGPT to make images of dogs" + "research best practices for beautiful remotion animation videos in claude code" + "photorealistic people in nano banana pro" + "What are the best rap songs lately" + "what are people saying about DeepSeek R1" + "best practices for cursor rules files for Cursor" + "prompt advice for using suno to make killer songs in simple mode" + "how do I use Codex with Claude Code on same app to make it better" + "kanye west" + "howie.ai" + "open claw" + "nano banana pro prompting" +) + +declare -a TYPES=( + "PROMPTING+TOOL" + "RECOMMENDATIONS" + "HOW-TO" + "PROMPTING+TOOL" + "RECOMMENDATIONS" + "GENERAL" + "PROMPTING" + "PROMPTING" + "RECOMMENDATIONS" + "NEWS" + "PROMPTING" + "PROMPTING" + "HOW-TO" + "NEWS" + "GENERAL" + "GENERAL" + "PROMPTING" +) + +slugify() { + echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | head -c 60 +} + +run_version() { + local version="$1" + local outdir="$2" + + echo "" + echo "==========================================" + echo " Running $version — ${#QUERIES[@]} queries" + echo "==========================================" + + for i in "${!QUERIES[@]}"; do + local query="${QUERIES[$i]}" + local type="${TYPES[$i]}" + local slug=$(slugify "$query") + local num=$((i + 1)) + local outfile="$outdir/${num}-${slug}.txt" + + echo "" + echo "[$version] ($num/${#QUERIES[@]}) $query [$type]" + echo " → $outfile" + + # Run claude --print with the skill invocation + # --no-session-persistence: don't save to session history + # Timeout after 5 minutes per query (generous for slow API calls) + if timeout 300 claude --print \ + "/last30days $query" \ + > "$outfile" 2>"$outdir/${num}-${slug}.stderr.txt"; then + echo " ✅ Done ($(wc -l < "$outfile") lines)" + else + echo " ❌ Failed or timed out" + echo "FAILED: timeout or error" >> "$outfile" + fi + + # Brief pause between queries to avoid rate limits + sleep 2 + done +} + +# === Phase 1: Test V1 === +echo "📦 Backing up current SKILL.md..." +cp "$SKILL_DIR/SKILL.md" "$SKILL_DIR/SKILL.md.v2.bak" + +echo "📥 Installing V1 SKILL.md from upstream..." +cd "$REPO_DIR" +git show upstream/main:SKILL.md > "$SKILL_DIR/SKILL.md" + +# Also save a copy for reference +cp "$SKILL_DIR/SKILL.md" "$OUT_DIR/v1-SKILL.md" + +run_version "V1" "$V1_DIR" + +# === Phase 2: Test V2 === +echo "" +echo "📥 Restoring V2 SKILL.md..." +cp "$SKILL_DIR/SKILL.md.v2.bak" "$SKILL_DIR/SKILL.md" + +# Also save a copy for reference +cp "$SKILL_DIR/SKILL.md" "$OUT_DIR/v2-SKILL.md" + +run_version "V2" "$V2_DIR" + +# === Phase 3: Generate summary === +echo "" +echo "==========================================" +echo " Generating comparison summary" +echo "==========================================" + +SUMMARY="$OUT_DIR/comparison-summary.md" + +cat > "$SUMMARY" << 'HEADER' +# V1 vs V2 Comparison Results + +Generated: $(date) + +## Output Files + +| # | Query | Type | V1 Lines | V2 Lines | +|---|-------|------|----------|----------| +HEADER + +# Replace the date placeholder +sed -i '' "s/\$(date)/$(date)/" "$SUMMARY" + +for i in "${!QUERIES[@]}"; do + local query="${QUERIES[$i]}" + local type="${TYPES[$i]}" + local slug=$(slugify "$query") + local num=$((i + 1)) + + local v1file="$V1_DIR/${num}-${slug}.txt" + local v2file="$V2_DIR/${num}-${slug}.txt" + + local v1lines=$(wc -l < "$v1file" 2>/dev/null || echo "0") + local v2lines=$(wc -l < "$v2file" 2>/dev/null || echo "0") + + echo "| $num | \`$query\` | $type | $v1lines | $v2lines |" >> "$SUMMARY" +done + +cat >> "$SUMMARY" << 'FOOTER' + +## Scorecard Template + +For each query, score both versions on: + +| Dimension | V1 | V2 | Notes | +|-----------|----|----|-------| +| Query Parsing Display (1-5) | | | | +| Source Coverage (1-5) | | | | +| Citation Quality (1-5) | | | | +| Summary Structure (1-5) | | | | +| Stats Box Format (1-5) | | | | +| Research Grounding (1-5) | | | | + +## Next Step + +Read each pair of output files and score them using the test plan at: +`docs/plans/2026-02-06-test-v1-vs-v2-comparison-plan.md` +FOOTER + +echo "" +echo "✅ All done!" +echo "📁 Results: $OUT_DIR" +echo "📊 Summary: $SUMMARY" +echo "" +echo "V1 outputs: $V1_DIR/" +echo "V2 outputs: $V2_DIR/" +echo "" +echo "To review, run:" +echo " open $OUT_DIR" + +# Cleanup backup +rm -f "$SKILL_DIR/SKILL.md.v2.bak" +``` + +## Acceptance Criteria + +- [ ] Script runs all 17 queries on v1 SKILL.md +- [ ] Script runs all 17 queries on v2 SKILL.md +- [ ] Each query output saved to a separate .txt file +- [ ] Comparison summary generated with line counts +- [ ] SKILL.md restored to v2 after testing +- [ ] Both SKILL.md versions saved in output dir for reference +- [ ] Script handles timeouts gracefully (5 min per query) +- [ ] Brief pause between queries to avoid rate limits + +## Cost Estimate + +- 34 total `claude --print` invocations +- Each invocation: ~1 Python script run (OpenAI + xAI API) + 2-3 WebSearches + Claude response +- Estimated: ~$0.10-0.30 per invocation for API calls +- **Total estimate: $3-10 for the full run** + +## Time Estimate + +- Each query: ~1-3 minutes (script + WebSearch + synthesis) +- 17 queries × 2 versions = 34 runs +- **Total: ~45-90 minutes** (could run in background) + +## How to Run + +```bash +cd /Users/mvanhorn/last30days-skill-private +chmod +x scripts/test-v1-vs-v2.sh +./scripts/test-v1-vs-v2.sh +``` + +Or run in background: +```bash +nohup ./scripts/test-v1-vs-v2.sh > test-run.log 2>&1 & +tail -f test-run.log +``` + +## After the Run + +Once all outputs are captured, Claude can read every file pair and generate the scored comparison doc with analysis — that's the part where I score each dimension 1-5 and write the final report. + +## Files + +| File | Purpose | +|------|---------| +| `scripts/test-v1-vs-v2.sh` | The test harness script | +| `docs/test-results/v1-vs-v2-*/` | Output directory (timestamped) | +| `docs/test-results/v1-vs-v2-*/v1/*.txt` | V1 outputs | +| `docs/test-results/v1-vs-v2-*/v2/*.txt` | V2 outputs | +| `docs/test-results/v1-vs-v2-*/comparison-summary.md` | Auto-generated summary | diff --git a/docs/plans/2026-02-06-feat-free-reddit-mcp-integration-plan.md b/docs/plans/2026-02-06-feat-free-reddit-mcp-integration-plan.md new file mode 100644 index 0000000..192a7ef --- /dev/null +++ b/docs/plans/2026-02-06-feat-free-reddit-mcp-integration-plan.md @@ -0,0 +1,352 @@ +--- +title: "feat: Free Reddit via MCP — Remove OpenAI Key Requirement" +type: feat +date: 2026-02-06 +--- + +# feat: Free Reddit via MCP — Remove OpenAI Key Requirement + +## Overview + +Replace the OpenAI Responses API (paid) for Reddit searching with a **free, zero-config Reddit MCP server**, eliminating the need for an `OPENAI_API_KEY` to get Reddit results in the last30days skill. This work happens in a **new forked repo** to keep the current release branch clean. + +## Problem Statement + +Today, the last30days skill requires an OpenAI API key (`OPENAI_API_KEY`) to search Reddit. This is the most expensive dependency in the stack — OpenAI charges per-call for the Responses API with web search. Users without an OpenAI key get zero Reddit results and fall back to WebSearch-only mode, which loses the engagement metrics (upvotes, comments) that make last30days uniquely valuable. + +**Goal:** Make Reddit search completely free with no API key registration required. + +## Research Findings + +### Option Analysis + +Five approaches were evaluated. Two stand out: + +| Option | Free? | Search? | Engagement? | Setup | Notes | +|--------|-------|---------|-------------|-------|-------| +| **reddit-mcp-buddy** (Node.js) | Yes (anonymous mode) | Yes | Yes | One CLI command | 371 stars, 3-tier auth, most popular | +| **mcp-server-reddit** (Python) | Yes (redditwarp) | **No** | Yes | pip install | Recommended by ClaudeLog, but browse-only | +| **.json URL trick** (curl) | Yes | Yes | Yes | Zero | ~10 req/min, no dependencies | +| Reddit OAuth (PRAW) | Free but needs registration | Yes | Yes | App registration | 100 req/min, most reliable | +| RSS feeds | Yes | Basic | **No** | Zero | Least useful, no metrics | + +### Top Contender: reddit-mcp-buddy + +**GitHub:** [karanb192/reddit-mcp-buddy](https://github.com/karanb192/reddit-mcp-buddy) +- 371 stars, actively maintained (last update: Jan 29, 2026) +- **Anonymous mode** — zero credentials, ~10 req/min +- **MCP tools:** `search_reddit`, `browse_subreddit`, `get_post_details`, `user_analysis`, `reddit_explain` +- **Install:** `claude mcp add --transport stdio reddit-mcp-buddy -s user -- npx -y reddit-mcp-buddy` +- Returns full engagement metrics (score, num_comments, upvote_ratio) +- TypeScript/Node.js (npx, no Python dependency) + +### Strong Alternative: .json URL trick + +- Append `.json` to any Reddit URL → full JSON response +- Search: `https://www.reddit.com/search.json?q=TOPIC&sort=relevance&t=month&limit=100` +- Zero dependencies, zero auth, zero setup +- Returns same data as official API (score, num_comments, created_utc, upvote_ratio) +- ~10 req/min rate limit (sufficient for skill use) +- Can be called via `curl` or Python `urllib` + +### Also Considered + +- **Hawstein/mcp-server-reddit** (134 stars, Python, no auth) — **No search tool**, only browse subreddits. Cannot replace OpenAI's keyword search capability. +- **Arindam200/reddit-mcp** (262 stars, PRAW) — Has search but **requires Reddit OAuth credentials**. Not truly zero-config. +- **adhikasp/mcp-reddit** (348 stars) — Hot threads only, no search. Last updated Dec 2024. + +## Proposed Solution + +### Approach A: MCP-First with .json Fallback (Recommended) + +Add a new module `mcp_reddit.py` that: + +1. **Detects** if a Reddit MCP server is configured in the user's Claude Code session +2. **If MCP available:** Calls MCP `search_reddit` tool via the skill's `allowed-tools` (the skill already allows tools — MCP tools become available when configured) +3. **If no MCP:** Falls back to Reddit's `.json` URL endpoints via `urllib` (stdlib, no dependencies) +4. **Normalize** MCP/JSON results to the existing `RedditItem` schema +5. **Enrich** with `reddit_enrich.py` (already works — fetches real thread data) + +This gives users three tiers of Reddit access: +- **Tier 1 (best):** Reddit MCP installed → full search + engagement via MCP tools +- **Tier 2 (good):** No MCP, no keys → `.json` URL search + enrichment +- **Tier 3 (existing):** OpenAI key present → existing `openai_reddit.py` still works (backward compat) + +### Approach B: .json-Only (Simpler) + +Skip MCP entirely. Add a `reddit_json.py` module that uses `https://www.reddit.com/search.json` directly. Simpler but misses the MCP ecosystem integration. + +### Approach C: MCP-Only (Cleaner) + +Require MCP setup. Simpler code but adds a user setup step (`claude mcp add ...`). + +**Recommendation: Approach A** — MCP-first with .json fallback gives zero-config Reddit search for everyone while rewarding users who set up MCP with a better experience. + +## Technical Approach + +### Architecture + +``` +User invokes /last30days "topic" + │ + ├─ env.py detects sources: + │ ├─ MCP reddit available? → mcp_reddit.py + │ ├─ No MCP, no key? → reddit_json.py (.json URL trick) + │ └─ OPENAI_API_KEY set? → openai_reddit.py (existing, backward compat) + │ + ├─ X search (Bird CLI / xAI — unchanged) + │ + └─ Normalize → Score → Dedupe → Render (unchanged) +``` + +### New Files + +#### `scripts/lib/reddit_json.py` +- Uses `urllib.request` (stdlib) to call Reddit's `.json` search endpoint +- URL: `https://www.reddit.com/search.json?q={topic}&sort=relevance&t=month&limit=100` +- Custom `User-Agent` header (required by Reddit) +- Parses response into the same format as `openai_reddit.py` output +- Handles pagination via `after` token if depth=deep (multiple requests) +- Rate limiting: 1 second delay between requests + +#### `scripts/lib/mcp_reddit.py` +- Detects MCP availability (check if Reddit MCP tools exist in session) +- Formats MCP tool calls for `search_reddit` with topic + date filters +- Normalizes MCP response to match existing `RedditItem` schema +- Falls back to `reddit_json.py` if MCP unavailable + +#### Modified Files + +- **`scripts/lib/env.py`** — Add Reddit source detection: MCP → .json → OpenAI +- **`scripts/last30days.py`** — Route Reddit search through new priority chain +- **`scripts/lib/normalize.py`** — Add normalizer for `.json` endpoint response format +- **`SKILL.md`** — Document MCP setup as optional enhancement + +### MCP Integration Design + +The MCP approach has a subtle challenge: the last30days skill runs as a **Python subprocess** (`python3 last30days.py`), but MCP tools are available to **Claude's session**, not to subprocesses. + +**Two paths to solve this:** + +**Path 1: SKILL.md orchestration** — The SKILL.md workflow calls the MCP `search_reddit` tool directly (before or in parallel with the Python script), saves the MCP response to a temp file, and the Python script reads it: + +``` +SKILL.md workflow: +1. Call MCP search_reddit → save to /tmp/last30days_mcp_reddit.json +2. Call python3 last30days.py --reddit-from=/tmp/last30days_mcp_reddit.json --emit=compact +3. Python script reads pre-fetched Reddit data instead of calling OpenAI +``` + +**Path 2: .json only for subprocess** — The Python script uses `.json` URLs directly (no MCP needed in subprocess). MCP is a bonus for users who want Claude to also browse specific threads interactively. + +**Recommendation: Path 2 for MVP, Path 1 as enhancement.** The `.json` approach is self-contained, testable, and doesn't require SKILL.md workflow changes. MCP can be layered on later. + +### Source Priority Chain (updated env.py) + +```python +def get_reddit_source(config: dict) -> str: + """Returns: 'mcp', 'json', 'openai', or 'none'""" + # 1. Check for pre-fetched MCP data (from SKILL.md) + if os.path.exists(MCP_REDDIT_CACHE_PATH): + return 'mcp' + # 2. Always available — no key needed + # (reddit .json endpoints are free) + return 'json' + # 3. OpenAI key present → legacy path + # if config.get('OPENAI_API_KEY'): + # return 'openai' +``` + +For MVP, the `.json` path is **always available** so it becomes the default. OpenAI path remains as opt-in for users who want higher rate limits. + +### Data Mapping: .json → RedditItem + +Reddit `.json` search returns `data.children[].data` with: + +```json +{ + "title": "Post title", + "permalink": "/r/subreddit/comments/abc123/...", + "subreddit": "ClaudeAI", + "score": 42, + "num_comments": 15, + "upvote_ratio": 0.95, + "created_utc": 1738800000, + "selftext": "Post body...", + "url": "https://...", + "author": "username" +} +``` + +Maps cleanly to existing `RedditItem`: + +| .json field | RedditItem field | Notes | +|-------------|------------------|-------| +| `title` | `title` | Direct | +| `permalink` | `url` | Prepend `https://www.reddit.com` | +| `subreddit` | `subreddit` | Direct | +| `score` | `engagement.score` | Direct | +| `num_comments` | `engagement.num_comments` | Direct | +| `upvote_ratio` | `engagement.upvote_ratio` | Direct | +| `created_utc` | `date` | Convert epoch → YYYY-MM-DD | +| `selftext` | (used for relevance) | AI relevance scoring needed | +| `author` | (not in current schema) | Ignore for now | + +### Relevance Scoring Without AI + +The current `openai_reddit.py` gets relevance scores **from OpenAI** (the AI judges how relevant each result is). With `.json` endpoints, we lose that AI relevance judgment. + +**Options:** +1. **Keyword matching** — Score based on how many query terms appear in title + selftext. Simple but effective. +2. **TF-IDF-like** — Weight rarer query terms higher. More accurate but more code. +3. **Let Claude judge** — Pass results to Claude in SKILL.md and have Claude score relevance. Most accurate but changes the workflow. +4. **Skip relevance, rely on Reddit's sort** — Reddit's `sort=relevance` already ranks by relevance. Trust it and use position-based scoring (first result = 1.0, last = 0.5). + +**Recommendation: Option 4 for MVP.** Reddit's search relevance ranking is already good. Use position-based relevance (1.0 → 0.5 linear decay over result set) combined with the existing engagement-based scoring. This requires zero external dependencies and no AI calls. + +## Implementation Phases + +### Phase 0: Fork Repository + +- [ ] Create new repo `last30days-free-reddit` (or branch in private repo) +- [ ] `git clone /Users/mvanhorn/last30days-skill-private /Users/mvanhorn/last30days-free-reddit` +- [ ] Create feature branch: `feat/free-reddit` +- [ ] Verify all existing tests pass on the new branch + +### Phase 1: reddit_json.py — Core .json Search + +- [ ] Create `scripts/lib/reddit_json.py` + - `search_reddit_json(topic: str, depth: str, date_from: str, date_to: str) -> list[dict]` + - Build search URL with `q`, `sort=relevance`, `t=month`, `limit` (25/50/100 by depth) + - Custom `User-Agent: last30days-skill/2.0 (by /u/last30days-bot)` + - Parse `data.children[].data` → list of raw dicts + - Handle pagination for deep mode (follow `after` token, max 3 pages) + - Rate limit: `time.sleep(1.0)` between requests + - Error handling: 429 (rate limit), 403 (blocked), network errors → return empty + error msg +- [ ] Create `tests/test_reddit_json.py` + - Mock HTTP responses using fixture files + - Test: basic search parsing, pagination, rate limit handling, error cases +- [ ] Create `fixtures/reddit_json_search_sample.json` + - Real `.json` search response (sanitized) + +### Phase 2: Normalize + Score .json Results + +- [ ] Add `normalize_reddit_json()` to `scripts/lib/normalize.py` + - Convert `.json` response format → `RedditItem` schema + - `created_utc` (epoch) → `YYYY-MM-DD` string + - `permalink` → full URL + - Position-based relevance: `1.0 - (index / total * 0.5)` → range [1.0, 0.5] + - Date confidence: `"high"` (Reddit provides exact timestamps) +- [ ] Update `scripts/lib/score.py` if needed + - `.json` results already have real engagement metrics — existing scoring formula works as-is + - No penalty needed (unlike WebSearch which lacks engagement) +- [ ] Add tests for normalization + scoring of `.json` data + +### Phase 3: Integration into Orchestrator + +- [ ] Update `scripts/lib/env.py` + - Add `get_reddit_source(config) -> str` returning `'json'`, `'openai'`, or `'none'` + - Priority: `.json` always available (default), `'openai'` if key present and user prefers + - Add `REDDIT_SOURCE` config option: `auto` (default), `json`, `openai` + - Update `get_available_sources()` to always include Reddit (since `.json` is free) + - Update `get_missing_keys()` — Reddit no longer shows as "missing" +- [ ] Update `scripts/last30days.py` + - Add `_search_reddit_json()` function alongside existing `_search_reddit()` + - Route based on `get_reddit_source()`: json → `_search_reddit_json()`, openai → `_search_reddit()` + - Skip `reddit_enrich.py` for `.json` results (already have real engagement metrics!) + - Update progress/stats output to show source: "Reddit (free)" vs "Reddit (OpenAI)" +- [ ] Update SKILL.md promo messaging + - Remove "Add OPENAI_API_KEY for Reddit" messaging + - Instead: "Reddit search included free! Add OpenAI key for AI-enhanced relevance scoring." +- [ ] Integration tests: full pipeline with `.json` mock data + +### Phase 4: Testing & Polish + +- [ ] Run all existing tests — ensure backward compatibility +- [ ] Manual test: invoke `/last30days` with NO API keys → should get Reddit + WebSearch results +- [ ] Manual test: invoke with OPENAI_API_KEY → should still use OpenAI path (backward compat) +- [ ] Manual test: compare result quality — `.json` vs OpenAI for same topic +- [ ] Update README.md — document free Reddit access +- [ ] Update SPEC.md — document new source priority chain + +### Phase 5 (Future): MCP Enhancement Layer + +- [ ] Detect Reddit MCP in Claude's session +- [ ] SKILL.md pre-fetches via MCP `search_reddit` → saves to temp file +- [ ] Python script reads pre-fetched MCP data via `--reddit-from=` flag +- [ ] MCP results get AI-judged relevance (since Claude sees them) +- [ ] Better than `.json` position-based relevance + +## Acceptance Criteria + +### Functional +- [ ] `/last30days "any topic"` returns Reddit results with **zero API keys configured** +- [ ] Reddit results include real engagement metrics (score, comments, upvote_ratio) +- [ ] Results are properly scored, deduped, and rendered (same quality as OpenAI path) +- [ ] Existing OpenAI path still works when key is present +- [ ] Existing X search (Bird CLI / xAI) is unchanged +- [ ] Stats box shows correct source indicator ("Reddit (free)" or "Reddit (OpenAI)") + +### Non-Functional +- [ ] No external Python packages (stdlib only — `urllib.request`, `json`, `time`) +- [ ] Respects Reddit rate limits (~10 req/min for unauthenticated) +- [ ] Graceful degradation if Reddit blocks requests (429/403 → empty results + error msg) +- [ ] All new code has unit tests with fixtures (no live API calls in tests) + +### Quality Gates +- [ ] All existing tests pass (zero regressions) +- [ ] New tests cover: search parsing, normalization, scoring, error handling, pagination +- [ ] Manual smoke test passes with zero API keys + +## Risk Analysis + +| Risk | Likelihood | Impact | Mitigation | +|------|-----------|--------|------------| +| Reddit blocks `.json` endpoints | Low | High | Fall back to OpenAI path; monitor for 429s | +| `.json` rate limit too restrictive | Medium | Medium | 1s delay between requests; cache results | +| Relevance quality lower without AI scoring | Medium | Medium | Reddit's `sort=relevance` is decent; can add keyword scoring later | +| Reddit changes `.json` response format | Low | Medium | Schema validation in normalize; fixture-based tests catch changes | +| Node.js MCP server dependency conflicts | N/A (Phase 5) | Low | MCP is optional enhancement, not required | + +## Dependencies + +- **None** — this feature uses only Python stdlib and Reddit's public `.json` endpoints +- Phase 5 (future) would add optional dependency on an MCP server + +## Repository Setup + +```bash +# Fork into new working repo +cp -r /Users/mvanhorn/last30days-skill-private /Users/mvanhorn/last30days-free-reddit +cd /Users/mvanhorn/last30days-free-reddit + +# Create feature branch +git checkout -b feat/free-reddit + +# Verify existing tests +python3 -m pytest tests/ -v +``` + +The new repo is disposable — once the feature is validated, changes merge back into `last30days-skill-private` via PR or cherry-pick. + +## References + +### Internal +- `scripts/lib/openai_reddit.py` — Current Reddit search (to be replaced/supplemented) +- `scripts/lib/env.py:get_available_sources()` — Source detection logic to update +- `scripts/lib/normalize.py` — Add `.json` normalizer alongside existing +- `scripts/lib/reddit_enrich.py` — May be skippable for `.json` results (already have engagement) +- `scripts/lib/schema.py:RedditItem` — Target schema (unchanged) + +### External +- [Reddit .json search endpoint](https://www.reddit.com/search.json?q=test&sort=relevance&t=month&limit=25) +- [Simon Willison — Scraping Reddit via JSON API](https://til.simonwillison.net/reddit/scraping-reddit-json) +- [karanb192/reddit-mcp-buddy](https://github.com/karanb192/reddit-mcp-buddy) — Best MCP option for Phase 5 +- [Hawstein/mcp-server-reddit](https://github.com/Hawstein/mcp-server-reddit) — ClaudeLog-recommended MCP (no search though) +- [Reddit API Rate Limits Guide](https://painonsocial.com/blog/reddit-api-rate-limits-guide) + +### Research Sources +- last30days skill output — community recommendations for Reddit search tools +- GitHub search — 10+ Reddit MCP repos evaluated +- npm/PyPI registries — package availability confirmed +- ClaudeLog — [Reddit MCP reference](https://claudelog.com/claude-code-mcps/reddit-mcp/) diff --git a/docs/plans/2026-02-07-release-v2-public-launch-plan.md b/docs/plans/2026-02-07-release-v2-public-launch-plan.md new file mode 100644 index 0000000..fbac083 --- /dev/null +++ b/docs/plans/2026-02-07-release-v2-public-launch-plan.md @@ -0,0 +1,67 @@ +--- +title: "release: Push V2 to public repo and launch" +type: release +date: 2026-02-07 +--- + +# release: Push V2 to public repo and launch + +## Overview + +Push all V2 changes from the private development repo to the public GitHub repo, then sync the installed skill. 52 commits need to go from `origin/main` (private) to `upstream/main` (public). + +## Current State + +- **Private repo:** `https://github.com/mvanhorn/last30days-skill-private` - 52 commits ahead of public +- **Public repo:** `https://github.com/mvanhorn/last30days-skill` - last commit is V1 (`cc892d7`) +- **Upstream remote:** Already configured in private repo +- **Untracked files in private:** test logs, draft plans - these should NOT be pushed + +## Steps + +### Step 1: Clean up private repo + +- [ ] Review untracked files - make sure nothing sensitive gets pushed +- [ ] Decide: commit the untracked docs/plans or leave them out of the push + +### Step 2: Push to public + +```bash +cd /Users/mvanhorn/last30days-skill-private +git push upstream main +``` + +This pushes all 52 commits from private `main` to public `main`. Since the private repo was forked from public, history is shared - this is a fast-forward push. + +### Step 3: Sync installed skill + +```bash +# Update the installed copy that Claude Code actually uses +cd ~/.claude/skills/last30days +git pull origin main +``` + +### Step 4: Verify + +- [ ] Check `https://github.com/mvanhorn/last30days-skill` shows V2 README with new examples +- [ ] Check SKILL.md has the new argument-hint and timing disclaimer +- [ ] Check bird_x.py has the fixed `_extract_core_subject()` and retry logic +- [ ] Run a quick `/last30days` test to confirm installed skill works + +## What Gets Published + +Key files going public: +- `README.md` - V2 features, 3 new examples (Nano Banana Pro, Kanye, Vibe Motion), speed tradeoff note +- `SKILL.md` - New argument-hint, timing disclaimer, citation rules +- `scripts/lib/bird_x.py` - Fixed query construction + retry logic +- `scripts/lib/http.py` - USER_AGENT bumped to 2.0 +- `.claude-plugin/plugin.json` - Marketplace support +- All Bird CLI integration code +- Phase 2 supplemental search code +- Model fallback chain + +## Risks + +- **Low risk:** This is a fast-forward push, no force push needed +- **Public API keys:** Already confirmed - no `.env` files or secrets in the repo +- **Untracked files:** Won't be pushed unless committed first diff --git a/docs/plans/2026-02-15-feat-clawhub-scanner-compliance-plan.md b/docs/plans/2026-02-15-feat-clawhub-scanner-compliance-plan.md new file mode 100644 index 0000000..b72569e --- /dev/null +++ b/docs/plans/2026-02-15-feat-clawhub-scanner-compliance-plan.md @@ -0,0 +1,194 @@ +--- +title: ClawHub Scanner Compliance for last30days-official +type: feat +date: 2026-02-15 +--- + +# ClawHub Scanner Compliance for last30days-official + +## Overview + +Make the last30days skill pass ClawHub's security scanner (VirusTotal + Code Insight) so it can be published as `last30days-official`. The user's 8 other mvanhorn skills already pass - we replicate their exact pattern. + +## Problem Statement + +ClawHub requires skills to pass a multi-layer security scan before publication: +1. Metadata validation (frontmatter fields) +2. VirusTotal automated scanning +3. LLM-powered Code Insight analysis (checks if capabilities match documentation) +4. Credential handling review + +The current last30days SKILL.md has basic `metadata.clawdbot` but is missing fields the scanner checks: `emoji`, `user-invocable`, `disable-model-invocation`, `files` declaration. There's no `## Security & Permissions` section (required pattern from passing skills). README has no security/privacy documentation. + +## Proposed Solution + +Follow the exact pattern from `clawdbot-skill-xai` and `clawdbot-skill-search-x` (both pass the scanner). Three files need changes. + +### Fix 1: SKILL.md Frontmatter + +Add missing scanner fields to the existing frontmatter: + +```yaml +--- +name: last30days +version: "2.1" +description: "Research a topic from the last 30 days. Also triggered by 'last30'. Sources: Reddit, X, YouTube, web." +argument-hint: 'last30 AI video tools, last30 best project management tools' +allowed-tools: Bash, Read, Write, AskUserQuestion, WebSearch +homepage: https://github.com/mvanhorn/last30days-skill +user-invocable: true +disable-model-invocation: true +metadata: + clawdbot: + emoji: "📰" + requires: + env: + - OPENAI_API_KEY + bins: + - node + - python3 + primaryEnv: OPENAI_API_KEY + files: + - "scripts/*" + homepage: https://github.com/mvanhorn/last30days-skill + tags: + - research + - reddit + - x + - youtube + - trends + - prompts +--- +``` + +Key additions: +- `user-invocable: true` - human must trigger it +- `disable-model-invocation: true` - agent cannot self-trigger +- `emoji: "📰"` - required display field +- `files: ["scripts/*"]` - prevents false "instruction-only but has scripts" flag + +### Fix 2: Security & Permissions Section in SKILL.md + +Add to the bottom of SKILL.md (matches xai/search-x pattern exactly): + +```markdown +## Security & Permissions + +**What this skill does:** +- Sends search queries to OpenAI's Responses API (`api.openai.com`) for Reddit discovery +- Sends search queries to Twitter's GraphQL API (via browser cookie auth) or xAI's API (`api.x.ai`) for X search +- Runs `yt-dlp` locally for YouTube search and transcript extraction (no API key, public data) +- Optionally sends search queries to Brave Search API, Parallel AI API, or OpenRouter API for web search +- Fetches public Reddit thread data from `reddit.com` for engagement metrics +- Stores research findings in local SQLite database (watchlist mode only) + +**What this skill does NOT do:** +- Does not post, like, or modify content on any platform +- Does not access your Reddit, X, or YouTube accounts +- Does not share API keys between providers (OpenAI key only goes to api.openai.com, etc.) +- Does not log, cache, or write API keys to output files +- Does not send data to any endpoint not listed above +- Cannot be invoked autonomously by the agent (`disable-model-invocation: true`) + +**Bundled scripts:** `scripts/last30days.py` (main research engine), `scripts/lib/` (search, enrichment, rendering modules), `scripts/lib/vendor/bird-search/` (vendored X search client, MIT licensed) + +Review scripts before first use to verify behavior. +``` + +### Fix 3: Security & Privacy Section in README.md + +Add after the "How It Works" section: + +```markdown +## Security & Privacy + +### Data that leaves your machine + +| Destination | Data Sent | API Key Required | +|------------|-----------|-----------------| +| `api.openai.com` | Search query (topic string) | OPENAI_API_KEY | +| `reddit.com` | Thread URLs for enrichment | None (public JSON) | +| Twitter GraphQL / `api.x.ai` | Search query | Browser cookies or XAI_API_KEY | +| `youtube.com` (via yt-dlp) | Search query | None (public search) | +| `api.search.brave.com` | Search query (optional) | BRAVE_API_KEY | +| `api.parallel.ai` | Search query (optional) | PARALLEL_API_KEY | +| `openrouter.ai` | Search query (optional) | OPENROUTER_API_KEY | + +Your research topic is included in all outbound API requests. If you research sensitive topics, be aware that query strings are transmitted to the API providers listed above. + +### Data stored locally + +- API keys: `~/.config/last30days/.env` (chmod 600 recommended) +- Watchlist database: `~/.local/share/last30days/research.db` (SQLite) +- Briefings: `~/.local/share/last30days/briefs/` + +### API key isolation + +Each API key is transmitted only to its respective endpoint. Your OpenAI key is never sent to xAI, Brave, or any other provider. Browser cookies for X are read locally and used only for Twitter GraphQL requests. +``` + +### Fix 4: Update .claude-plugin Files + +**plugin.json** - bump version, add youtube keyword: + +```json +{ + "name": "last30days", + "description": "Research any topic from the last 30 days across Reddit, X, YouTube, and the web", + "version": "2.1.0", + "author": {"name": "mvanhorn"}, + "repository": "https://github.com/mvanhorn/last30days-skill", + "license": "MIT", + "keywords": ["research", "reddit", "twitter", "x", "youtube", "trends", "prompts"], + "skills": ["./"] +} +``` + +**marketplace.json** - add version, update description: + +```json +{ + "name": "last30days", + "owner": {"name": "mvanhorn", "url": "https://github.com/mvanhorn"}, + "metadata": { + "description": "Research any topic from the last 30 days across Reddit, X, YouTube, and the web", + "version": "2.1.0" + }, + "plugins": [{"name": "last30days", "source": "."}] +} +``` + +## What We DON'T Need + +Based on the audit of your 8 passing skills: +- **Script-level security manifest headers** - your passing skills (xai, search-x, parallel) do NOT have these. The scanner relies on SKILL.md, not per-file headers. +- **Separate SECURITY.md file** - not needed; README section + SKILL.md section is sufficient. +- **Shell injection fixes** - already clean. All subprocess calls use list-form args, no `shell=True` anywhere. +- **Credential leak fixes** - already clean. All keys loaded from env vars, none hardcoded or logged. + +## Acceptance Criteria + +- [x] SKILL.md frontmatter has `user-invocable`, `disable-model-invocation`, `emoji`, `files` +- [x] SKILL.md has `## Security & Permissions` section with "does" and "does NOT do" lists +- [x] README.md has `## Security & Privacy` section with endpoint table and key isolation docs +- [x] plugin.json version bumped to 2.1.0, youtube keyword added +- [x] marketplace.json version and description updated +- [x] `python3 scripts/last30days.py --diagnose` still works after changes +- [x] Synced to all installed skill locations +- [ ] Published to ClawHub as `last30days-official` (when auth is fixed) + +## Files to Modify + +| File | Change | +|------|--------| +| `SKILL.md` | Add frontmatter fields + Security & Permissions section | +| `README.md` | Add Security & Privacy section | +| `.claude-plugin/plugin.json` | Bump version, add youtube keyword | +| `.claude-plugin/marketplace.json` | Add version, update description | + +## References + +- [13-point ClawHub checklist](https://gist.github.com/adhishthite/0db995ecfe2f23e09d0b2d418491982c) +- [ClawHub docs](https://docs.openclaw.ai/tools/clawhub) +- [ClawHub Developer Guide 2026](https://www.digitalapplied.com/blog/clawhub-skills-marketplace-developer-guide-2026) +- Your passing skills: `clawdbot-skill-xai`, `clawdbot-skill-search-x` (exact pattern replicated) diff --git a/docs/plans/2026-02-15-fix-watchlist-engagement-null-crash-plan.md b/docs/plans/2026-02-15-fix-watchlist-engagement-null-crash-plan.md new file mode 100644 index 0000000..e498b02 --- /dev/null +++ b/docs/plans/2026-02-15-fix-watchlist-engagement-null-crash-plan.md @@ -0,0 +1,76 @@ +--- +title: "fix: watchlist engagement null crash" +type: fix +date: 2026-02-15 +--- + +# fix: Watchlist crashes on `engagement: null` from X posts + +## Problem + +When X posts return `engagement: null` (JSON null) instead of `engagement: {}` (empty object), the watchlist `_run_topic` findings parser crashes. This is because Python's `dict.get("engagement", {})` returns `None` when the key **exists** with value `None` — the default `{}` only applies when the key is **missing**. + +```python +# CRASHES — .get() returns None, not {} +item.get("engagement", {}).get("likes", 0) +# AttributeError: 'NoneType' object has no attribute 'get' +``` + +**Reporter:** Soft launch tester, 2026-02-15 +**Severity:** Medium — breaks watchlist `run-one` and `run-all` for any topic that pulls X posts with null engagement +**One-shot research unaffected** — the main `last30days.py` pipeline uses `normalize.py` which has `isinstance(eng_raw, dict)` guards + +## Root Cause + +Two locations use the vulnerable `dict.get("key", {})` pattern: + +1. **`scripts/watchlist.py:188`** — THE REPORTED BUG + ```python + "engagement_score": item.get("engagement", {}).get("likes", 0), + ``` + +2. **`scripts/lib/normalize.py:177`** — YouTube normalizer (same pattern, latent) + ```python + eng_raw = item.get("engagement", {}) + ``` + +Three other locations are already safe — they use `isinstance(eng_raw, dict)`: +- `scripts/lib/normalize.py:70` (Reddit) +- `scripts/lib/normalize.py:130` (X) +- `scripts/lib/xai_x.py:190` + +## Fix + +Apply the `or {}` idiom (as suggested by reporter): + +### scripts/watchlist.py:188 + +```python +# Before +"engagement_score": item.get("engagement", {}).get("likes", 0), +# After +"engagement_score": (item.get("engagement") or {}).get("likes", 0), +``` + +### scripts/lib/normalize.py:177 + +```python +# Before +eng_raw = item.get("engagement", {}) +# After +eng_raw = item.get("engagement") or {} +``` + +## Acceptance Criteria + +- [ ] `watchlist.py run-one` handles X posts with `engagement: null` without crashing +- [ ] `watchlist.py run-one` handles X posts with `engagement: {}` (empty object) +- [ ] `watchlist.py run-one` handles X posts with no engagement key at all +- [ ] YouTube normalizer handles `engagement: null` without crashing +- [ ] Existing tests still pass + +## Regarding the screenshot question + +The tester asked "Did you use watchlist on open claw or Claude?" — watchlist is designed for the **open variant** (Open Claw). It works in Claude Code too since it's just Python + SQLite, but the SKILL.md routing for watchlist commands is only in `variants/open/SKILL.md`. The main `SKILL.md` is one-shot research only. + +Answer to give tester: "Watchlist works in both — it's plain Python. But the skill routing that understands 'watch add topic' is in the open variant. In Claude Code you'd need to call the script directly or use the open variant SKILL.md." diff --git a/docs/plans/2026-02-15-fix-youtube-display-and-search-quality-plan.md b/docs/plans/2026-02-15-fix-youtube-display-and-search-quality-plan.md new file mode 100644 index 0000000..de40443 --- /dev/null +++ b/docs/plans/2026-02-15-fix-youtube-display-and-search-quality-plan.md @@ -0,0 +1,221 @@ +--- +title: Fix YouTube Display and Search Quality +type: fix +date: 2026-02-15 +--- + +# Fix YouTube Display and Search Quality + +## Overview + +YouTube is the v2.1 headline feature but it's broken in two ways: results don't appear in Claude's synthesis (display bug), and search quality is worse than youtube.com (search bug). Both need fixing before launch. + +## Problem Statement + +**Display bug:** YouTube data exists in the script output but Claude never sees it. Reproduced on 4/5 recent test runs (Kanye, Seedance 2, Peter Steinberger, YouTube thumbnails). The skill worked once — the earlier "YouTube thumbnails" and "OpenClaw" runs showed YouTube stats — but subsequent runs silently dropped it. + +**Search quality bug:** User searched "how to get on seedance 2" on youtube.com and got multiple recent results. yt-dlp returned 10 videos for the same query, but they included old irrelevant content because date filtering is broken. + +## Root Cause Analysis + +### Display Bug — Three compounding causes + +1. **`2>&1` in SKILL.md bash command** (line 79) merges stderr progress messages into stdout. When Claude Code receives this mixed output, the YouTube section (which renders LAST after Reddit + X) can get lost in the noise or hit the 30K char Bash output limit. + +2. **Background execution** (partially fixed). The old SKILL.md said "DO WEBSEARCH WHILE SCRIPT RUNS" which caused Claude to background the bash command. Backgrounded commands return truncated output via Task Output. *Already fixed in this session — SKILL.md now says FOREGROUND with 5-minute timeout.* + +3. **No explicit model instruction to look for YouTube.** The SKILL.md tells Claude to synthesize but doesn't emphasize that YouTube data is in the script output and must be included. + +### Search Quality Bug — `--flat-playlist` breaks date filtering + +The yt-dlp command in `youtube_yt.py:110-116`: +```bash +yt-dlp ytsearch{count}:{query} --dateafter {YYYYMMDD} --flat-playlist --dump-json +``` + +**`--flat-playlist` causes three problems:** +1. `--dateafter` is silently ignored (no video-level metadata to filter on) +2. All items have `date: None` (upload_date not in flat-playlist JSON) +3. Old content leaks in (e.g., "the greatest youtube thumbnails of all time" returned for a 30-day query) + +**`_extract_core_subject()` over-strips useful YouTube terms:** +- Strips "tips", "tutorial", "review" — but these ARE the content types people search for on YouTube +- "youtube thumbnail tips" → "youtube thumbnail" loses the intent signal + +## Proposed Solution + +### Phase 1: Fix Display (Critical — blocks launch) + +#### 1a. Remove `2>&1` from SKILL.md bash command + +**File:** `SKILL.md:79` (both `last30days-skill-private/SKILL.md` and `~/.claude/skills/last30days21/SKILL.md`) + +```bash +# Before: +python3 "${SKILL_ROOT}/scripts/last30days.py" "$ARGUMENTS" --emit=compact 2>&1 + +# After: +python3 "${SKILL_ROOT}/scripts/last30days.py" "$ARGUMENTS" --emit=compact +``` + +This removes ~1-5KB of progress spam from the model's input and ensures clean stdout-only output. + +#### 1b. Add YouTube-specific synthesis instruction to SKILL.md + +After the "Read the ENTIRE output" instruction, add: + +```markdown +**The script output has THREE sections: Reddit items, X items, and YouTube items (in that order). +If you see YouTube items in the output, you MUST include them in your synthesis and stats block. +YouTube items look like: `**{video_id}** (score:N) {channel} [N views, N likes]`** +``` + +#### 1c. Verify fix with test run + +Run `/last30days21 youtube thumbnail tips` and confirm YouTube appears in stats. + +### Phase 2: Fix Search Quality (High — headline feature quality) + +#### 2a. Remove `--flat-playlist` flag + +**File:** `scripts/lib/youtube_yt.py:110-116` + +```python +# Before: +cmd = [ + "yt-dlp", + f"ytsearch{count}:{core_topic}", + "--dateafter", date_filter, + "--flat-playlist", + "--dump-json", +] + +# After: +cmd = [ + "yt-dlp", + f"ytsearch{count}:{core_topic}", + "--dateafter", date_filter, + "--dump-json", + "--no-warnings", + "--no-download", +] +``` + +**Impact:** Slower (yt-dlp resolves each video page for metadata) but: +- `--dateafter` actually works — filters old content +- `upload_date` populated — items get real dates +- Engagement metrics more accurate + +**Risk:** Could increase search time from ~5s to ~30-60s for 20 videos. Mitigate by reducing default count or increasing timeout. + +**Alternative if too slow:** Keep `--flat-playlist` but append year to search query: +```python +# Bias toward recent content since --dateafter doesn't work with flat-playlist +search_query = f"{core_topic} {from_date[:4]}" # e.g., "youtube thumbnail 2026" +``` + +#### 2b. Fix `_extract_core_subject()` for YouTube-relevant terms + +**File:** `scripts/lib/youtube_yt.py:67-76` + +Don't strip terms that are useful YouTube content type signals: + +```python +# YouTube-specific: keep 'tips', 'tutorial', 'review' etc. +# These are stripped for Reddit/X search but are valuable for YouTube +noise = { + 'best', 'top', 'good', 'great', 'awesome', 'killer', + 'latest', 'new', 'news', 'update', 'updates', + 'trending', 'hottest', 'popular', 'viral', + 'practices', 'features', + 'recommendations', 'advice', + 'prompt', 'prompts', 'prompting', + 'methods', 'strategies', 'approaches', +} +# NOTE: 'tips', 'tricks', 'tutorial', 'guide', 'review', 'reviews' +# are intentionally KEPT — they're YouTube content types +``` + +#### 2c. Clean question marks and trailing punctuation + +**File:** `scripts/lib/youtube_yt.py:48-80` + +```python +# At the end of _extract_core_subject(): +result = ' '.join(filtered) if filtered else text +return result.rstrip('?!.') # Clean trailing punctuation +``` + +### Phase 3: Polish (Medium — nice to have before launch) + +#### 3a. Increase subprocess timeout for non-flat-playlist mode + +**File:** `scripts/lib/youtube_yt.py:119-121` + +```python +# Before: +result = subprocess.run(cmd, capture_output=True, text=True, timeout=60) + +# After — resolving video pages takes longer: +result = subprocess.run(cmd, capture_output=True, text=True, timeout=120) +``` + +#### 3b. Add year hint to search query for recency bias + +Even with `--dateafter` working, YouTube's search algorithm ranks by relevance not recency. Adding the year helps: + +```python +# After core_topic extraction: +import datetime +current_year = datetime.datetime.now().year +search_query = f"{core_topic} {current_year}" +``` + +#### 3c. Reduce compact render item count for YouTube + +The compact render currently shows up to 15 YouTube items. With transcripts, this is too much output. Reduce to 10: + +**File:** `scripts/lib/render.py` — in `render_compact()`, add YouTube-specific limit or reduce the default. + +## Acceptance Criteria + +- [ ] `/last30days21 youtube thumbnail tips` shows YouTube in stats block +- [ ] YouTube items have real dates (not `None`) +- [ ] Old content (> 30 days) filtered out by `--dateafter` +- [ ] "youtube thumbnail tips" search returns videos about thumbnail tips (not generic old content) +- [ ] "How to access Seedance 2" returns recent Seedance 2 tutorials +- [ ] Script completes within 5 minutes for default depth +- [ ] No `2>&1` in SKILL.md bash command + +## Files to Modify + +| File | Changes | +|------|---------| +| `SKILL.md` | Remove `2>&1`, add YouTube synthesis instruction | +| `scripts/lib/youtube_yt.py` | Remove `--flat-playlist`, fix noise words, add year hint, increase timeout | +| `scripts/lib/render.py` | Optional: reduce YouTube item limit in compact mode | + +## Testing + +```bash +# Quick smoke test — should show YouTube items with dates +cd ~/.claude/skills/last30days21 +python3 scripts/last30days.py "youtube thumbnail tips" --quick --emit=compact 2>/dev/null | grep -c "youtube.com" + +# Date test — should show YYYY-MM-DD dates, not None +python3 -c " +from scripts.lib import youtube_yt +r = youtube_yt.search_youtube('youtube thumbnail tips', '2026-01-16', '2026-02-15', depth='quick') +for v in r['items'][:3]: print(v['date'], v['title'][:50]) +" + +# Full integration — run the skill in Claude Code and verify YouTube in stats +``` + +## References + +- SKILL.md bash command: `scripts/last30days.py` line 79 +- YouTube search: `scripts/lib/youtube_yt.py` lines 83-174 +- Core subject extraction: `scripts/lib/youtube_yt.py` lines 48-80 +- Compact render: `scripts/lib/render.py` lines 48-238 +- Prior YouTube plan: `docs/plans/2026-02-14-feat-youtube-transcript-search-plan.md` diff --git a/docs/plans/2026-02-15-fix-youtube-timeout-and-reddit-resilience-plan.md b/docs/plans/2026-02-15-fix-youtube-timeout-and-reddit-resilience-plan.md new file mode 100644 index 0000000..d167601 --- /dev/null +++ b/docs/plans/2026-02-15-fix-youtube-timeout-and-reddit-resilience-plan.md @@ -0,0 +1,102 @@ +--- +title: Fix YouTube Timeout and Reddit Resilience +type: fix +date: 2026-02-15 +--- + +# Fix YouTube Timeout and Reddit Resilience + +## Overview + +YouTube search+transcript fetching exceeds the 60s future timeout on popular topics (20 videos + 5 transcripts), discarding all results. Reddit 429 rate-limiting burns through the entire time budget with aggressive retries, causing global timeouts. Both issues cause the script to return incomplete results and sometimes crash. + +## Problem Statement + +**YouTube timeout (blocks launch):** YouTube found 20 Seedance 2.0 videos and fetched 4/5 transcripts, but the 60s future timeout killed everything — `✗ Error: YouTube search timed out after 60s`. The data was there; the budget wasn't. + +**Reddit 429 cascade (degrades reliability):** One enrichment item hitting 429 burns up to 93s (3 retries x 30s timeout + backoff) against a 45s budget. Phase 2 supplemental search then also 429s, burning another 30s. Total wasted: 75s on doomed requests, triggering the 180s global timeout. + +Observed failures across 6 test runs: +- `seedance 2 access`: YouTube timed out (60s), Reddit 0 threads +- `kanye west bully`: Global timeout first run, needed --quick retry +- `Peter Steinberger`: Global timeout during enrichment +- `nano banana pro`: Reddit timed out, YouTube worked (5 videos in time) +- `kanye west bully` (retry): Reddit 0 threads, YouTube 4 videos + +## Proposed Solution + +### Fix 1: Bump YouTube future timeout (NOT YET DONE) + +**File:** `scripts/last30days.py:40-44` + +Give YouTube its own timeout, separate from the shared `future` timeout. YouTube inherently takes longer because it does search + parallel transcript fetching. + +```python +# Option A: YouTube-specific timeout key +TIMEOUT_PROFILES = { + "quick": {"global": 90, "future": 30, "youtube_future": 60, ...}, + "default": {"global": 180, "future": 60, "youtube_future": 90, ...}, + "deep": {"global": 300, "future": 90, "youtube_future": 120, ...}, +} + +# Option B: Simpler — just bump default future to 90 for all sources +TIMEOUT_PROFILES = { + "quick": {"global": 90, "future": 45, ...}, + "default": {"global": 180, "future": 90, ...}, + "deep": {"global": 300, "future": 120, ...}, +} +``` + +**Recommendation:** Option A (YouTube-specific key) — keeps Reddit/X futures tight while giving YouTube the breathing room it needs. Reddit/X finish in 20-40s; YouTube needs 60-90s for transcript fetching. + +Then where YouTube future is collected (~line 646): +```python +youtube_timeout = timeouts.get("youtube_future", timeouts["future"]) +youtube_items, youtube_error = youtube_future.result(timeout=youtube_timeout) +``` + +### Fix 2: Reddit 429 fail-fast (ALREADY DONE — needs commit) + +**Status:** Implemented in working tree, uncommitted. Changes across 4 files: + +| File | Change | +|------|--------| +| `scripts/lib/http.py` | `get_reddit_json()` accepts `timeout` and `retries` params (was hardcoded 30s/3) | +| `scripts/lib/reddit_enrich.py` | `RedditRateLimitError` exception; `fetch_thread_data()` propagates 429; `enrich_reddit_item()` defaults to 10s timeout / 1 retry | +| `scripts/lib/openai_reddit.py` | `search_subreddits()` reduced to 1 retry; breaks subreddit loop on first 429 | +| `scripts/last30days.py` | Enrichment loop catches `RedditRateLimitError`, cancels futures, bails; `rate_limited` flag skips Phase 2 Reddit | + +**Impact:** 429 scenario drops from ~75s wasted to ~12s wasted. + +## Acceptance Criteria + +- [ ] YouTube `seedance 2 access` query completes with videos (was timing out at 60s) +- [ ] YouTube `kanye west bully` query returns 4+ videos with transcripts +- [ ] Reddit 429 detected within ~12s, remaining enrichment skipped +- [ ] Phase 2 Reddit skipped when rate-limited +- [ ] No global timeout on default depth for any of the 5 test queries +- [ ] All changes committed and synced to `~/.claude/skills/last30days21/` and `~/.claude/skills/last30days/` + +## Files to Modify + +| File | Status | Changes | +|------|--------|---------| +| `scripts/last30days.py` | Modify (partially done) | Add `youtube_future` to TIMEOUT_PROFILES; use it for YouTube future collection | +| `scripts/lib/http.py` | Done (uncommitted) | Parameterized `get_reddit_json()` timeout/retries | +| `scripts/lib/reddit_enrich.py` | Done (uncommitted) | `RedditRateLimitError`, fail-fast enrichment | +| `scripts/lib/openai_reddit.py` | Done (uncommitted) | 429 early-bail in `search_subreddits()` | + +## Testing + +```bash +# Quick smoke test — YouTube should complete within 90s +cd ~/.claude/skills/last30days21 +python3 scripts/last30days.py "seedance 2 access" --emit=compact 2>&1 | grep -E "YouTube|timeout" + +# Verify YouTube items in output +python3 scripts/last30days.py "kanye west bully" --quick --emit=compact 2>/dev/null | grep "youtube.com" | wc -l + +# Full integration — run the skill in Claude Code +# /last30days21 seedance 2 access +# Verify YouTube appears in stats block +``` diff --git a/docs/plans/2026-02-17-feat-github-release-v2.1-plan.md b/docs/plans/2026-02-17-feat-github-release-v2.1-plan.md new file mode 100644 index 0000000..cba4ed9 --- /dev/null +++ b/docs/plans/2026-02-17-feat-github-release-v2.1-plan.md @@ -0,0 +1,255 @@ +--- +title: "feat: Create GitHub Release for v2.1" +type: feat +date: 2026-02-17 +--- + +# Create GitHub Release for v2.1 + +## Overview + +The last30days skill is at v2.1 with 2,747 stars and 317 forks, but has **zero git tags and zero GitHub Releases**. The code is already live on the public repo (`upstream/main`). All marketing copy is written. This plan creates a proper v2.1.0 GitHub Release from existing materials. + +## Problem Statement + +GitHub Releases provide: +- A landing page for each version with formatted release notes +- Discoverability (GitHub shows releases in the sidebar, feeds them to the Explore page) +- A stable reference point for users (`git checkout v2.1.0`) +- A "Full Changelog" diff link +- RSS feed for watchers +- New contributor callouts (community goodwill) + +Currently users have no way to reference a specific version of the skill. The README says "v2.1" but there's nothing in git to anchor that. + +## Proposed Solution + +Create the first GitHub Release (v2.1.0) on the **public** repo using existing marketing copy. Use an annotated tag (not lightweight) for proper `git describe` support and fork propagation. + +## Implementation Steps + +### Phase 1: Tag and Release + +#### 1. Create CHANGELOG.md + +Create `CHANGELOG.md` following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format. Source material already exists: + +- `README.md` "What's New in V2.1" and "What's New in V2" sections +- `docs/v2.1-tweets.md` (raw verified test results, feature descriptions) +- `docs/v2.1-launch-copy.md` (feature copy, social posts) +- `docs/pr-credits.md` (contributor credits) +- Git log (`git log --oneline` for commit references) + +Structure: +```markdown +# Changelog + +## [2.1.0] - 2026-02-15 + +### Highlights + +30 days of research. 30 seconds of work. Four sources. Zero stale prompts. + +Three headline features... + +### Added +- Open-class skill with watchlists (SQLite-backed, FTS5) +- YouTube as 4th research source via yt-dlp (search + transcript extraction) +- OpenAI Codex CLI compatibility ($last30days invocation) +- Bundled X search (vendored Bird GraphQL client, no external CLI) +- Native web search backends (Parallel AI, Brave, OpenRouter/Perplexity Sonar) +- Briefing and history modes (open variant) +- --diagnose flag for source status checking +- --store flag for SQLite accumulation + +### Changed +- Smarter query construction (strips noise words, auto-retry) +- Two-phase search (Phase 2 entity-aware drill-down) +- Reddit JSON enrichment (real upvotes/comments from reddit.com/.json) +- Engagement-weighted scoring (relevance 45%, recency 25%, engagement 30%) +- Model auto-selection with 7-day cache + +### Fixed +- YouTube timeout increased to 90s +- Reddit 429 rate limit fail-fast +- YouTube soft date filter (keeps evergreen content) +- Eager import crash in __init__.py (Codex compatibility) + +### New Contributors +- @JosephOIbrahim - Windows Unicode fix +- @levineam - Model fallback for unverified orgs +- @jonthebeef - --days=N configurable lookback flag + +### Credits +- @steipete - Bird CLI (vendored X search) and yt-dlp inspiration +- @galligan - Marketplace plugin inspiration +- @hutchins - Pushed for YouTube feature + +## [1.0.0] - 2026-01-15 + +Initial public release. Reddit + X search via OpenAI and xAI APIs. +``` + +#### 2. Create annotated tag on the public repo + +```bash +cd /Users/mvanhorn/last30days-skill-private + +# Tag on the current HEAD (which matches upstream/main) +git tag -a v2.1.0 -m "Release v2.1.0: Watchlists, YouTube transcripts, Codex CLI, bundled X search" + +# Push to public repo +git push upstream v2.1.0 +``` + +Use annotated tag (not lightweight) because: +- Stores tagger metadata and date +- Works with `git describe` +- Propagates to forks (317 forks) +- Supports GPG signing if desired later + +#### 3. Craft release notes + +Assemble from existing copy. The release body should follow this structure: + +```markdown +## Highlights + +The AI world reinvents itself every month. This skill keeps you current. + +`/last30days` researches your topic across **Reddit, X, YouTube, and the web** +from the last 30 days, finds what the community is actually upvoting, sharing, +and saying on camera, and writes you a prompt that works today. + +**Three headline features in v2.1:** + +1. **Open-class skill with watchlists** - Track competitors, people, topics on a schedule. + Pair with Open Claw for automated briefings. SQLite-backed with FTS5 search. + +2. **YouTube transcripts as a 4th source** - When yt-dlp is installed, searches YouTube, + grabs view counts, and reads the actual transcripts. A 20-minute review has 10x the + signal of one X post. + +3. **Codex CLI compatibility** - Same skill, same engine, same four sources. + Install to `~/.agents/skills/last30days` and invoke with `$last30days`. + +Plus: **Bundled X search** - Vendored Bird GraphQL client. No external CLI needed. +Just Node.js 22+ and browser cookies. + +## Real Results (verified 2/15) + +| Topic | Reddit | X | YouTube | Web | +|-------|--------|---|---------|-----| +| Nano Banana Pro | - | 32 posts, 164 likes | 5 videos, 98K views | - | +| Seedance 2.0 | 21 threads | 33 posts | 20 videos | 5 pages | +| OpenClaw use cases | 35 threads, 1,130 upvotes | 23 posts | 20 videos, 1.57M views | - | +| YouTube thumbnails | 7 threads, 654 upvotes | 32 posts | 18 videos, 6.15M views | - | + +## What's New + +### Added +- Open-class skill with watchlist, briefing, and history modes +- YouTube search + transcript extraction via yt-dlp +- OpenAI Codex CLI compatibility +- Bundled Twitter/X search (vendored Bird GraphQL) +- Native web search (Parallel AI, Brave, OpenRouter) +- `--diagnose` and `--store` flags +- Conversational first-run experience (NUX) + +### Changed +- Two-phase search architecture (entity-aware drill-down) +- Reddit JSON enrichment for real engagement metrics +- Smarter query construction with auto-retry +- Engagement-weighted scoring algorithm + +### Fixed +- YouTube/Reddit timeout resilience +- Reddit 429 rate limit fail-fast +- Eager import crash in Codex environments + +## New Contributors + +- @JosephOIbrahim - Windows Unicode fix +- @levineam - Model fallback for unverified orgs +- @jonthebeef - `--days=N` configurable lookback + +## Credits + +- @steipete - Bird CLI and yt-dlp/summarize inspiration +- @galligan - Marketplace plugin inspiration +- @hutchins - Pushed for YouTube feature + +## Install + +```bash +# Claude Code +claude install-skill https://github.com/mvanhorn/last30days-skill + +# Manual +git clone https://github.com/mvanhorn/last30days-skill ~/.claude/skills/last30days +``` + +**Full Changelog**: https://github.com/mvanhorn/last30days-skill/commits/v2.1.0 +``` + +#### 4. Create the GitHub Release + +```bash +gh release create v2.1.0 \ + --repo mvanhorn/last30days-skill \ + --verify-tag \ + --title "v2.1.0 - Watchlists, YouTube Transcripts, Codex CLI" \ + -F release-notes.md +``` + +No binary assets needed - this is an interpreted skill, and GitHub auto-generates source archives. + +### Phase 2: Future Automation (Optional) + +#### 5. Add `.github/release.yml` for auto-generated notes on future releases + +```yaml +changelog: + exclude: + labels: + - ignore-for-release + categories: + - title: "Breaking Changes" + labels: [breaking-change] + - title: "Features" + labels: [enhancement, feature] + - title: "Bug Fixes" + labels: [bug, fix] + - title: "Documentation" + labels: [documentation] + - title: "Other Changes" + labels: ["*"] +``` + +This enables `gh release create v2.2.0 --generate-notes` for future releases with automatic PR categorization. + +## Acceptance Criteria + +- [x] `CHANGELOG.md` exists in repo root following Keep a Changelog format +- [x] Annotated tag `v2.1.0` exists on the public repo +- [x] GitHub Release `v2.1.0` is published at `github.com/mvanhorn/last30days-skill/releases` +- [x] Release notes include: highlights, real results table, feature list, contributor credits, install instructions +- [x] Release appears in the repo sidebar on GitHub + +## Key Decisions + +1. **v2.1.0 only** - Don't retroactively create v1.0.0 or v2.0.0 tags. The git history doesn't have clean boundary commits for those versions, and retroactive tags add complexity without value. + +2. **Release on the public repo** (`upstream`), not the private repo. Users see `mvanhorn/last30days-skill`. + +3. **CHANGELOG.md as source of truth** - The release notes are derived from CHANGELOG.md, not the other way around. Future releases update CHANGELOG.md first, then create the release. + +4. **Update star count** - The existing copy says "1.5K stars" but the repo now has 2,747. Update in release notes. + +## References + +- Existing copy: `docs/v2.1-launch-copy.md`, `docs/v2.1-tweets.md` +- Contributors: `docs/pr-credits.md` +- README features: `README.md` "What's New" sections +- GitHub Releases docs: https://docs.github.com/en/repositories/releasing-projects-on-github +- Keep a Changelog: https://keepachangelog.com/en/1.1.0/ diff --git a/docs/plans/2026-02-20-feat-last30days-com-trending-topics-plan.md b/docs/plans/2026-02-20-feat-last30days-com-trending-topics-plan.md new file mode 100644 index 0000000..49a6421 --- /dev/null +++ b/docs/plans/2026-02-20-feat-last30days-com-trending-topics-plan.md @@ -0,0 +1,274 @@ +--- +title: "feat: Last30Days.com - Automated Trending Topic Research" +type: feat +date: 2026-02-20 +--- + +# Last30Days.com - Automated Trending Topic Research + +## Overview + +A website at Last30Days.com that automatically shows daily trending topics researched by the /last30days engine. The core challenge: the skill is query-driven (you give it a topic), but a trending page needs to *discover* what topics to research. This plan covers how to source trending topics, run them through the engine, and publish results. + +## Problem Statement + +The /last30days skill has 2,747 GitHub stars but no public-facing showcase. Users have to install the skill and run it themselves. A website that automatically shows trending topic results would: + +1. **Drive installs** - people see the quality and want it for their own topics +2. **SEO surface area** - each topic page is indexable content +3. **Demonstrate capability** - "here's what /last30days found about X today" with real stats +4. **Content flywheel** - daily fresh content with zero manual curation + +## Trending Topic Discovery: The Options + +The skill has **no existing trending discovery mechanism** - it's entirely query-driven. Here are the available sources, ranked by practicality. + +### Tier 1: Free, High Signal, Zero Friction + +| Source | What It Returns | Auth | Cost | Best For | +|--------|----------------|------|------|----------| +| **Wikipedia Pageviews** | Top 100 most-viewed articles yesterday | None | Free | General public interest (news, culture, events) | +| **Hacker News** | Top 30 stories with scores | None | Free | Tech/startup topics | +| **Reddit r/all/hot + rising** | Hottest and rapidly rising posts | OAuth (free) | Free | Broad internet culture | +| **Google News RSS** | Top headlines, algorithmically curated | None | Free | Mainstream news | +| **YouTube mostPopular** | Top 50 trending videos by region | API key | Free (10K units/day) | Pop culture, entertainment | + +### Tier 2: Very Cheap, High Value + +| Source | What It Returns | Auth | Cost | Best For | +|--------|----------------|------|------|----------| +| **Perplexity Sonar API** | "What's trending today?" with sourced answers | API key | ~$1/month | Meta-aggregator that replaces multiple sources | +| **Google Trends (pytrends)** | Daily trending Google searches | None | Free but flaky | What people are actually searching | +| **Bird `getNews()`** | X Explore page trending topics | Browser cookies | Free | Real-time Twitter/X conversation | + +### Tier 3: Paid, Skip for MVP + +| Source | Cost | Why Skip | +|--------|------|----------| +| X/Twitter API | $200/month minimum | Too expensive; Bird `getNews()` is free | +| Exploding Topics | $249/month | Overkill for daily trends | +| TikTok | Gated, requires approval | Application process | + +### Existing Codebase Hooks + +Several pieces already exist in the codebase that could be leveraged: + +1. **Bird `getNews()` is already on disk.** The vendored Bird library at `scripts/lib/vendor/bird-search/` includes `twitter-client-news.js` which fetches X's Explore page tabs (For You, Trending, News, Sports, Entertainment). Only needs a ~30-line `bird-news.mjs` wrapper to expose it. Zero API keys needed. + +2. **`store.get_trending(days=7)`** at `scripts/store.py:559` ranks watchlist topics by recent finding activity. Could feed "what the skill has been researching" as a meta-signal. + +3. **Scoring algorithm** at `scripts/lib/score.py` has engagement formulas for Reddit, X, and YouTube that could rank "buzz" by reweighting engagement >> relevance. + +4. **Brave Trending API** exists but isn't implemented in `brave_search.py`. Could be added. + +## Proposed Architecture + +### Topic Discovery Pipeline (daily cron) + +``` +┌─────────────────────────────────────────────────┐ +│ STEP 1: Fetch trending signals (parallel, free) │ +├─────────────────────────────────────────────────┤ +│ Wikipedia Pageviews ─┐ │ +│ Hacker News Top 30 ─┤ │ +│ Reddit r/all/hot ─┼─→ Raw topics + signals │ +│ Google News RSS ─┤ │ +│ YouTube mostPopular ─┤ │ +│ Bird getNews() ─┘ │ +└─────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────┐ +│ STEP 2: Cluster + rank │ +├─────────────────────────────────────────────────┤ +│ Extract topic keywords from titles │ +│ Cluster by semantic similarity │ +│ Rank by cross-source frequency │ +│ ("Pope Leo XIV" on Wikipedia + Reddit + News │ +│ = high confidence trend) │ +│ Output: top 15-20 topics, ranked │ +└─────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────┐ +│ STEP 3: Run /last30days on top topics │ +├─────────────────────────────────────────────────┤ +│ Top 3-5 topics: full research (--emit=json) │ +│ → Full "What I learned" synthesis articles │ +│ Remaining 10-15: quick research (--quick) │ +│ → Stats teasers (32 X posts, 5 YouTube...) │ +└─────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────┐ +│ STEP 4: Publish to website │ +├─────────────────────────────────────────────────┤ +│ Generate static HTML/JSON │ +│ Deploy to Last30Days.com │ +│ RSS feed for subscribers │ +└─────────────────────────────────────────────────┘ +``` + +### Topic Clustering Strategy + +The hardest part is deduplicating across sources. "Pope Francis Dies" (Google News), "Pope_Francis" (Wikipedia #1 viewed), and a r/worldnews post are all the same topic. Approaches: + +**Option A: LLM clustering (recommended for MVP)** +Feed all raw titles to an LLM and ask it to cluster into distinct topics with a representative label. ~$0.01 per day via a fast model. Simple, accurate, handles edge cases. + +**Option B: TF-IDF + cosine similarity** +Extract keywords, compute pairwise similarity, agglomerative clustering. No API cost but worse on paraphrased titles. + +**Option C: Embedding similarity** +Embed all titles, cluster by cosine distance. Better than TF-IDF, costs a few cents per day. + +### Website Architecture + +**Option A: Static site (recommended for MVP)** +- Daily cron generates JSON + static HTML +- Host on GitHub Pages, Cloudflare Pages, or Vercel +- Zero server cost, zero maintenance +- Framework: plain HTML/CSS, or minimal Astro/11ty + +**Option B: Next.js with ISR** +- Incremental Static Regeneration rebuilds pages daily +- More flexibility for future features (search, filtering, user accounts) +- Hosting: Vercel free tier + +**Option C: Full web app** +- Database-backed, real-time updates, user accounts +- Overkill for MVP + +### Cost Estimate (daily operation) + +| Item | Cost | +|------|------| +| Trending source APIs | $0 (all free tier) | +| LLM clustering (fast model) | ~$0.01/day | +| Full research on 3-5 topics | ~$0.10-0.50/day (OpenAI API for Reddit search) | +| Quick research on 10-15 topics | ~$0.05-0.20/day | +| Static hosting | $0 (GitHub/Cloudflare Pages) | +| **Total** | **~$5-20/month** | + +## Implementation Phases + +### Phase 1: Topic Discovery Script (MVP) + +Build `scripts/discover_trending.py` that: +- Fetches Wikipedia Pageviews, HN top stories, Reddit hot, Google News RSS in parallel +- Filters out evergreen/non-topical Wikipedia pages (e.g., "Main Page", "ChatGPT" permanent traffic) +- Uses a fast LLM to cluster raw titles into 15-20 distinct topics +- Outputs ranked JSON: `[{topic, sources, confidence, category}]` + +**Acceptance criteria:** +- [ ] Fetches from at least 4 free trending sources in parallel +- [ ] Clusters raw titles into deduplicated topics via LLM +- [ ] Filters Wikipedia evergreen pages (maintain a blocklist) +- [ ] Outputs ranked JSON with topic name, source count, category +- [ ] Runs in < 60 seconds +- [ ] No paid API keys required for discovery (only LLM clustering) + +### Phase 2: Research Automation + +Wire discovered topics into the existing /last30days engine: +- Top 3-5 topics: `python3 scripts/last30days.py "$TOPIC" --emit=json --deep` +- Remaining topics: `python3 scripts/last30days.py "$TOPIC" --emit=json --quick` +- Store all results in `~/.local/share/last30days/trending/YYYY-MM-DD/` + +**Acceptance criteria:** +- [ ] Orchestration script runs discovery then research sequentially +- [ ] Full research on top N topics, quick on the rest +- [ ] Results stored as dated JSON files +- [ ] Total daily runtime < 30 minutes +- [ ] Handles timeouts/failures gracefully (skip topic, continue) + +### Phase 3: Website Generation + +Build a static site generator that reads the daily JSON and produces Last30Days.com: +- Homepage: today's trending topics grid (title, category, key stat, source badges) +- Topic pages: full synthesis for showcase topics, stats teaser for others +- Archive: previous days accessible by date +- RSS feed +- CTA: "Want to research your own topic? Install the skill" + +**Acceptance criteria:** +- [ ] Static HTML generated from daily JSON +- [ ] Homepage shows today's 15-20 trending topics +- [ ] 3-5 showcase topic pages with full synthesis +- [ ] Remaining topics show stats teasers + install CTA +- [ ] Deploys to Last30Days.com (Cloudflare Pages or similar) +- [ ] RSS feed for daily updates +- [ ] Mobile responsive + +### Phase 4: Bird Trending Integration (bonus) + +Wire up the already-vendored Bird `getNews()` for X trending: +- Create `scripts/lib/vendor/bird-search/bird-news.mjs` (~30 lines) +- Add X Explore trending data as a 5th discovery source +- X trends are the fastest-moving signal and fill the "what's happening right now" gap + +**Acceptance criteria:** +- [ ] `bird-news.mjs` wrapper exposes X Explore trending topics +- [ ] Integrated into discovery pipeline as an additional source +- [ ] Falls back gracefully if no X session cookies available + +## Alternative Approaches Considered + +### Perplexity-only approach +Just ask Perplexity Sonar "what are the top 20 trending topics today?" daily. Simpler, but: +- Single point of failure +- Less transparent (can't show "sourced from Reddit, Wikipedia, HN") +- Model may hallucinate or miss niche topics +- **Verdict:** Good fallback, not primary. + +### Curated topics (manual) +Manually pick topics each day. Defeats the purpose of automation. Could supplement for "editorial picks." + +### Social listening tools (Brandwatch, Sprout Social, etc.) +Expensive ($500+/month), enterprise-focused, overkill. + +## Technical Considerations + +- **Rate limits:** Wikipedia, HN, and RSS have no practical limits for 1 daily call. Reddit's 100 QPM is generous. YouTube's 10K units/day allows ~3,000 `mostPopular` calls. +- **Wikipedia filtering:** The top Wikipedia pages are always "Main Page", "Special:Search", etc. Need a blocklist of ~50 evergreen pages plus heuristics (skip pages under 1,000 characters, skip disambiguation pages). +- **Cron timing:** Run discovery at ~2am UTC (after Wikipedia pageviews finalize for previous day). Run research at ~3am UTC. Deploy site by ~5am UTC. +- **Cost control:** The /last30days engine uses OpenAI API for Reddit search. Full research on 5 topics * $0.05-0.10 each = ~$0.25-0.50/day. Quick research is cheaper. +- **Domain:** Last30Days.com needs to be registered (check availability). + +## Dependencies & Risks + +| Risk | Mitigation | +|------|------------| +| pytrends breaks (Google changes) | pytrends is a bonus source, not required. Other 4+ sources sufficient. | +| Wikipedia pageviews delayed | Fall back to Perplexity Sonar for day's topics | +| OpenAI API cost spikes | Cap at 5 full + 15 quick topics per day; use --quick for most | +| Bird cookie auth stops working | X trending is a bonus source; discovery works without it | +| Domain not available | Check availability; alternatives: last30.day, last30days.app | + +## Success Metrics + +- Daily automated publishing with zero manual intervention +- 15-20 trending topics surfaced daily +- 3-5 full synthesis articles per day +- Site loads in < 2 seconds (static) +- Drives measurable GitHub star growth / skill installs + +## References + +### Trending APIs (free) +- Wikipedia Pageviews: `wikimedia.org/api/rest_v1/metrics/pageviews/top/{project}/{access}/{year}/{month}/{day}` +- Hacker News: `hacker-news.firebaseio.com/v0/topstories.json` +- Reddit: `oauth.reddit.com/r/all/hot` (or `reddit.com/r/all/hot.json` unauthenticated) +- Google News RSS: `news.google.com/rss` +- YouTube: `googleapis.com/youtube/v3/videos?chart=mostPopular` + +### Existing codebase hooks +- Bird `getNews()`: `scripts/lib/vendor/bird-search/vendor/package/dist/lib/twitter-client-news.js` +- Store trending: `scripts/store.py:559` (`get_trending()`) +- Scoring: `scripts/lib/score.py` (engagement formulas) +- Brave Trending API: not implemented, available in Brave docs + +### Inspiration +- [Keep a Changelog](https://keepachangelog.com/) - clean daily update format +- [Hacker News front page](https://news.ycombinator.com/) - minimal trending UI +- [Exploding Topics](https://explodingtopics.com/) - trending topic showcase (paid, $249/mo) diff --git a/docs/plans/2026-02-25-analysis-cross-source-comparison-plan.md b/docs/plans/2026-02-25-analysis-cross-source-comparison-plan.md new file mode 100644 index 0000000..013fbe2 --- /dev/null +++ b/docs/plans/2026-02-25-analysis-cross-source-comparison-plan.md @@ -0,0 +1,3348 @@ +--- +title: Cross-Source Comparison Analysis and Improvement Plan +type: analysis +status: active +date: 2026-02-25 +--- + +# 15-Test Side-by-Side Comparison: Base vs HN vs CROSS + +## Executive Summary + +Ran 5 canonical topics through all 3 skill versions (Base, HN, CROSS) for 15 total full last30days runs. **CROSS wins every topic and every category.** YouTube dynamic scoring is the biggest differentiator - correctly promoting relevant videos and demoting off-topic ones. Cross-source linking works but is hampered by a too-strict threshold (3 items linked vs 26 possible with the hybrid fix). HN adds strong value for tech topics. + +**Overall Scores (weighted: 25% diversity + 25% score quality + 25% relevance accuracy + 25% bonus features):** + +| Version | Total | Avg/Topic | Win Rate | +|---------|-------|-----------|----------| +| **CROSS** | **370.1** | **74.0** | **5/5 topics** | +| HN | 315.5 | 63.1 | 0/5 | +| Base | 307.0 | 61.4 | 0/5 | + +## Test Configuration + +- **Versions:** Base (427a4e4, pre-HN), HN (main/f60a435), CROSS (feat/youtube-relevance-cross-source/0591f55) +- **Mode:** `--quick --emit=json` +- **Ordering:** Topic-sequential (all 3 versions of each topic back-to-back) +- **Date:** 2026-02-25 +- **`__pycache__` cleanup:** Between every git checkout +- **Source routing:** Identical across all 15 runs (verified via --diagnose) +- **Duration:** ~11 minutes for all 15 runs + +### 5 Test Topics + +| # | Topic | Category | +|---|-------|----------| +| 1 | "Claude Code skills and MCP servers" | Developer tools | +| 2 | "Seedance AI video generation" | Creative AI | +| 3 | "M4 MacBook Pro review" | Consumer tech | +| 4 | "best rap songs 2026" | Pop culture | +| 5 | "React vs Svelte 2026" | Framework debate | + +--- + +## Full Results: Source Coverage (15 runs) + +| Topic | Version | Reddit | X | YouTube | HN | Web | Total | +|-------|---------|--------|---|---------|----|----|-------| +| Claude Code | base | 4 | 11 | 10 | 0 | 0 | 25 | +| Claude Code | hn | 3 | 12 | 10 | 15 | 0 | 40 | +| Claude Code | cross | 10 | 12 | 10 | 15 | 0 | 47 | +| Seedance | base | 16 | 11 | 10 | 0 | 0 | 37 | +| Seedance | hn | 20 | 11 | 10 | 15 | 0 | 56 | +| Seedance | cross | 19 | 11 | 10 | 15 | 0 | 55 | +| MacBook | base | 3 | 12 | 10 | 0 | 0 | 25 | +| MacBook | hn | 3 | 12 | 10 | 0 | 0 | 25 | +| MacBook | cross | 3 | 12 | 10 | 0 | 0 | 25 | +| Rap songs | base | 6 | 12 | 3 | 0 | 0 | 21 | +| Rap songs | hn | 10 | 12 | 3 | 0 | 0 | 25 | +| Rap songs | cross | 11 | 12 | 3 | 0 | 0 | 26 | +| React/Svelte | base | 4 | 9 | 10 | 0 | 0 | 23 | +| React/Svelte | hn | 3 | 9 | 10 | 0 | 0 | 22 | +| React/Svelte | cross | 6 | 9 | 10 | 0 | 0 | 25 | + +**Key observations:** +- HN adds 15 items for Claude Code and Seedance (tech/AI), 0 for MacBook/Rap/React (non-HN audience) +- Reddit counts vary slightly between runs due to API non-determinism (e.g., Claude Code: 4 base vs 10 cross) +- X and YouTube are highly consistent across versions (same search code, similar results) + +--- + +## Full Results: YouTube Relevance Scores (15 runs) + +| Topic | Version | Min | Avg | Max | Scores | +|-------|---------|-----|-----|-----|--------| +| Claude Code | base | 0.70 | 0.70 | 0.70 | all 0.7 (hardcoded) | +| Claude Code | hn | 0.70 | 0.70 | 0.70 | all 0.7 (hardcoded) | +| Claude Code | cross | 0.60 | 0.70 | 0.80 | [0.6, 0.8, 0.8, 0.6, 0.6, 0.8, 0.6, 0.8, 0.8, 0.6] | +| Seedance | base | 0.70 | 0.70 | 0.70 | all 0.7 (hardcoded) | +| Seedance | hn | 0.70 | 0.70 | 0.70 | all 0.7 (hardcoded) | +| Seedance | cross | 0.25 | 0.68 | 1.00 | [0.75, 0.75, 0.75, 0.75, 0.75, 0.75, 1.0, 0.5, 0.5, 0.25] | +| MacBook | base | 0.70 | 0.70 | 0.70 | all 0.7 (hardcoded) | +| MacBook | hn | 0.70 | 0.70 | 0.70 | all 0.7 (hardcoded) | +| MacBook | cross | 0.75 | 0.82 | 1.00 | [0.75, 1.0, 1.0, 0.75, 1.0, 0.75, 0.75, 0.75, 0.75, 0.75] | +| Rap songs | base | 0.70 | 0.70 | 0.70 | all 0.7 (hardcoded) | +| Rap songs | hn | 0.70 | 0.70 | 0.70 | all 0.7 (hardcoded) | +| Rap songs | cross | 0.33 | 0.78 | 1.00 | [1.0, 1.0, 0.33] | +| React/Svelte | base | 0.70 | 0.70 | 0.70 | all 0.7 (hardcoded) | +| React/Svelte | hn | 0.70 | 0.70 | 0.70 | all 0.7 (hardcoded) | +| React/Svelte | cross | 0.25 | 0.53 | 1.00 | [0.75, 0.75, 0.75, 1.0, 0.75, 0.25, 0.25, 0.25, 0.25, 0.25] | + +### YouTube Matched-Item Analysis (Base vs CROSS, same videos) + +Videos matched by title across Base and CROSS runs of each topic: + +**Claude Code** (10/10 matched): +- Videos with "MCP" or "Skills" + "Claude Code" in title: promoted +4 to +5 score points (0.7 -> 0.8 relevance) +- Videos with only "Skills" or "MCP" (not both): demoted -4 to -5 points (0.7 -> 0.6) +- Correct behavior: videos matching more query keywords rank higher + +**Seedance** (8/9 matched): +- "Ultimate AI Video Generation - Seedance 2.0 PREVIEW": promoted +11 points (0.7 -> 1.0) - exact match +- "Seedance 2.0 Changes Filmmaking Forever": demoted -22 points (0.7 -> 0.25) - title about filmmaking, not AI video generation +- Correct behavior: off-topic videos correctly penalized + +**MacBook** (8/10 matched): +- "M4 MacBook Pro Review - Things to Know": promoted +8 points (0.7 -> 1.0) - exact match +- "The Macbook Pro M4 is Insane.": demoted -8 points (0.7 -> 0.75) - less specific match +- Correct behavior: specific reviews ranked above hype videos + +**Rap songs** (3/3 matched): +- "New Rap Songs 2026 Mix February": promoted +13 points (0.7 -> 1.0) - exact match +- "Lit Hip Hop Mix 2026": demoted -16 points (0.7 -> 0.33) - "hip hop" != "rap" in token matching +- Borderline: "hip hop" should probably match "rap" (improvement opportunity) + +**React/Svelte** (9/10 matched): +- "Vue vs React vs Svelte (2026)": promoted +9 points (0.7 -> 1.0) - contains both keywords +- "I Was Wrong About Svelte...": demoted -22 points (0.7 -> 0.25) - only has "Svelte", missing "React" +- "React wants to win you back...": demoted -21 points (0.7 -> 0.25) - only has "React", missing "Svelte" +- Correct behavior: comparison videos rank above single-framework videos + +--- + +## Full Results: Cross-Source Links (CROSS only) + +| Topic | Items Linked | Link Pairs | Example | +|-------|-------------|------------|---------| +| Claude Code | 0 | 0 | (none - max Jaccard was 0.37) | +| Seedance | 3 | 2 | Reddit-HN: "ByteDance launches Seedance 2.0" <-> "Seedance 2 Video Generator" | +| MacBook | 0 | 0 | (none - max Jaccard was 0.35) | +| Rap songs | 0 | 0 | (no cross-source overlap at all) | +| React/Svelte | 0 | 0 | (none - max Jaccard was 0.30) | + +**Total: 3 items linked out of ~178 across 5 tests.** The 0.5 char-trigram Jaccard threshold is far too strict. + +### With hybrid fix (token+trigram Jaccard at 0.40): + +| Topic | Current Links | Hybrid Links | Items Tagged | +|-------|--------------|-------------|-------------| +| Claude Code | 0 | 3 pairs | 5 items | +| Seedance | 2 | 19 pairs | 14 items | +| MacBook | 0 | 4 pairs | 5 items | +| Rap songs | 0 | 0 pairs | 0 items | +| React/Svelte | 0 | 1 pair | 2 items | +| **Total** | **2** | **27 pairs** | **26 items** | + +--- + +## Full Results: Score Distributions (Top 10) + +| Topic | Version | Top10 Avg | Top10 Median | #1 Score | #1 Source | +|-------|---------|-----------|-------------|----------|-----------| +| Claude Code | base | 73.3 | 74 | 86 | x | +| Claude Code | hn | 74.6 | 74 | 90 | hackernews | +| Claude Code | cross | 78.3 | 77 | 90 | hackernews | +| Seedance | base | 75.0 | 75 | 86 | x | +| Seedance | hn | 76.7 | 76 | 86 | x | +| Seedance | cross | 76.5 | 75 | 86 | x | +| MacBook | base | 66.5 | 64 | 86 | x | +| MacBook | hn | 66.5 | 64 | 86 | x | +| MacBook | cross | 66.7 | 64 | 86 | x | +| Rap songs | base | 64.7 | 64 | 86 | x | +| Rap songs | hn | 66.8 | 64 | 86 | x | +| Rap songs | cross | 66.2 | 64 | 86 | x | +| React/Svelte | base | 60.0 | 61 | 76 | x | +| React/Svelte | hn | 59.8 | 60 | 76 | x | +| React/Svelte | cross | 59.1 | 57 | 76 | x | + +**Key observations:** +- CROSS has highest top-10 average for Claude Code (78.3 vs 73.3 base) thanks to YouTube re-scoring promoting relevant videos +- HN #1 item (score 90) overtakes X #1 (score 86) for Claude Code - HN adds high-value technical content +- MacBook/Rap/React scores are similar across all versions (no HN contribution, YouTube changes are small) + +--- + +## Full Results: Source Diversity (Shannon Entropy, Top 15) + +| Topic | Base | HN | CROSS | Best | +|-------|------|-----|-------|------| +| Claude Code | 1.34 | 1.23 | 1.75 | **CROSS** | +| Seedance | 1.34 | 1.43 | 1.64 | **CROSS** | +| MacBook | 0.72 | 0.72 | 0.72 | tie | +| Rap songs | 0.91 | 1.24 | 1.10 | **HN** | +| React/Svelte | 1.43 | 1.40 | 1.40 | **Base** | + +CROSS wins diversity for tech topics because YouTube re-scoring redistributes which items rank in the top 15, creating a more balanced source mix. + +--- + +## Full Results: HN Value-Add + +**Claude Code (HN version):** 15 HN items, **5 in top 10** +- #1 (score:90) "Show HN: I built a 55K-word email marketing knowledge base and Claude Code skill" +- #6 (score:74) "Show HN: AI Marketing Skills for Claude Code" +- #7 (score:70) "Show HN: Poncho, a general agent harness built for the web" +- #8 (score:69) "Show HN: Axon - A Kubernetes-native framework for AI coding agents" +- #9 (score:65) "Show HN: Turn Claude Code or Codex into proactive, autonomous 24/7 AI agents" + +**Claude Code (CROSS version):** 15 HN items, **2 in top 10** +- #1 (score:90) "Show HN: I built a 55K-word email marketing knowledge base..." +- #9 (score:74) "Show HN: AI Marketing Skills for Claude Code" + +(Fewer HN in CROSS top 10 because CROSS has more Reddit items (10 vs 3) displacing HN from rankings) + +**Seedance:** 1 HN item in top 10 (both HN and CROSS versions) +- #8-10 (score:75) "Seedance 2.0 preview: The best video model of 2026" + +**MacBook, Rap, React/Svelte:** 0 HN items (expected - non-HN topics) + +--- + +## Scoring Verdict + +Weighted scoring: 25% diversity + 25% score quality + 25% relevance accuracy + 25% bonus features + +| Topic | Base | HN | CROSS | Winner | +|-------|------|-----|-------|--------| +| Claude Code | 64.4 | 69.3 | **73.9** | CROSS | +| Seedance | 64.9 | 63.2 | **84.4** | CROSS | +| MacBook | 59.7 | 59.7 | **66.0** | CROSS | +| Rap songs | 55.5 | 61.3 | **71.5** | CROSS | +| React/Svelte | 62.5 | 62.0 | **74.4** | CROSS | +| **TOTAL** | **307.0** | **315.5** | **370.1** | **CROSS** | + +### Category Winners + +| Category | Winner | Scores (Base / HN / CROSS) | +|----------|--------|---------------------------| +| Tech (Claude Code + React/Svelte) | **CROSS** | 127.0 / 131.3 / **148.3** | +| Consumer/Creative (MacBook + Seedance) | **CROSS** | 124.5 / 122.9 / **150.4** | +| Culture (Rap songs) | **CROSS** | 55.5 / 61.3 / **71.5** | + +**CROSS wins all 5 topics and all 3 categories. No regressions found.** + +### Why CROSS Wins + +1. **YouTube relevance accuracy** is the single biggest differentiator. It correctly promotes on-topic videos and demotes off-topic ones, changing rankings in every topic. +2. **Source diversity** improves for tech topics because YouTube re-scoring redistributes which items make the top 15. +3. **Cross-source linking** adds value where it fires (Seedance), but is currently too conservative to help in most cases. +4. **HN source** (shared with HN version) adds strong value for tech topics. +5. **No score regressions** - non-HN topics (MacBook, Rap, React) perform at least as well as Base. + +### Why CROSS Is Not Yet the GOAT + +1. **Cross-source linking barely works** - 3 items linked out of ~178 (1.7%). Should be ~26 items (14.6%) with hybrid fix. +2. **Cross-ref rendering is cryptic** - `[xref: HN5, HN4]` means nothing to users. Should show source names. +3. **"hip hop" doesn't match "rap"** in YouTube relevance scoring - needs synonym awareness. +4. **React/Svelte returns 0 HN items** - should have some, needs HN search tuning. + +--- + +## GOAT Improvement Plan: Make CROSS the Best Version + +### Priority 1: Fix Cross-Source Linking (HIGH IMPACT) + +Goes from 3 to 26 linked items across 5 tests. + +**Changes to `scripts/lib/dedupe.py`:** + +```python +# Add token Jaccard function +STOPWORDS = frozenset({ + 'the', 'a', 'an', 'to', 'for', 'how', 'is', 'in', 'of', 'on', + 'and', 'with', 'from', 'by', 'at', 'this', 'that', 'it', 'my', + 'your', 'i', 'me', 'we', 'you', 'what', 'are', 'do', 'can', + 'its', 'be', 'or', 'not', 'no', 'so', 'if', 'but', 'about', + 'all', 'just', 'get', 'has', 'have', 'was', 'will', 'show', 'hn', +}) + +def _tokenize_for_xref(text: str) -> Set[str]: + words = re.sub(r'[^\w\s]', ' ', text.lower()).split() + return {w for w in words if w not in STOPWORDS and len(w) > 1} + +def _token_jaccard(text_a: str, text_b: str) -> float: + tokens_a = _tokenize_for_xref(text_a) + tokens_b = _tokenize_for_xref(text_b) + if not tokens_a or not tokens_b: + return 0.0 + intersection = len(tokens_a & tokens_b) + union = len(tokens_a | tokens_b) + return intersection / union if union else 0.0 + +def _hybrid_similarity(text_a: str, text_b: str) -> float: + trigram_sim = jaccard_similarity(get_ngrams(text_a), get_ngrams(text_b)) + token_sim = _token_jaccard(text_a, text_b) + return max(trigram_sim, token_sim) +``` + +- [ ] Add `_tokenize_for_xref()`, `_token_jaccard()`, `_hybrid_similarity()` to dedupe.py +- [ ] Change `cross_source_link()` to use `_hybrid_similarity()` instead of `jaccard_similarity()` +- [ ] Lower threshold from 0.50 to 0.40 +- [ ] Strip "Show HN:" prefix from HN titles in `_get_cross_source_text()` +- [ ] Add tests for the new hybrid functions + +### Priority 2: Human-Readable Cross-Ref Tags (MEDIUM IMPACT) + +Change from `[xref: HN5, HN4]` to `[also on: HN, Reddit]`. + +**Changes to `scripts/lib/render.py`:** + +```python +def _xref_tag(item) -> str: + refs = getattr(item, 'cross_refs', None) + if not refs: + return "" + # Map IDs to source names + source_names = set() + for ref_id in refs: + if ref_id.startswith('R'): + source_names.add('Reddit') + elif ref_id.startswith('X'): + source_names.add('X') + elif ref_id.startswith('YT'): + source_names.add('YouTube') + elif ref_id.startswith('HN'): + source_names.add('HN') + elif ref_id.startswith('W'): + source_names.add('Web') + return f" [also on: {', '.join(sorted(source_names))}]" +``` + +- [ ] Update `_xref_tag()` in render.py to show source names instead of IDs +- [ ] Add a cross-ref summary count to the report footer (e.g., "3 topics discussed across multiple platforms") + +### Priority 3: YouTube Synonym Awareness (LOW IMPACT) + +"hip hop" should match "rap" in relevance scoring. Add a small synonym map. + +**Changes to `scripts/lib/youtube_yt.py`:** + +```python +SYNONYMS = { + 'hip': {'rap', 'hiphop'}, + 'rap': {'hip', 'hop', 'hiphop'}, + 'js': {'javascript'}, + 'javascript': {'js'}, + 'ts': {'typescript'}, + 'typescript': {'ts'}, + 'ai': {'artificial', 'intelligence'}, + 'ml': {'machine', 'learning'}, +} + +def _tokenize(text: str) -> Set[str]: + words = re.sub(r'[^\w\s]', ' ', text.lower()).split() + tokens = {w for w in words if w not in STOPWORDS and len(w) > 1} + # Expand synonyms + expanded = set(tokens) + for t in tokens: + if t in SYNONYMS: + expanded.update(SYNONYMS[t]) + return expanded +``` + +- [ ] Add SYNONYMS dict to youtube_yt.py +- [ ] Expand tokens with synonyms in `_tokenize()` +- [ ] Add tests for synonym matching + +### Priority 4: HN Search Broadening (LOW IMPACT) + +React/Svelte returned 0 HN items despite being a common HN topic. + +- [ ] Investigate HN Algolia query construction for framework topics +- [ ] Consider splitting multi-word topics into OR queries (e.g., "React" OR "Svelte") +- [ ] Test with broader HN search terms + +### Not Recommended + +- **Semantic similarity (embeddings)** - Token Jaccard at 0.40 already captures the right matches with zero dependencies +- **LLM-based matching** - Overkill for title comparison. Save LLM tokens for synthesis. +- **URL deduplication** - Different platforms rarely share URLs in titles + +--- + +## Raw Data + +All 15 JSON result files saved at `/tmp/last30days-comparison/full/`: + +| File | Size | Topic | Version | +|------|------|-------|---------| +| base-1-claude-code.json | 26KB | Claude Code | Base | +| hn-1-claude-code.json | 38KB | Claude Code | HN | +| cross-1-claude-code.json | 43KB | Claude Code | CROSS | +| base-2-seedance.json | 34KB | Seedance | Base | +| hn-2-seedance.json | 49KB | Seedance | HN | +| cross-2-seedance.json | 49KB | Seedance | CROSS | +| base-3-macbook.json | 26KB | MacBook | Base | +| hn-3-macbook.json | 26KB | MacBook | HN | +| cross-3-macbook.json | 26KB | MacBook | CROSS | +| base-4-rap.json | 23KB | Rap songs | Base | +| hn-4-rap.json | 25KB | Rap songs | HN | +| cross-4-rap.json | 27KB | Rap songs | CROSS | +| base-5-react-svelte.json | 25KB | React/Svelte | Base | +| hn-5-react-svelte.json | 24KB | React/Svelte | HN | +| cross-5-react-svelte.json | 27KB | React/Svelte | CROSS | + +Runner script: `/tmp/last30days-comparison/run-15-tests.sh` + +--- + +## Full Research Output (15 runs) + +The actual research results each version produced - the items, scores, titles, URLs, and insights that would be fed to Claude for synthesis. + +### Base - Claude Code skills and MCP servers + +#### Reddit Discussions + +**R2** (score:76) r/ClaudeCode (2026-02-22) [0 pts, 0 comments] + We 3x'd our team's Claude Code skill usage in 2 weeks — here's how + https://www.reddit.com/r/ClaudeCode/comments/1rbr5t7/we_3xd_our_teams_claude_code_skill_usage_in_2/ + *Discussion of Claude Code skills adoption/usage, plus mentions syncing skills/hooks/MCP configs.* + +**R4** (score:75) r/ClaudeCode (2026-02-23) [0 pts, 0 comments] + ClaudeInOne — a full framework for Claude Code, installed in one command + https://www.reddit.com/r/ClaudeCode/comments/1rcbria/claudeinone_a_full_framework_for_claude_code/ + *Framework bundling many Claude Code skills/agents/commands; lots of skills-related discussion.* + +**R1** (score:74) r/ClaudeCode (2026-02-18) [0 pts, 0 comments] + Self-improvement Loop: My favorite Claude Code Skill + https://www.reddit.com/r/ClaudeCode/comments/1r89084/selfimprovement_loop_my_favorite_claude_code_skill/ + *Directly about a Claude Code Skill (custom skill, how it works, SKILL.md).* + +**R3** (score:66) r/ClaudeCode (2026-02-10) [0 pts, 0 comments] + I built 12 SEO skills for Claude Code (open source) + https://www.reddit.com/r/ClaudeCode/comments/1r0l549/i_built_12_seo_skills_for_claude_code_open_source/ + *Concrete example of building and sharing Claude Code skills.* + +#### X/Twitter Posts + +**X6** (score:86) @zeeg (2026-02-25) [4 likes, 1 rp] + @adamwathan Codex has been crushing it for me with implicit skill usage to the point I was shocked + +Claude Code I resorted to explicit mentions and I + https://x.com/zeeg/status/2026745680195367091 + ** + +**X4** (score:80) @ihtesham2005 (2026-02-25) [6 likes, 0 rp] + 🚨 Anthropic just open-sourced the exact Skills library their own engineers use internally. + +Stop building Claude workflows from scratch. + +These are pl + https://x.com/ihtesham2005/status/2026752089473314975 + ** + +**X9** (score:74) @DevTenta (2026-02-25) [4 likes, 0 rp] + Day 7 building my first B2C app + +Been setting everything up this week. Manually built 5 agents to help me in the early stages and set up the Claude Co + https://x.com/DevTenta/status/2026742028730527786 + ** + +**X8** (score:64) @JorgeJaramillo (2026-02-25) [0 likes, 0 rp] + @Jompiras Claude code + skills. Muy breve. + https://x.com/JorgeJaramillo/status/2026743135435362649 + ** + +**X5** (score:64) @arpan7sarkar (2026-02-25) [0 likes, 0 rp] + @tom_doerr 135 agents, 35 curated skills, 121 plugins and 6 MCP configs all in one Claude Code toolkit is absolutely insane value 🤯 this is the kind o + https://x.com/arpan7sarkar/status/2026746479369662551 + ** + +**X7** (score:64) @arpan7sarkar (2026-02-25) [0 likes, 0 rp] + @tom_doerr A curated collection of Claude Skills organized by category is exactly what power users have been waiting for 🤩 document skills, code tools + https://x.com/arpan7sarkar/status/2026745559986614335 + **d + +**X2** (score:64) @yasuky (2026-02-25) [0 likes, 0 rp] + Claude CodeのSkillsを作成例から徹底理解する https://t.co/vZTseio2Ii + https://x.com/yasuky/status/2026754773786226708 + ** + +**X1** (score:64) @matgoldsborough (2026-02-25) [0 likes, 0 rp] + How: I told Claude Code "build me a CRM." Our builder skill generated schemas, skills, server, seed data. I pointed Claude Desktop at it. Contacts, le + https://x.com/matgoldsborough/status/2026755742737510749 + ** + +**X10** (score:64) @0x_Kapoor (2026-02-25) [0 likes, 0 rp] + I can build another open claw, or claude bot, or even a messaging app in a nice prompt and although having engineering skills, I can push the code on + https://x.com/0x_Kapoor/status/2026738663321907214 + ** + +**X11** (score:56) @ghumare64 (2026-02-25) [1 likes, 0 rp] + 1. I use https://t.co/NtFYJEiFs9 inspired by @bcherny +2. I use https://t.co/EwnmyWNFqn to sync skills and scan skills from the marketplace +3. I chat + https://x.com/ghumare64/status/2026736176695246966 + ** + +**X12** (score:56) @hamen (2026-02-25) [1 likes, 0 rp] + The 'vibe coding' revolution is forcing a reckoning. + +Developers with 15+ years experience are either embracing AI to expand their reach or watching t + https://x.com/hamen/status/2026734075243790396 + ** + +#### YouTube Videos + +**X8afcX2s2Mo** (score:74, rel:0.70) Grace Leung (2026-02-21) [47474 views, 1900 likes] + Claude Skills: Build Your First AI Marketing Team in 16 Minutes (Claude Code) + https://www.youtube.com/watch?v=X8afcX2s2Mo + *YouTube video about claude code skills and mcp servers* + Transcript: Every marketer I know is stretch themed. Too many channels, too many deliverables, never enough time, whether you're run... + +**0J2_YGuNrDo** (score:61, rel:0.70) Grace Leung (2025-12-16) [155806 views, 4760 likes] + Claude Code just Built me an AI Agent Team (Claude Code + Skills + MCP) + https://www.youtube.com/watch?v=0J2_YGuNrDo + *YouTube video about claude code skills and mcp servers* + Transcript: I use clot more than any other AI but even I dismiss Clark as just for developers and I was wrong after using myself I a... + +**vIUJ4Hd7be0** (score:59, rel:0.70) Leon van Zyl (2026-02-09) [25538 views, 699 likes] + Claude Code Skills - The Only Tutorial You Need + https://www.youtube.com/watch?v=vIUJ4Hd7be0 + *YouTube video about claude code skills and mcp servers* + +**Gqh_KdHP1Xk** (score:56, rel:0.70) Robin Ebers (2025-08-21) [90281 views, 2719 likes] + 8 MCP Servers That Make Claude Code 10x Better + https://www.youtube.com/watch?v=Gqh_KdHP1Xk + *YouTube video about claude code skills and mcp servers* + Transcript: Over 90% of all MCP servers are complete overhyped garbage. But some of them are not only worth it, they can be complete... + +**901VMcZq8X4** (score:51, rel:0.70) Sean Kochel (2025-10-21) [44602 views, 1356 likes] + These 5 Claude Code Skills Are Your New Unfair Advantage + https://www.youtube.com/watch?v=901VMcZq8X4 + *YouTube video about claude code skills and mcp servers* + +**l7qVtHpctic** (score:47, rel:0.70) Kenny Liao (2026-01-17) [21765 views, 728 likes] + Claude Code's MCP Problem Just Got Fixed + https://www.youtube.com/watch?v=l7qVtHpctic + *YouTube video about claude code skills and mcp servers* + +**M5CsRj6zSCA** (score:41, rel:0.70) Eric Tech (2025-12-12) [11286 views, 266 likes] + 5 MCP Servers That Make Claude Code 10x More Powerful (Full-Stack App Build) + https://www.youtube.com/watch?v=M5CsRj6zSCA + *YouTube video about claude code skills and mcp servers* + +**qthyl0GCpDo** (score:35, rel:0.70) Postman (2025-11-20) [7045 views, 105 likes] + Claude Skills vs MCP: What’s the Difference and When to Use Each? + https://www.youtube.com/watch?v=qthyl0GCpDo + *YouTube video about claude code skills and mcp servers* + +**ZroGqu7GyXM** (score:34, rel:0.70) Solo Swift Crafter (2025-10-27) [4880 views, 98 likes] + Claude Code Skills vs MCP vs Sub Agents: What Works for Solo Devs? + https://www.youtube.com/watch?v=ZroGqu7GyXM + *YouTube video about claude code skills and mcp servers* + +**jzf7DQa2CAc** (score:31, rel:0.70) Matt Kuda (2026-01-19) [2547 views, 87 likes] + How I Use Claude Code With Skills, MCP, Agents & Plugins + https://www.youtube.com/watch?v=jzf7DQa2CAc + *YouTube video about claude code skills and mcp servers* + +**Total: Reddit=4, X=11, YouTube=10, HN=0, Web=0** + +### HN - Claude Code skills and MCP servers + +#### Reddit Discussions + +**R1** (score:0) r/ClaudeAI (2025-06-17) [0 pts, 0 comments] + Claude code and mcp servers + https://www.reddit.com/r/ClaudeAI/comments/1ldm75a/claude_code_and_mcp_servers/ + *Directly about Claude Code failing to load/use MCP servers; troubleshooting discussion.* + +**R2** (score:0) r/ClaudeCode (2025-10-23) [0 pts, 0 comments] + Claude Skills: is it a big deal? + https://www.reddit.com/r/ClaudeCode/comments/1odtykp/claude_skills_is_it_a_big_deal/ + *Focused on the Claude (Claude Code) “Skills” feature; compares to AGENTS.md/slash commands/MCPs.* + +**R3** (score:0) r/ClaudeAI (2025-10-25) [0 pts, 0 comments] + I spent way too long cataloguing Claude Code tools. Here’s everything I found (with actual links) + https://www.reddit.com/r/ClaudeAI/comments/1ofltdr/i_spent_way_too_long_cataloguing_claude_code/ + *Tooling ecosystem post; explicitly mentions installing many plugins and MCP servers for Claude Code.* + +#### X/Twitter Posts + +**X5** (score:86) @zeeg (2026-02-25) [4 likes, 1 rp] + @adamwathan Codex has been crushing it for me with implicit skill usage to the point I was shocked + +Claude Code I resorted to explicit mentions and I + https://x.com/zeeg/status/2026745680195367091 + ** + +**X3** (score:80) @ihtesham2005 (2026-02-25) [6 likes, 0 rp] + 🚨 Anthropic just open-sourced the exact Skills library their own engineers use internally. + +Stop building Claude workflows from scratch. + +These are pl + https://x.com/ihtesham2005/status/2026752089473314975 + ** + +**X8** (score:74) @DevTenta (2026-02-25) [4 likes, 0 rp] + Day 7 building my first B2C app + +Been setting everything up this week. Manually built 5 agents to help me in the early stages and set up the Claude Co + https://x.com/DevTenta/status/2026742028730527786 + ** + +**X7** (score:64) @JorgeJaramillo (2026-02-25) [0 likes, 0 rp] + @Jompiras Claude code + skills. Muy breve. + https://x.com/JorgeJaramillo/status/2026743135435362649 + ** + +**X4** (score:64) @arpan7sarkar (2026-02-25) [0 likes, 0 rp] + @tom_doerr 135 agents, 35 curated skills, 121 plugins and 6 MCP configs all in one Claude Code toolkit is absolutely insane value 🤯 this is the kind o + https://x.com/arpan7sarkar/status/2026746479369662551 + ** + +**X6** (score:64) @arpan7sarkar (2026-02-25) [0 likes, 0 rp] + @tom_doerr A curated collection of Claude Skills organized by category is exactly what power users have been waiting for 🤩 document skills, code tools + https://x.com/arpan7sarkar/status/2026745559986614335 + ** + +**X2** (score:64) @yasuky (2026-02-25) [0 likes, 0 rp] + Claude CodeのSkillsを作成例から徹底理解する https://t.co/vZTseio2Ii + https://x.com/yasuky/status/2026754773786226708 + ** + +**X12** (score:64) @zhuoyuan45514 (2026-02-25) [0 likes, 0 rp] + Day 52 (01/21/2026) (Rest Day) + +> Go to New York + watch Broadway Show + +> Catch up on claude code updates on way back (2hrs) + - Learn difference be + https://x.com/zhuoyuan45514/status/2026733326631923776 + ** + +**X1** (score:64) @matgoldsborough (2026-02-25) [0 likes, 0 rp] + How: I told Claude Code "build me a CRM." Our builder skill generated schemas, skills, server, seed data. I pointed Claude Desktop at it. Contacts, le + https://x.com/matgoldsborough/status/2026755742737510749 + ** + +**X9** (score:64) @0x_Kapoor (2026-02-25) [0 likes, 0 rp] + I can build another open claw, or claude bot, or even a messaging app in a nice prompt and although having engineering skills, I can push the code on + https://x.com/0x_Kapoor/status/2026738663321907214 + ** + +**X10** (score:56) @ghumare64 (2026-02-25) [1 likes, 0 rp] + 1. I use https://t.co/NtFYJEiFs9 inspired by @bcherny +2. I use https://t.co/EwnmyWNFqn to sync skills and scan skills from the marketplace +3. I chat + https://x.com/ghumare64/status/2026736176695246966 + ** + +**X11** (score:56) @hamen (2026-02-25) [1 likes, 0 rp] + The 'vibe coding' revolution is forcing a reckoning. + +Developers with 15+ years experience are either embracing AI to expand their reach or watching t + https://x.com/hamen/status/2026734075243790396 + ** + +#### YouTube Videos + +**X8afcX2s2Mo** (score:74, rel:0.70) Grace Leung (2026-02-21) [47474 views, 1900 likes] + Claude Skills: Build Your First AI Marketing Team in 16 Minutes (Claude Code) + https://www.youtube.com/watch?v=X8afcX2s2Mo + *YouTube video about claude code skills and mcp servers* + +**0J2_YGuNrDo** (score:61, rel:0.70) Grace Leung (2025-12-16) [155806 views, 4760 likes] + Claude Code just Built me an AI Agent Team (Claude Code + Skills + MCP) + https://www.youtube.com/watch?v=0J2_YGuNrDo + *YouTube video about claude code skills and mcp servers* + Transcript: I use clot more than any other AI but even I dismiss Clark as just for developers and I was wrong after using myself I a... + +**fOxC44g8vig** (score:58, rel:0.70) Anthropic (2025-11-26) [129573 views, 2991 likes] + Claude Agent Skills Explained + https://www.youtube.com/watch?v=fOxC44g8vig + *YouTube video about claude code skills and mcp servers* + Transcript: Hi, my name is Otto and in this video we're going to discuss agent skills. Agents today are pretty intelligent, but they... + +**Gqh_KdHP1Xk** (score:56, rel:0.70) Robin Ebers (2025-08-21) [90281 views, 2719 likes] + 8 MCP Servers That Make Claude Code 10x Better + https://www.youtube.com/watch?v=Gqh_KdHP1Xk + *YouTube video about claude code skills and mcp servers* + Transcript: Over 90% of all MCP servers are complete overhyped garbage. But some of them are not only worth it, they can be complete... + +**421T2iWTQio** (score:51, rel:0.70) Kenny Liao (2025-10-24) [50477 views, 1010 likes] + The Only Claude Skills Guide You Need (Beginner to Expert) + https://www.youtube.com/watch?v=421T2iWTQio + *YouTube video about claude code skills and mcp servers* + +**l7qVtHpctic** (score:47, rel:0.70) Kenny Liao (2026-01-17) [21765 views, 728 likes] + Claude Code's MCP Problem Just Got Fixed + https://www.youtube.com/watch?v=l7qVtHpctic + *YouTube video about claude code skills and mcp servers* + +**M5CsRj6zSCA** (score:41, rel:0.70) Eric Tech (2025-12-12) [11287 views, 266 likes] + 5 MCP Servers That Make Claude Code 10x More Powerful (Full-Stack App Build) + https://www.youtube.com/watch?v=M5CsRj6zSCA + *YouTube video about claude code skills and mcp servers* + +**qthyl0GCpDo** (score:35, rel:0.70) Postman (2025-11-20) [7045 views, 105 likes] + Claude Skills vs MCP: What’s the Difference and When to Use Each? + https://www.youtube.com/watch?v=qthyl0GCpDo + *YouTube video about claude code skills and mcp servers* + +**ZroGqu7GyXM** (score:34, rel:0.70) Solo Swift Crafter (2025-10-27) [4885 views, 98 likes] + Claude Code Skills vs MCP vs Sub Agents: What Works for Solo Devs? + https://www.youtube.com/watch?v=ZroGqu7GyXM + *YouTube video about claude code skills and mcp servers* + +**jzf7DQa2CAc** (score:31, rel:0.70) Matt Kuda (2026-01-19) [2547 views, 87 likes] + How I Use Claude Code With Skills, MCP, Agents & Plugins + https://www.youtube.com/watch?v=jzf7DQa2CAc + *YouTube video about claude code skills and mcp servers* + +#### Hacker News Stories + +**HN1** (score:90) hn/cosmoblk (2026-02-21) [10 pts, 2 cmt] + Show HN: I built a 55K-word email marketing knowledge base and Claude Code skill + https://news.ycombinator.com/item?id=47096335 + *HN story about Show HN: I built a 55K-word email marketing knowledge base a* + - So it's an AI spambot? + +**HN4** (score:74) hn/superamped (2026-02-25) [3 pts, 1 cmt] + Show HN: AI Marketing Skills for Claude Code + https://news.ycombinator.com/item?id=47154377 + *HN story about Show HN: AI Marketing Skills for Claude Code* + +**HN12** (score:70) hn/heycesr (2026-02-18) [4 pts, 2 cmt] + Show HN: Poncho, a general agent harness built for the web + https://news.ycombinator.com/item?id=47061949 + *HN story about Show HN: Poncho, a general agent harness built for the web* + - I have been trying it and it's very cool! + +**HN11** (score:69) hn/gjkim042 (2026-02-24) [1 pts, 4 cmt] + Show HN: Axon – A Kubernetes-native framework for AI coding agents + https://news.ycombinator.com/item?id=47137491 + *HN story about Show HN: Axon – A Kubernetes-native framework for AI coding * + +**HN14** (score:65) hn/alternateman (2026-02-17) [3 pts, 2 cmt] + Show HN: Turn Claude Code or Codex into proactive, autonomous 24/7 AI agents + https://news.ycombinator.com/item?id=47054100 + *HN story about Show HN: Turn Claude Code or Codex into proactive, autonomou* + +**HN2** (score:64) hn/hauschildt (2026-02-19) [3 pts, 0 cmt] + Show HN: Agent skills to build photo, video and design editors on the web + https://news.ycombinator.com/item?id=47073035 + *HN story about Show HN: Agent skills to build photo, video and design edito* + +**HN8** (score:57) hn/barefootsanders (2026-02-25) [1 pts, 0 cmt] + Show HN: Upjack – Declarative framework for building apps over MCP + https://news.ycombinator.com/item?id=47157331 + *HN story about Show HN: Upjack – Declarative framework for building apps ov* + +**HN7** (score:56) hn/san-techie21 (2026-02-16) [1 pts, 1 cmt] + Show HN: Gulama – Security-first open-source AI agent (OpenClaw alternative) + https://news.ycombinator.com/item?id=47031982 + *HN story about Show HN: Gulama – Security-first open-source AI agent (OpenC* + +**HN15** (score:48) hn/martin-hall (2026-02-20) [1 pts, 0 cmt] + AI Skills Platform (Stealth) – Technical Co-Founder – Remote (US) – Equity + https://news.ycombinator.com/item?id=47081777 + *HN story about AI Skills Platform (Stealth) – Technical Co-Founder – Remote* + +**HN10** (score:47) hn/ClaytheMachine (2026-02-15) [1 pts, 0 cmt] + Show HN: SkillSandbox – Capability-based sandbox for AI agent skills (Rust) + https://news.ycombinator.com/item?id=47027734 + *HN story about Show HN: SkillSandbox – Capability-based sandbox for AI agen* + +**HN6** (score:47) hn/micronink (2026-01-30) [1 pts, 2 cmt] + Show HN: Indx.sh – Directory of AI coding rules, MCP servers, and tool + https://news.ycombinator.com/item?id=46822378 + *HN story about Show HN: Indx.sh – Directory of AI coding rules, MCP servers* + +**HN3** (score:45) hn/paolobietolini (2026-02-07) [1 pts, 0 cmt] + Show HN: GTM MCP Server- Let AI Manage Your Google Tag Manager Containers + https://news.ycombinator.com/item?id=46922159 + *HN story about Show HN: GTM MCP Server- Let AI Manage Your Google Tag Manag* + +**HN9** (score:38) hn/digitcatphd (2026-02-03) [1 pts, 0 cmt] + Show HN: ClawsMarket – Marketplace where AI agents discover tools + https://news.ycombinator.com/item?id=46878646 + *HN story about Show HN: ClawsMarket – Marketplace where AI agents discover * + +**HN13** (score:36) hn/aspectrr (2026-02-04) [1 pts, 0 cmt] + Show HN: Fluid.sh – Claude Code for Infrastructure + https://news.ycombinator.com/item?id=46886358 + *HN story about Show HN: Fluid.sh – Claude Code for Infrastructure* + +**HN5** (score:34) hn/victordg (2026-01-27) [1 pts, 0 cmt] + Claude Code skill for building ChatGPT Apps + https://news.ycombinator.com/item?id=46780766 + *HN story about Claude Code skill for building ChatGPT Apps* + +**Total: Reddit=3, X=12, YouTube=10, HN=15, Web=0** + +### CROSS - Claude Code skills and MCP servers + +#### Reddit Discussions + +**R1** (score:78) r/ClaudeCode (2026-02-23) [0 pts, 0 comments] + ClaudeInOne — a full framework for Claude Code, installed in one command + https://www.reddit.com/r/ClaudeCode/comments/1rcbria/claudeinone_a_full_framework_for_claude_code/ + *Directly about Claude Code skills (213 skills) and a reusable framework for Claude Code workflows.* + +**R10** (score:77) r/ClaudeCode (2026-02-25) [0 pts, 0 comments] + Claude Code works great… until you have too many MCP servers + https://www.reddit.com/r/ClaudeCode/comments/1rehy2x/claude_code_works_great_until_you_have_too_many/ + *Scaling issues and architecture discussion when using many MCP servers with Claude Code (mentions gateway).* + +**R2** (score:77) r/ClaudeCode (2026-02-22) [0 pts, 0 comments] + I built an MCP server that gives Claude Code a semantic graph of your codebase - went from ~15k tokens of grep output to ~3k of relevant context + https://www.reddit.com/r/ClaudeCode/comments/1rbu1nq/i_built_an_mcp_server_that_gives_claude_code_a/ + *An MCP server specifically designed to improve Claude Code context retrieval and reduce token waste.* + +**R9** (score:76) r/ClaudeAI (2026-02-24) [0 pts, 0 comments] + I built 25 MCP servers so Claude Code stops wasting tokens on terminal formatting + https://www.reddit.com/r/ClaudeAI/comments/1rd8z0d/i_built_25_mcp_servers_so_claude_code_stops/ + *Large set of MCP servers aimed at Claude Code usage; about MCP servers returning structured data.* + +**R4** (score:75) r/ClaudeCode (2026-02-22) [0 pts, 0 comments] + We 3x'd our team's Claude Code skill usage in 2 weeks — here's how + https://www.reddit.com/r/ClaudeCode/comments/1rbr5t7/we_3xd_our_teams_claude_code_skill_usage_in_2/ + *Team-level discussion of increasing Claude Code skill adoption; mentions syncing skills and MCP configs.* + +**R8** (score:72) r/ClaudeAI (2026-02-19) [0 pts, 0 comments] + I used Claude Code to build an MCP proxy server that gives it safe(r) access to my email. Here's what I learned about the security challenges + https://www.reddit.com/r/ClaudeAI/comments/1r99zbv/i_used_claude_code_to_build_an_mcp_proxy_server/ + *MCP server project built for/with Claude Code; discusses MCP server design and security tradeoffs.* + +**R3** (score:72) r/ClaudeCode (2026-02-18) [0 pts, 0 comments] + Self-improvement Loop: My favorite Claude Code Skill + https://www.reddit.com/r/ClaudeCode/comments/1r89084/selfimprovement_loop_my_favorite_claude_code_skill/ + *Focused on a concrete Claude Code skill (“wrap-up”) and how skills are used in real sessions.* + +**R5** (score:65) r/ClaudeCode (2026-02-10) [0 pts, 0 comments] + I built 12 SEO skills for Claude Code (open source) + https://www.reddit.com/r/ClaudeCode/comments/1r0l549/i_built_12_seo_skills_for_claude_code_open_source/ + *Showcase of multiple Claude Code skills and how they run via slash commands/subagents.* + +**R6** (score:57) r/ClaudeCode (2026-02-02) [0 pts, 0 comments] + [Skill Viewer] Built a Chrome extension to view Claude Code skills on GitHub - seeking feedback + https://www.reddit.com/r/ClaudeCode/comments/1qtq7yr/skill_viewer_built_a_chrome_extension_to_view/ + *Tooling around discovering/viewing Claude Code skills; directly about skills ecosystem.* + +**R14** (score:43) r/ClaudeCode (None) [0 pts, 0 comments] + Difference between Skills and these: Subagents, Claude.MD and slash commands? + https://www.reddit.com/r/ClaudeCode/comments/1o8t6xe/difference_between_skills_and_these_subagents/ + *Conceptual thread clarifying what Skills are versus subagents/CLAUDE.md/slash commands (relevant to skills usage).* + +#### X/Twitter Posts + +**X5** (score:86) @zeeg (2026-02-25) [5 likes, 1 rp] + @adamwathan Codex has been crushing it for me with implicit skill usage to the point I was shocked + +Claude Code I resorted to explicit mentions and I + https://x.com/zeeg/status/2026745680195367091 + ** + +**X3** (score:78) @ihtesham2005 (2026-02-25) [6 likes, 0 rp] + 🚨 Anthropic just open-sourced the exact Skills library their own engineers use internally. + +Stop building Claude workflows from scratch. + +These are pl + https://x.com/ihtesham2005/status/2026752089473314975 + ** + +**X8** (score:72) @DevTenta (2026-02-25) [4 likes, 0 rp] + Day 7 building my first B2C app + +Been setting everything up this week. Manually built 5 agents to help me in the early stages and set up the Claude Co + https://x.com/DevTenta/status/2026742028730527786 + ** + +**X7** (score:64) @JorgeJaramillo (2026-02-25) [0 likes, 0 rp] + @Jompiras Claude code + skills. Muy breve. + https://x.com/JorgeJaramillo/status/2026743135435362649 + ** + +**X4** (score:64) @arpan7sarkar (2026-02-25) [0 likes, 0 rp] + @tom_doerr 135 agents, 35 curated skills, 121 plugins and 6 MCP configs all in one Claude Code toolkit is absolutely insane value 🤯 this is the kind o + https://x.com/arpan7sarkar/status/2026746479369662551 + ** + +**X6** (score:64) @arpan7sarkar (2026-02-25) [0 likes, 0 rp] + @tom_doerr A curated collection of Claude Skills organized by category is exactly what power users have been waiting for 🤩 document skills, code tools + https://x.com/arpan7sarkar/status/2026745559986614335 + ** + +**X2** (score:64) @yasuky (2026-02-25) [0 likes, 0 rp] + Claude CodeのSkillsを作成例から徹底理解する https://t.co/vZTseio2Ii + https://x.com/yasuky/status/2026754773786226708 + ** + +**X12** (score:64) @zhuoyuan45514 (2026-02-25) [0 likes, 0 rp] + Day 52 (01/21/2026) (Rest Day) + +> Go to New York + watch Broadway Show + +> Catch up on claude code updates on way back (2hrs) + - Learn difference be + https://x.com/zhuoyuan45514/status/2026733326631923776 + ** + +**X1** (score:64) @matgoldsborough (2026-02-25) [0 likes, 0 rp] + How: I told Claude Code "build me a CRM." Our builder skill generated schemas, skills, server, seed data. I pointed Claude Desktop at it. Contacts, le + https://x.com/matgoldsborough/status/2026755742737510749 + ** + +**X9** (score:64) @0x_Kapoor (2026-02-25) [0 likes, 0 rp] + I can build another open claw, or claude bot, or even a messaging app in a nice prompt and although having engineering skills, I can push the code on + https://x.com/0x_Kapoor/status/2026738663321907214 + ** + +**X10** (score:56) @ghumare64 (2026-02-25) [1 likes, 0 rp] + 1. I use https://t.co/NtFYJEiFs9 inspired by @bcherny +2. I use https://t.co/EwnmyWNFqn to sync skills and scan skills from the marketplace +3. I chat + https://x.com/ghumare64/status/2026736176695246966 + ** + +**X11** (score:56) @hamen (2026-02-25) [1 likes, 0 rp] + The 'vibe coding' revolution is forcing a reckoning. + +Developers with 15+ years experience are either embracing AI to expand their reach or watching t + https://x.com/hamen/status/2026734075243790396 + ** + +#### YouTube Videos + +**X8afcX2s2Mo** (score:69, rel:0.60) Grace Leung (2026-02-21) [47512 views, 1900 likes] + Claude Skills: Build Your First AI Marketing Team in 16 Minutes (Claude Code) + https://www.youtube.com/watch?v=X8afcX2s2Mo + *YouTube: Claude Skills: Build Your First AI Marketing Team in 16 Minu* + Transcript: Every marketer I know is stretch themed. Too many channels, too many deliverables, never enough time, whether you're run... + +**0J2_YGuNrDo** (score:66, rel:0.80) Grace Leung (2025-12-16) [155806 views, 4760 likes] + Claude Code just Built me an AI Agent Team (Claude Code + Skills + MCP) + https://www.youtube.com/watch?v=0J2_YGuNrDo + *YouTube: Claude Code just Built me an AI Agent Team (Claude Code + Sk* + Transcript: I use clot more than any other AI but even I dismiss Clark as just for developers and I was wrong after using myself I a... + +**Gqh_KdHP1Xk** (score:61, rel:0.80) Robin Ebers (2025-08-21) [90281 views, 2719 likes] + 8 MCP Servers That Make Claude Code 10x Better + https://www.youtube.com/watch?v=Gqh_KdHP1Xk + *YouTube: 8 MCP Servers That Make Claude Code 10x Better* + Transcript: Over 90% of all MCP servers are complete overhyped garbage. But some of them are not only worth it, they can be complete... + +**vIUJ4Hd7be0** (score:54, rel:0.60) Leon van Zyl (2026-02-09) [25538 views, 699 likes] + Claude Code Skills - The Only Tutorial You Need + https://www.youtube.com/watch?v=vIUJ4Hd7be0 + *YouTube: Claude Code Skills - The Only Tutorial You Need* + +**901VMcZq8X4** (score:47, rel:0.60) Sean Kochel (2025-10-21) [44618 views, 1356 likes] + These 5 Claude Code Skills Are Your New Unfair Advantage + https://www.youtube.com/watch?v=901VMcZq8X4 + *YouTube: These 5 Claude Code Skills Are Your New Unfair Advantage* + +**M5CsRj6zSCA** (score:45, rel:0.80) Eric Tech (2025-12-12) [11286 views, 266 likes] + 5 MCP Servers That Make Claude Code 10x More Powerful (Full-Stack App Build) + https://www.youtube.com/watch?v=M5CsRj6zSCA + *YouTube: 5 MCP Servers That Make Claude Code 10x More Powerful (Full-* + +**l7qVtHpctic** (score:43, rel:0.60) Kenny Liao (2026-01-17) [21765 views, 728 likes] + Claude Code's MCP Problem Just Got Fixed + https://www.youtube.com/watch?v=l7qVtHpctic + *YouTube: Claude Code's MCP Problem Just Got Fixed* + +**ZroGqu7GyXM** (score:39, rel:0.80) Solo Swift Crafter (2025-10-27) [4885 views, 98 likes] + Claude Code Skills vs MCP vs Sub Agents: What Works for Solo Devs? + https://www.youtube.com/watch?v=ZroGqu7GyXM + *YouTube: Claude Code Skills vs MCP vs Sub Agents: What Works for Solo* + +**jzf7DQa2CAc** (score:36, rel:0.80) Matt Kuda (2026-01-19) [2547 views, 87 likes] + How I Use Claude Code With Skills, MCP, Agents & Plugins + https://www.youtube.com/watch?v=jzf7DQa2CAc + *YouTube: How I Use Claude Code With Skills, MCP, Agents & Plugins* + +**qthyl0GCpDo** (score:30, rel:0.60) Postman (2025-11-20) [7067 views, 105 likes] + Claude Skills vs MCP: What’s the Difference and When to Use Each? + https://www.youtube.com/watch?v=qthyl0GCpDo + *YouTube: Claude Skills vs MCP: What’s the Difference and When to Use * + +#### Hacker News Stories + +**HN1** (score:90) hn/cosmoblk (2026-02-21) [10 pts, 2 cmt] + Show HN: I built a 55K-word email marketing knowledge base and Claude Code skill + https://news.ycombinator.com/item?id=47096335 + *HN story about Show HN: I built a 55K-word email marketing knowledge base a* + - So it's an AI spambot? + +**HN4** (score:74) hn/superamped (2026-02-25) [3 pts, 1 cmt] + Show HN: AI Marketing Skills for Claude Code + https://news.ycombinator.com/item?id=47154377 + *HN story about Show HN: AI Marketing Skills for Claude Code* + +**HN12** (score:70) hn/heycesr (2026-02-18) [4 pts, 2 cmt] + Show HN: Poncho, a general agent harness built for the web + https://news.ycombinator.com/item?id=47061949 + *HN story about Show HN: Poncho, a general agent harness built for the web* + - I have been trying it and it's very cool! + +**HN11** (score:69) hn/gjkim042 (2026-02-24) [1 pts, 4 cmt] + Show HN: Axon – A Kubernetes-native framework for AI coding agents + https://news.ycombinator.com/item?id=47137491 + *HN story about Show HN: Axon – A Kubernetes-native framework for AI coding * + +**HN14** (score:65) hn/alternateman (2026-02-17) [3 pts, 2 cmt] + Show HN: Turn Claude Code or Codex into proactive, autonomous 24/7 AI agents + https://news.ycombinator.com/item?id=47054100 + *HN story about Show HN: Turn Claude Code or Codex into proactive, autonomou* + +**HN2** (score:64) hn/hauschildt (2026-02-19) [3 pts, 0 cmt] + Show HN: Agent skills to build photo, video and design editors on the web + https://news.ycombinator.com/item?id=47073035 + *HN story about Show HN: Agent skills to build photo, video and design edito* + +**HN8** (score:57) hn/barefootsanders (2026-02-25) [1 pts, 0 cmt] + Show HN: Upjack – Declarative framework for building apps over MCP + https://news.ycombinator.com/item?id=47157331 + *HN story about Show HN: Upjack – Declarative framework for building apps ov* + +**HN7** (score:56) hn/san-techie21 (2026-02-16) [1 pts, 1 cmt] + Show HN: Gulama – Security-first open-source AI agent (OpenClaw alternative) + https://news.ycombinator.com/item?id=47031982 + *HN story about Show HN: Gulama – Security-first open-source AI agent (OpenC* + +**HN15** (score:48) hn/martin-hall (2026-02-20) [1 pts, 0 cmt] + AI Skills Platform (Stealth) – Technical Co-Founder – Remote (US) – Equity + https://news.ycombinator.com/item?id=47081777 + *HN story about AI Skills Platform (Stealth) – Technical Co-Founder – Remote* + +**HN10** (score:47) hn/ClaytheMachine (2026-02-15) [1 pts, 0 cmt] + Show HN: SkillSandbox – Capability-based sandbox for AI agent skills (Rust) + https://news.ycombinator.com/item?id=47027734 + *HN story about Show HN: SkillSandbox – Capability-based sandbox for AI agen* + +**HN6** (score:47) hn/micronink (2026-01-30) [1 pts, 2 cmt] + Show HN: Indx.sh – Directory of AI coding rules, MCP servers, and tool + https://news.ycombinator.com/item?id=46822378 + *HN story about Show HN: Indx.sh – Directory of AI coding rules, MCP servers* + +**HN3** (score:45) hn/paolobietolini (2026-02-07) [1 pts, 0 cmt] + Show HN: GTM MCP Server- Let AI Manage Your Google Tag Manager Containers + https://news.ycombinator.com/item?id=46922159 + *HN story about Show HN: GTM MCP Server- Let AI Manage Your Google Tag Manag* + +**HN9** (score:38) hn/digitcatphd (2026-02-03) [1 pts, 0 cmt] + Show HN: ClawsMarket – Marketplace where AI agents discover tools + https://news.ycombinator.com/item?id=46878646 + *HN story about Show HN: ClawsMarket – Marketplace where AI agents discover * + +**HN13** (score:36) hn/aspectrr (2026-02-04) [1 pts, 0 cmt] + Show HN: Fluid.sh – Claude Code for Infrastructure + https://news.ycombinator.com/item?id=46886358 + *HN story about Show HN: Fluid.sh – Claude Code for Infrastructure* + +**HN5** (score:34) hn/victordg (2026-01-27) [1 pts, 0 cmt] + Claude Code skill for building ChatGPT Apps + https://news.ycombinator.com/item?id=46780766 + *HN story about Claude Code skill for building ChatGPT Apps* + +**Total: Reddit=10, X=12, YouTube=10, HN=15, Web=0** + +### Base - Seedance AI video generation + +#### Reddit Discussions + +**R23** (score:76) r/generativeAI (2026-02-24) [0 pts, 0 comments] + Official website for creating content with Seedance 2.0? + https://www.reddit.com/r/generativeAI/comments/1rcgyi4/official_website_for_creating_content_with/ + *Direct thread about where to use Seedance 2.0 for generating videos (and discussion of scams/third parties).* + +**R19** (score:76) r/generativeAI (2026-02-22) [0 pts, 0 comments] + This is terrifying!! Seedance 2.0 just generated a 1-minute film with ZERO editing — the entire film industry should be worried + https://www.reddit.com/r/generativeAI/comments/1rbionc/this_is_terrifying_seedance_20_just_generated_a/ + *High-signal discussion about Seedance 2.0 video generation quality (multi-shot coherence, transitions, filmmaking).* + +**R20** (score:75) r/Seedance_AI (2026-02-23) [0 pts, 0 comments] + Help! I need to use seedance or jimeng video function + https://www.reddit.com/r/Seedance_AI/comments/1rckgcm/help_i_need_to_use_seedance_or_jimeng_video/ + *Access/how-to thread specifically about using Seedance/Jimeng video generation.* + +**R18** (score:72) r/Seedance_AI (2026-02-20) [0 pts, 0 comments] + so disappointed and frustrated with recent changes (wasted 1k$+) + https://www.reddit.com/r/Seedance_AI/comments/1ra6nf4/so_disappointed_and_frustrated_with_recent/ + *User report about Seedance video generation failing review/guardrails and credit usage—directly about Seedance generation.* + +**R25** (score:71) r/SaasDevelopers (2026-02-24) [0 pts, 0 comments] + Third-party Seedance 2.0 API + https://www.reddit.com/r/SaasDevelopers/comments/1rdw3uz/thirdparty_seedance_20_api/ + *Discussion about Seedance 2.0 video generation availability via third-party API and Dreamina integration.* + +**R21** (score:71) r/Seedance_AI (2026-02-23) [0 pts, 0 comments] + What happened to doubao? + https://www.reddit.com/r/Seedance_AI/comments/1rcox3z/what_happened_to_doubao/ + *Discussion around ByteDance/Doubao access issues while trying to use Seedance 2 (video generation availability).* + +**R28** (score:68) r/Seedance_AI (2026-02-24) [0 pts, 0 comments] + Selfie Situation • Seedance 2.0 • Third-party API by useapi.net + https://www.reddit.com/r/Seedance_AI/comments/1rduxop/selfie_situation_seedance_20_thirdparty_api_by/ + *Seedance 2.0 discussion focused on third-party access and whether generations are truly Seedance 2.0 quality.* + +**R22** (score:68) r/AIGuild (2026-02-23) [0 pts, 0 comments] + ByteDance Faces Hollywood Backlash Over Seedance 2.0 + https://www.reddit.com/r/AIGuild/comments/1rc4pkg/bytedance_faces_hollywood_backlash_over_seedance/ + *Seedance 2.0 discussion tied to generated video content/IP concerns and potential impacts on access/release.* + +**R17** (score:67) r/AI_UGC_Marketing (2026-02-17) [0 pts, 0 comments] + Elevenlabs silently removed Seedance 2.0 after SAG-AFTRA incident, can’t create UGC style video + https://www.reddit.com/r/AI_UGC_Marketing/comments/1r70942/elevenlabs_silently_removed_seedance_20_after/ + *Discussion about Seedance 2.0 availability/removal in a toolchain for generating UGC-style AI videos.* + +**R27** (score:64) r/u_PoppyVonMiller (2026-02-24) [0 pts, 0 comments] + Seedance 2.0 Music Video + https://www.reddit.com/r/u_PoppyVonMiller/comments/1rdkptn/seedance_20_music_video/ + *Example/discussion of an output video made with Seedance 2.0 (AI video generation results).* + +**R13** (score:63) r/generativeAI (2026-02-10) [0 pts, 0 comments] + Where is the official Seedance website? + https://www.reddit.com/r/generativeAI/comments/1r19eem/where_is_the_official_seedance_website/ + *Seedance access/official-site discussion (important for actually generating videos with Seedance).* + +**R15** (score:62) r/AiVideos_NoRules (2026-02-15) [0 pts, 0 comments] + Seedance 2.0 + https://www.reddit.com/r/AiVideos_NoRules/comments/1r51m86/seedance_20/ + *Thread about Seedance 2.0 as an AI video generator and its capabilities/impact.* + +**R12** (score:62) r/GoogleGeminiAI (2026-02-09) [0 pts, 0 comments] + Has anyone used Seedance 2.0 yet? + https://www.reddit.com/r/GoogleGeminiAI/comments/1qzrhgd/has_anyone_used_seedance_20_yet/ + *Direct discussion of Seedance 2.0 usage/access for AI video generation and where people are trying it.* + +**R24** (score:61) r/aivideo (2026-02-24) [0 pts, 0 comments] + [Removed] + https://www.reddit.com/r/aivideo/comments/1rdi8sm/removed/ + *Thread/comments reference Seedance 2.0 access and community moderation around AI-generated video/IP.* + +**R16** (score:61) r/nairobitechies (2026-02-16) [0 pts, 0 comments] + ByteDance Seedance: Multimodal Video Generation Reaches a New Threshold + https://www.reddit.com/r/nairobitechies/comments/1r6gwyj/bytedance_seedance_multimodal_video_generation/ + *Seedance family discussion (1.0/1.5/2.0) focusing on video generation features and multimodal inputs.* + +**R14** (score:58) r/u_Impossible-Dish409 (2026-02-11) [0 pts, 0 comments] + Seedance 2.0 – What’s New and Why It Matters + https://www.reddit.com/r/u_Impossible-Dish409/comments/1r1lekj/seedance_20_whats_new_and_why_it_matters/ + *Overview/discussion of Seedance 2.0 capabilities for multimodal AI video generation and editing.* + +#### X/Twitter Posts + +**X9** (score:86) @HBCoop_ (2026-02-25) [9 likes, 0 rp] + First Seedance 2.0 Test! + +Seedance 5.0 Lite @krea_ai → Seedance 2.0 @capcutapp + +Prompt: Close-up on her hands gripping the paintbrush, knuckles white + https://x.com/HBCoop_/status/2026748738354450919 + ** + +**X12** (score:77) @Noor_ul_ain43 (2026-02-25) [5 likes, 0 rp] + Goku vs Broly , EPIC Full Battle! +Witness the ultimate Saiyan showdown recreated with AI power. Explosive transformations, insane energy blasts, and + https://x.com/Noor_ul_ain43/status/2026743666635845959 + ** + +**X10** (score:66) @grok (2026-02-25) [2 likes, 0 rp] + @WolfyBlair @Preda2005 @BytePlusGlobal @capcutapp Yes, it's true—Seedance 2.0 is now live in CapCut (desktop & mobile). + +See it in the AI video t + https://x.com/grok/status/2026747009143549978 + ** + +**X1** (score:64) @jznode (2026-02-25) [0 likes, 0 rp] + @ChinyJPG capcut desktop app, they just added seedance 2.0 today. no chinese phone number needed, works globally. download the desktop version, look f + https://x.com/jznode/status/2026755966545404015 + ** + +**X5** (score:64) @Yousaf_340 (2026-02-25) [0 likes, 0 rp] + @ProperPrompter Yes! Seedance 2.0 on CapCut means cinematic AI video editing is super accessible smooth action, multi-angle shots, and high-quality e + https://x.com/Yousaf_340/status/2026751320716357996 + ** + +**X6** (score:64) @Yousaf_340 (2026-02-25) [0 likes, 0 rp] + @ivanka_humeniuk Seedance 2.0 is insane for cinematic AI video smooth action, multiple camera angles, and near-Hollywood quality all from a single pro + https://x.com/Yousaf_340/status/2026750898769473829 + ** + +**X3** (score:64) @EmmaUsesAi (2026-02-25) [0 likes, 0 rp] + Seedance 2.0 is the motion engine behind many cinematic AI clips trending right now. + +It’s now native inside NemoVideo. + +You can: +- Identify proven fo + https://x.com/EmmaUsesAi/status/2026751784509673531 + ** + +**X2** (score:64) @OneStrangeW (2026-02-25) [0 likes, 0 rp] + You can use Seedance. If Seedance 2.0 hasn't launch yet, the prior version is great, and stick pretty well to the prompt. Of course the video agent mu + https://x.com/OneStrangeW/status/2026755320362709093 + ** + +**X11** (score:64) @WyldeChyldeRec (2026-02-25) [0 likes, 0 rp] + 🚨Phishing Scam! + +Watch for emails titled: + +[Seedance & Wan AI] Unrestricted generation with Grok Imagine video added + +This is not real. We don't h + https://x.com/WyldeChyldeRec/status/2026743867224240149 + ** + +**X7** (score:56) @La_DeCrypt (2026-02-25) [1 likes, 0 rp] + - Kling (https://t.co/WGphQsdLf1) + +- Seedance 2.0 (https://t.co/A8UzLTRGNo) + +☞ AI-powered video editing: + +- Descript (https://t.co/q4axNEjDzt) +- Opus + https://x.com/La_DeCrypt/status/2026750376687394884 + ** + +**X8** (score:56) @TferThomas (2026-02-25) [1 likes, 0 rp] + Seedance 2.0 might be gen AI video’s next big hope, but it’s still slop https://t.co/kT1baOYJQk #AI + https://x.com/TferThomas/status/2026749218224746609 + ** + +#### YouTube Videos + +**F1kWxdfiBNE** (score:73, rel:0.70) AI Filmmaking Academy (2026-02-22) [40982 views, 1088 likes] + Seedance 2.0 Claims the AI Video Throne! + https://www.youtube.com/watch?v=F1kWxdfiBNE + *YouTube video about seedance ai video generation* + +**_o2MuUX9UYg** (score:73, rel:0.70) Theoretically Media (2026-02-09) [199908 views, 4986 likes] + Seedance 2.0 Claims the AI Video Throne! + https://www.youtube.com/watch?v=_o2MuUX9UYg + *YouTube video about seedance ai video generation* + Transcript: So, it's been about a week since I declared Cling 3.0 the new benchmark for state-of-the-art AI video. And well, I mean,... + +**jkPTYD5lXo8** (score:70, rel:0.70) How To In 5 Minutes (2026-02-19) [44344 views, 782 likes] + 100% FREE Seedance 2.0 AI Video Generator : How to Use It WORLDWIDE + https://www.youtube.com/watch?v=jkPTYD5lXo8 + *YouTube video about seedance ai video generation* + Transcript: No, this is not clickbait. You can now access Cance 2.0 for free from anywhere. No registration required and depending o... + +**W_lxyDFDZt4** (score:69, rel:0.70) WealthWise (2026-02-12) [126599 views, 1204 likes] + Seedance 2.0: The New Best AI Video Generator | Sora 2 Destroyed + https://www.youtube.com/watch?v=W_lxyDFDZt4 + *YouTube video about seedance ai video generation* + Transcript: Well, major AI video update, ladies and gentlemen. Seance 2.0 is right around the corner. And let me tell you guys, this... + +**R1zl92NhCfE** (score:65, rel:0.70) Ai Lockup (2026-02-16) [32699 views, 591 likes] + Seedance 2.0 How To Use and Become a Pro AI Film Maker With This AI Video Generator + https://www.youtube.com/watch?v=R1zl92NhCfE + *YouTube video about seedance ai video generation* + +**G1Ad4a8sdJU** (score:63, rel:0.70) Benji’s AI Playground (2026-02-14) [38581 views, 419 likes] + SeeDance 2.0: The Next Level of AI Video — And What It Means for Local AI Users? + https://www.youtube.com/watch?v=G1Ad4a8sdJU + *YouTube video about seedance ai video generation* + +**n625xVounGM** (score:61, rel:0.70) John Savage AI (2026-02-23) [7184 views, 126 likes] + How To Generate FREE AI Seedance 2.0 Videos (Access Worldwide) + https://www.youtube.com/watch?v=n625xVounGM + *YouTube video about seedance ai video generation* + +**kJ0NAVmd4f4** (score:58, rel:0.70) Rogue Cell Pictures (2026-02-25) [1544 views, 147 likes] + Seedance 2.0 Changes Filmmaking Forever | New Original Series + https://www.youtube.com/watch?v=kJ0NAVmd4f4 + *YouTube video about seedance ai video generation* + +**FP8TaJSFohs** (score:55, rel:0.70) Chem Beast (2026-02-24) [3194 views, 31 likes] + How To Use Seedance 2 0 Full AI Video Generator Tutorial + Free Access Guide + https://www.youtube.com/watch?v=FP8TaJSFohs + *YouTube video about seedance ai video generation* + +**61ThJGqwHsI** (score:48, rel:0.70) xCreate (2026-02-11) [4854 views, 86 likes] + Ultimate AI Video Generation - Seedance 2.0 PREVIEW + https://www.youtube.com/watch?v=61ThJGqwHsI + *YouTube video about seedance ai video generation* + +**Total: Reddit=16, X=11, YouTube=10, HN=0, Web=0** + +### HN - Seedance AI video generation + +#### Reddit Discussions + +**R2** (score:79) r/singularity (2026-02-25) [0 pts, 0 comments] + Official: Seedance 2.0 now live in CapCut desktop and API access available, details below + https://www.reddit.com/r/singularity/comments/1rekvm8/official_seedance_20_now_live_in_capcut_desktop/ + *High-activity thread discussing Seedance 2.0 launch claims, CapCut availability, censorship, and pricing.* + +**R3** (score:77) r/generativeAI (2026-02-24) [0 pts, 0 comments] + Official website for creating content with Seedance 2.0? + https://www.reddit.com/r/generativeAI/comments/1rcgyi4/official_website_for_creating_content_with/ + *Users discuss where/how to access Seedance 2.0 and warn about scams.* + +**R4** (score:76) r/AITalkers (2026-02-24) [0 pts, 0 comments] + Is there an official way to create content using Seedance 2.0? + https://www.reddit.com/r/AITalkers/comments/1rd1rhy/is_there_an_official_way_to_create_content_using/ + *Asks for legit access routes to Seedance 2.0; includes discussion of official vs third-party options.* + +**R5** (score:75) r/Seedance_AI (2026-02-23) [0 pts, 0 comments] + Help! I need to use seedance or jimeng video function + https://www.reddit.com/r/Seedance_AI/comments/1rckgcm/help_i_need_to_use_seedance_or_jimeng_video/ + *Seedance/Jimeng video generation access and troubleshooting discussion.* + +**R6** (score:75) r/generativeAI (2026-02-23) [0 pts, 0 comments] + Seedance 2.0 is available in Open Source tools already + https://www.reddit.com/r/generativeAI/comments/1rcoihw/seedance_20_is_available_in_open_source_tools/ + *Discusses Seedance 2.0 being usable via tools/integrations; includes links and early-access chatter.* + +**R8** (score:74) r/generativeAI (2026-02-22) [0 pts, 0 comments] + This is terrifying!! Seedance 2.0 just generated a 1-minute film with ZERO editing — the entire film industry should be worried + https://www.reddit.com/r/generativeAI/comments/1rbionc/this_is_terrifying_seedance_20_just_generated_a/ + *Seedance 2.0 output showcase + discussion of cinematic coherence and workflow.* + +**R7** (score:73) r/AIGuild (2026-02-23) [0 pts, 0 comments] + ByteDance Faces Hollywood Backlash Over Seedance 2.0 + https://www.reddit.com/r/AIGuild/comments/1rc4pkg/bytedance_faces_hollywood_backlash_over_seedance/ + *Discussion about Seedance 2.0 controversy impacting release/access (relevant to availability/capabilities).* + +**R9** (score:73) r/Seedance_AI (2026-02-22) [0 pts, 0 comments] + Is Jimeng silently banning accounts on Jimeng.Jianying? Paid account stuck with “network error” but free account works. + https://www.reddit.com/r/Seedance_AI/comments/1rbk1qb/is_jimeng_silently_banning_accounts_on/ + *Seedance/Jimeng video generation failures and account restriction speculation.* + +**R12** (score:73) r/Seedance_AI (2026-02-21) [0 pts, 0 comments] + Jimeng Web for seedance 2.0 “Network Error, Generation Failed” for 24 Hours – Anyone Else? + https://www.reddit.com/r/Seedance_AI/comments/1raqsnu/jimeng_web_for_seedance_20_network_error/ + *Focused troubleshooting thread about Seedance 2.0 generation failures on Jimeng web.* + +**R10** (score:71) r/u_Educational-Lion7812 (2026-02-21) [0 pts, 0 comments] + Seedance 2.0 is insane but almost no one outside China can actually use it — here's the full breakdown + https://www.reddit.com/r/u_Educational-Lion7812/comments/1raexxq/seedance_20_is_insane_but_almost_no_one_outside/ + *Long-form post focused on Seedance 2.0 access constraints and how people are using it.* + +**R11** (score:71) r/Seedance_AI (2026-02-21) [0 pts, 0 comments] + so disappointed and frustrated with recent changes (wasted 1k$+) + https://www.reddit.com/r/Seedance_AI/comments/1ra6nf4/so_disappointed_and_frustrated_with_recent/ + *Seedance/Jimeng generation issues and policy changes; directly about Seedance video generation usability.* + +**R15** (score:66) r/singularity (2026-02-13) [0 pts, 0 comments] + ByteDance releases Seedance 2.0 video model with Director mode and multimodal upgrades + https://www.reddit.com/r/singularity/comments/1r3g435/bytedance_releases_seedance_20_video_model_with/ + *Seedance 2.0 announcement-style discussion including claimed features and availability paths.* + +**R13** (score:65) r/Seedance_AI (2026-02-15) [0 pts, 0 comments] + Seedance 2.0 Made This LeBron James vs Regular Person Look Real + https://www.reddit.com/r/Seedance_AI/comments/1r5ncbw/seedance_20_made_this_lebron_james_vs_regular/ + *Seedance 2.0 video output example + prompting discussion (text-to-video).* + +**R21** (score:64) r/AI_UGC_Marketing (2026-02-17) [0 pts, 0 comments] + Elevenlabs silently removed Seedance 2.0 after SAG-AFTRA incident, can’t create UGC style video + https://www.reddit.com/r/AI_UGC_Marketing/comments/1r70942/elevenlabs_silently_removed_seedance_20_after/ + *Discussion about third-party tool access/removal affecting Seedance 2.0 video generation workflows.* + +**R14** (score:64) r/Seedance_AI (2026-02-14) [0 pts, 0 comments] + be very careful of scam sites that do not have it like nemovideo + https://www.reddit.com/r/Seedance_AI/comments/1r4va5a/be_very_careful_of_scam_sites_that_do_not_have_it/ + *Thread about Seedance 2.0 access claims and scam warnings; relevant to where to generate videos.* + +**R17** (score:63) r/ByteDance (2026-02-13) [0 pts, 0 comments] + The censorship just ruined Seedance 2.0 + https://www.reddit.com/r/ByteDance/comments/1r43yl8/the_censorship_just_ruined_seedance_20/ + *Discusses Seedance 2.0 restrictions that affect what videos you can generate.* + +**R16** (score:63) r/AIHubSpace (2026-02-13) [0 pts, 0 comments] + [SEEDANCE 2.0] I made the post and many of you doubted it. I warned you that “Hollywood” would take action. + https://www.reddit.com/r/AIHubSpace/comments/1r3satk/seedance_20_i_made_the_post_and_many_of_you/ + *Discussion about Seedance 2.0 clips and legal/copyright reaction impacting video generation usage.* + +**R18** (score:62) r/AIGuild (2026-02-12) [0 pts, 0 comments] + Seedance 2.0: ByteDance’s New Movie-Making Machine + https://www.reddit.com/r/AIGuild/comments/1r3940k/seedance_20_bytedances_new_moviemaking_machine/ + *Seedance 2.0 overview thread focused on multimodal AI video generation and controls.* + +**R19** (score:60) r/u_Impossible-Dish409 (2026-02-11) [0 pts, 0 comments] + Seedance 2.0 – What’s New and Why It Matters + https://www.reddit.com/r/u_Impossible-Dish409/comments/1r1lekj/seedance_20_whats_new_and_why_it_matters/ + *Feature rundown and comparisons for Seedance 2.0 video generation capabilities.* + +**R20** (score:58) r/GoogleGeminiAI (2026-02-09) [0 pts, 0 comments] + Has anyone used Seedance 2.0 yet? + https://www.reddit.com/r/GoogleGeminiAI/comments/1qzrhgd/has_anyone_used_seedance_20_yet/ + *General user discussion about whether/where Seedance 2.0 can be used and early impressions.* + +#### X/Twitter Posts + +**X9** (score:86) @HBCoop_ (2026-02-25) [9 likes, 0 rp] + First Seedance 2.0 Test! + +Seedance 5.0 Lite @krea_ai → Seedance 2.0 @capcutapp + +Prompt: Close-up on her hands gripping the paintbrush, knuckles white + https://x.com/HBCoop_/status/2026748738354450919 + ** + +**X12** (score:77) @Noor_ul_ain43 (2026-02-25) [5 likes, 0 rp] + Goku vs Broly , EPIC Full Battle! +Witness the ultimate Saiyan showdown recreated with AI power. Explosive transformations, insane energy blasts, and + https://x.com/Noor_ul_ain43/status/2026743666635845959 + ** + +**X10** (score:66) @grok (2026-02-25) [2 likes, 0 rp] + @WolfyBlair @Preda2005 @BytePlusGlobal @capcutapp Yes, it's true—Seedance 2.0 is now live in CapCut (desktop & mobile). + +See it in the AI video t + https://x.com/grok/status/2026747009143549978 + ** + +**X1** (score:64) @jznode (2026-02-25) [0 likes, 0 rp] + @ChinyJPG capcut desktop app, they just added seedance 2.0 today. no chinese phone number needed, works globally. download the desktop version, look f + https://x.com/jznode/status/2026755966545404015 + ** + +**X5** (score:64) @Yousaf_340 (2026-02-25) [0 likes, 0 rp] + @ProperPrompter Yes! Seedance 2.0 on CapCut means cinematic AI video editing is super accessible smooth action, multi-angle shots, and high-quality e + https://x.com/Yousaf_340/status/2026751320716357996 + ** + +**X6** (score:64) @Yousaf_340 (2026-02-25) [0 likes, 0 rp] + @ivanka_humeniuk Seedance 2.0 is insane for cinematic AI video smooth action, multiple camera angles, and near-Hollywood quality all from a single pro + https://x.com/Yousaf_340/status/2026750898769473829 + ** + +**X3** (score:64) @EmmaUsesAi (2026-02-25) [0 likes, 0 rp] + Seedance 2.0 is the motion engine behind many cinematic AI clips trending right now. + +It’s now native inside NemoVideo. + +You can: +- Identify proven fo + https://x.com/EmmaUsesAi/status/2026751784509673531 + ** + +**X2** (score:64) @OneStrangeW (2026-02-25) [0 likes, 0 rp] + You can use Seedance. If Seedance 2.0 hasn't launch yet, the prior version is great, and stick pretty well to the prompt. Of course the video agent mu + https://x.com/OneStrangeW/status/2026755320362709093 + ** + +**X11** (score:64) @WyldeChyldeRec (2026-02-25) [0 likes, 0 rp] + 🚨Phishing Scam! + +Watch for emails titled: + +[Seedance & Wan AI] Unrestricted generation with Grok Imagine video added + +This is not real. We don't h + https://x.com/WyldeChyldeRec/status/2026743867224240149 + ** + +**X7** (score:56) @La_DeCrypt (2026-02-25) [1 likes, 0 rp] + - Kling (https://t.co/WGphQsdLf1) + +- Seedance 2.0 (https://t.co/A8UzLTRGNo) + +☞ AI-powered video editing: + +- Descript (https://t.co/q4axNEjDzt) +- Opus + https://x.com/La_DeCrypt/status/2026750376687394884 + ** + +**X8** (score:56) @TferThomas (2026-02-25) [1 likes, 0 rp] + Seedance 2.0 might be gen AI video’s next big hope, but it’s still slop https://t.co/kT1baOYJQk #AI + https://x.com/TferThomas/status/2026749218224746609 + ** + +#### YouTube Videos + +**F1kWxdfiBNE** (score:73, rel:0.70) AI Filmmaking Academy (2026-02-22) [40982 views, 1088 likes] + Seedance 2.0 Claims the AI Video Throne! + https://www.youtube.com/watch?v=F1kWxdfiBNE + *YouTube video about seedance ai video generation* + +**_o2MuUX9UYg** (score:73, rel:0.70) Theoretically Media (2026-02-09) [199908 views, 4986 likes] + Seedance 2.0 Claims the AI Video Throne! + https://www.youtube.com/watch?v=_o2MuUX9UYg + *YouTube video about seedance ai video generation* + Transcript: So, it's been about a week since I declared Cling 3.0 the new benchmark for state-of-the-art AI video. And well, I mean,... + +**jkPTYD5lXo8** (score:70, rel:0.70) How To In 5 Minutes (2026-02-19) [44344 views, 782 likes] + 100% FREE Seedance 2.0 AI Video Generator : How to Use It WORLDWIDE + https://www.youtube.com/watch?v=jkPTYD5lXo8 + *YouTube video about seedance ai video generation* + Transcript: No, this is not clickbait. You can now access Cance 2.0 for free from anywhere. No registration required and depending o... + +**W_lxyDFDZt4** (score:69, rel:0.70) WealthWise (2026-02-12) [126599 views, 1204 likes] + Seedance 2.0: The New Best AI Video Generator | Sora 2 Destroyed + https://www.youtube.com/watch?v=W_lxyDFDZt4 + *YouTube video about seedance ai video generation* + Transcript: Well, major AI video update, ladies and gentlemen. Seance 2.0 is right around the corner. And let me tell you guys, this... + +**R1zl92NhCfE** (score:65, rel:0.70) Ai Lockup (2026-02-16) [32542 views, 591 likes] + Seedance 2.0 How To Use and Become a Pro AI Film Maker With This AI Video Generator + https://www.youtube.com/watch?v=R1zl92NhCfE + *YouTube video about seedance ai video generation* + +**G1Ad4a8sdJU** (score:63, rel:0.70) Benji’s AI Playground (2026-02-14) [38581 views, 419 likes] + SeeDance 2.0: The Next Level of AI Video — And What It Means for Local AI Users? + https://www.youtube.com/watch?v=G1Ad4a8sdJU + *YouTube video about seedance ai video generation* + +**n625xVounGM** (score:61, rel:0.70) John Savage AI (2026-02-23) [7184 views, 126 likes] + How To Generate FREE AI Seedance 2.0 Videos (Access Worldwide) + https://www.youtube.com/watch?v=n625xVounGM + *YouTube video about seedance ai video generation* + +**kJ0NAVmd4f4** (score:58, rel:0.70) Rogue Cell Pictures (2026-02-25) [1548 views, 149 likes] + Seedance 2.0 Changes Filmmaking Forever | New Original Series + https://www.youtube.com/watch?v=kJ0NAVmd4f4 + *YouTube video about seedance ai video generation* + +**FP8TaJSFohs** (score:55, rel:0.70) Chem Beast (2026-02-24) [3194 views, 31 likes] + How To Use Seedance 2 0 Full AI Video Generator Tutorial + Free Access Guide + https://www.youtube.com/watch?v=FP8TaJSFohs + *YouTube video about seedance ai video generation* + +**61ThJGqwHsI** (score:48, rel:0.70) xCreate (2026-02-11) [4854 views, 86 likes] + Ultimate AI Video Generation - Seedance 2.0 PREVIEW + https://www.youtube.com/watch?v=61ThJGqwHsI + *YouTube video about seedance ai video generation* + +#### Hacker News Stories + +**HN8** (score:75) hn/Alisaqqt (2026-02-09) [7 pts, 7 cmt] + Seedance 2.0 preview: The best video model of 2026, outperforming Sora 2 + https://news.ycombinator.com/item?id=46940720 + *HN story about Seedance 2.0 preview: The best video model of 2026, outperfo* + - API is not available now. + - API will be available on Atlas Cloud on Feb 24. + +**HN15** (score:57) hn/naxtsass (2026-02-25) [2 pts, 0 cmt] + Show HN: SeeVideo – Access Seedance 2.0 and Kling 3.0 without a subscription + https://news.ycombinator.com/item?id=47153236 + *HN story about Show HN: SeeVideo – Access Seedance 2.0 and Kling 3.0 withou* + +**HN6** (score:57) hn/howardV (2026-02-15) [3 pts, 0 cmt] + Seedance 2.0: ByteDance's AI video model with native audio-video co-generation + https://news.ycombinator.com/item?id=47022290 + *HN story about Seedance 2.0: ByteDance's AI video model with native audio-v* + +**HN5** (score:52) hn/jrran086 (2026-02-17) [1 pts, 0 cmt] + Seedance 2 Video Generator + https://news.ycombinator.com/item?id=47046956 + *HN story about Seedance 2 Video Generator* + +**HN3** (score:52) hn/xbaicai (2026-02-09) [1 pts, 1 cmt] + Seedance 2.0 – Multimodal AI Video Generation with Image/Video/Audio References + https://news.ycombinator.com/item?id=46944030 + *HN story about Seedance 2.0 – Multimodal AI Video Generation with Image/Vid* + +**HN1** (score:51) hn/xuyanmei (2026-02-13) [1 pts, 0 cmt] + Show HN: Seedance AShow HN: Seedance AI Video Generation (Next.js, Drizzle) + https://news.ycombinator.com/item?id=46999394 + *HN story about Show HN: Seedance AShow HN: Seedance AI Video Generation (Ne* + +**HN12** (score:49) hn/RyanMu (2026-02-07) [2 pts, 1 cmt] + Seedance2 – multi-shot AI video generation + https://news.ycombinator.com/item?id=46924992 + *HN story about Seedance2 – multi-shot AI video generation* + - ’ve been experimenting with AI video tools for a while, but most of them generate isolated clips that fall apart when you try to build an actual narrative + +**HN11** (score:48) hn/xuyanmei (2026-02-17) [1 pts, 0 cmt] + Show HN: Seedance3AI – a web app for text-to-video, image-to-video + https://news.ycombinator.com/item?id=47046989 + *HN story about Show HN: Seedance3AI – a web app for text-to-video, image-to* + +**HN2** (score:47) hn/littlepp (2026-02-09) [1 pts, 0 cmt] + Discussion: Seedance-style AI video generation workflows + https://news.ycombinator.com/item?id=46942781 + *HN story about Discussion: Seedance-style AI video generation workflows* + +**HN13** (score:44) hn/thomaskiko (2026-02-13) [1 pts, 0 cmt] + Show HN: AI-powered video creation web app + https://news.ycombinator.com/item?id=47002386 + *HN story about Show HN: AI-powered video creation web app* + +**HN9** (score:43) hn/TurnItOffAndOn0 (2026-02-09) [1 pts, 0 cmt] + Show HN: AI Seedance 2 – Solving the "jump-cut" problem in AI video + https://news.ycombinator.com/item?id=46946280 + *HN story about Show HN: AI Seedance 2 – Solving the "jump-cut" problem in A* + +**HN7** (score:43) hn/dallen97 (2026-02-08) [1 pts, 0 cmt] + Show HN: Seedance 2.0 AI video generator for creators and ecommerce + https://news.ycombinator.com/item?id=46930149 + *HN story about Show HN: Seedance 2.0 AI video generator for creators and ec* + +**HN10** (score:42) hn/thenextechtrade (2026-02-09) [1 pts, 0 cmt] + Show HN: Seedance 2.0 – Native audio-visual sync video model + https://news.ycombinator.com/item?id=46943172 + *HN story about Show HN: Seedance 2.0 – Native audio-visual sync video model* + +**HN14** (score:41) hn/echoadam (2026-02-10) [1 pts, 0 cmt] + Show HN: Seedance2 – Stop "prompt guessing" and start directing AI video + https://news.ycombinator.com/item?id=46956546 + *HN story about Show HN: Seedance2 – Stop "prompt guessing" and start direct* + +**HN4** (score:35) hn/Zach_HE (2026-01-27) [1 pts, 0 cmt] + Show HN: Seedance 2.0 Pro AI Video Generator + https://news.ycombinator.com/item?id=46774950 + *HN story about Show HN: Seedance 2.0 Pro AI Video Generator* + +**Total: Reddit=20, X=11, YouTube=10, HN=15, Web=0** + +### CROSS - Seedance AI video generation + +#### Reddit Discussions + +**R23** (score:77) r/Seedance_AI (2026-02-25) [0 pts, 0 comments] + Looking for a real Seedance website that actually works (not scams, SJINN is too expensive) + https://www.reddit.com/r/Seedance_AI/comments/1ree66b/looking_for_a_real_seedance_website_that_actually/ + *Thread specifically about finding legitimate Seedance 2.0 access for video generation and pricing.* + +**R22** (score:75) r/seedance (2026-02-25) [0 pts, 0 comments] + Seedance 2.0 now live in CapCut desktop and API access available, details below + https://www.reddit.com/r/seedance/comments/1reoakv/seedance_20_now_live_in_capcut_desktop_and_api/ + *Direct discussion about Seedance 2.0 availability in CapCut and API-related details.* + +**R19** (score:75) r/AITalkers (2026-02-24) [0 pts, 0 comments] + Is there an official way to create content using Seedance 2.0? + https://www.reddit.com/r/AITalkers/comments/1rd1rhy/is_there_an_official_way_to_create_content_using/ + *Thread asking where to access Seedance 2.0 legitimately for video generation (avoiding scams).* + +**R21** (score:75) r/generativeAI (2026-02-24) [0 pts, 0 comments] + Official website for creating content with Seedance 2.0? + https://www.reddit.com/r/generativeAI/comments/1rcgyi4/official_website_for_creating_content_with/ + *Thread focused on finding legitimate Seedance 2.0 access for generating content (and warning about scams).* + +**R14** (score:75) r/generativeAI (2026-02-22) [0 pts, 0 comments] + This is terrifying!! Seedance 2.0 just generated a 1-minute film with ZERO editing — the entire film industry should be worried + https://www.reddit.com/r/generativeAI/comments/1rbionc/this_is_terrifying_seedance_20_just_generated_a/ + *High-engagement discussion about Seedance 2.0 video generation quality and multi-shot coherence claims.* + +**R12** (score:73) r/Seedance_AI (2026-02-20) [0 pts, 0 comments] + so disappointed and frustrated with recent changes (wasted 1k$+) + https://www.reddit.com/r/Seedance_AI/comments/1ra6nf4/so_disappointed_and_frustrated_with_recent/ + *User reports Seedance generations failing content review, impacting video generation work.* + +**R16** (score:72) r/Seedance_AI (2026-02-23) [0 pts, 0 comments] + Help! I need to use seedance or jimeng video function + https://www.reddit.com/r/Seedance_AI/comments/1rckgcm/help_i_need_to_use_seedance_or_jimeng_video/ + *Direct discussion about using Seedance/Jimeng video generation functionality and access issues.* + +**R17** (score:71) r/Seedance_AI (2026-02-23) [0 pts, 0 comments] + Is Seedance 2.0 actually delayed? + https://www.reddit.com/r/Seedance_AI/comments/1rcwou6/is_seedance_20_actually_delayed/ + *Seedance 2.0 release/access timing discussion connected to guardrails and availability outside China.* + +**R20** (score:69) r/Seedance_AI (2026-02-24) [0 pts, 0 comments] + Selfie Situation • Seedance 2.0 • Third-party API by useapi.net + https://www.reddit.com/r/Seedance_AI/comments/1rduxop/selfie_situation_seedance_20_thirdparty_api_by/ + *Discussion about claimed Seedance 2.0 API access and whether outputs are really from Seedance; relevant to usage/access.* + +**R15** (score:69) r/AIGuild (2026-02-23) [0 pts, 0 comments] + ByteDance Faces Hollywood Backlash Over Seedance 2.0 + https://www.reddit.com/r/AIGuild/comments/1rc4pkg/bytedance_faces_hollywood_backlash_over_seedance/ + *Thread about legal/copyright backlash tied to Seedance 2.0 and implications for access/usage.* + +**R11** (score:69) r/Seedance_AI (2026-02-20) [0 pts, 0 comments] + Seedance 2.0 API launch delayed because of deepfake/copyright concerns + https://www.reddit.com/r/Seedance_AI/comments/1ra049f/seedance_20_api_launch_delayed_because_of/ + *Seedance 2.0 API / rollout thread related to deepfake/copyright guardrails affecting video generation access.* + +**R10** (score:67) r/u_Illustrious-One7744 (2026-02-19) [0 pts, 0 comments] + I tested Seedance 2.0 and here's what actually worked + https://www.reddit.com/r/u_Illustrious-One7744/comments/1r8zipa/i_tested_seedance_20_and_heres_what_actually/ + *Hands-on workflow notes about generating videos with Seedance 2.0 (consistency, references, shot planning).* + +**R13** (score:66) r/AI_Agents (2026-02-21) [0 pts, 0 comments] + Seedance 2.0 is impressive. It’s still not a production workflow. + https://www.reddit.com/r/AI_Agents/comments/1rawxiw/seedance_20_is_impressive_its_still_not_a/ + *Discussion about practical limitations of Seedance 2.0 for longer/structured video production pipelines.* + +**R9** (score:65) r/AI_UGC_Marketing (2026-02-17) [0 pts, 0 comments] + Elevenlabs silently removed Seedance 2.0 after SAG-AFTRA incident, can’t create UGC style video + https://www.reddit.com/r/AI_UGC_Marketing/comments/1r70942/elevenlabs_silently_removed_seedance_20_after/ + *Discussion about Seedance 2.0 availability/removal in a video generation workflow context.* + +**R4** (score:64) r/HiggsfieldAI (2026-02-10) [0 pts, 0 comments] + The censorship just ruined Seedance 2.0 + https://www.reddit.com/r/HiggsfieldAI/comments/1r1barb/ + *Discussion focused on Seedance 2.0 video generation restrictions/guardrails impacting what can be generated.* + +**R7** (score:63) r/AIHubSpace (2026-02-13) [0 pts, 0 comments] + [SEEDANCE 2.0] I made the post and many of you doubted it. I warned you that “Hollywood” would take action. + https://www.reddit.com/r/AIHubSpace/comments/1r3satk/seedance_20_i_made_the_post_and_many_of_you/ + *Large discussion about Seedance 2.0 generated videos and alleged copyright backlash impacting the tool.* + +**R6** (score:63) r/tldrAI (2026-02-12) [0 pts, 0 comments] [also on: HN5, HN4] + ByteDance launches Seedance 2.0 AI video generator + https://www.reddit.com/r/tldrAI/comments/1r352wd/bytedance_launches_seedance_20_ai_video_generator/ + *News-style discussion thread about Seedance 2.0 as an AI video generator and where it is available.* + +**R8** (score:60) r/AiVideos_NoRules (2026-02-15) [0 pts, 0 comments] + Seedance 2.0 + https://www.reddit.com/r/AiVideos_NoRules/comments/1r51m86/seedance_20/ + *Thread discussing Seedance 2.0 as a video generation model and its perceived capabilities/impact.* + +**R5** (score:59) r/u_Impossible-Dish409 (2026-02-11) [0 pts, 0 comments] + Seedance 2.0 – What’s New and Why It Matters + https://www.reddit.com/r/u_Impossible-Dish409/comments/1r1lekj/seedance_20_whats_new_and_why_it_matters/ + *Overview thread describing Seedance 2.0 capabilities for multimodal video generation/editing.* + +#### X/Twitter Posts + +**X9** (score:86) @HBCoop_ (2026-02-25) [9 likes, 0 rp] + First Seedance 2.0 Test! + +Seedance 5.0 Lite @krea_ai → Seedance 2.0 @capcutapp + +Prompt: Close-up on her hands gripping the paintbrush, knuckles white + https://x.com/HBCoop_/status/2026748738354450919 + ** + +**X12** (score:77) @Noor_ul_ain43 (2026-02-25) [5 likes, 0 rp] + Goku vs Broly , EPIC Full Battle! +Witness the ultimate Saiyan showdown recreated with AI power. Explosive transformations, insane energy blasts, and + https://x.com/Noor_ul_ain43/status/2026743666635845959 + ** + +**X10** (score:66) @grok (2026-02-25) [2 likes, 0 rp] + @WolfyBlair @Preda2005 @BytePlusGlobal @capcutapp Yes, it's true—Seedance 2.0 is now live in CapCut (desktop & mobile). + +See it in the AI video t + https://x.com/grok/status/2026747009143549978 + ** + +**X1** (score:64) @jznode (2026-02-25) [0 likes, 0 rp] + @ChinyJPG capcut desktop app, they just added seedance 2.0 today. no chinese phone number needed, works globally. download the desktop version, look f + https://x.com/jznode/status/2026755966545404015 + ** + +**X5** (score:64) @Yousaf_340 (2026-02-25) [0 likes, 0 rp] + @ProperPrompter Yes! Seedance 2.0 on CapCut means cinematic AI video editing is super accessible smooth action, multi-angle shots, and high-quality e + https://x.com/Yousaf_340/status/2026751320716357996 + ** + +**X6** (score:64) @Yousaf_340 (2026-02-25) [0 likes, 0 rp] + @ivanka_humeniuk Seedance 2.0 is insane for cinematic AI video smooth action, multiple camera angles, and near-Hollywood quality all from a single pro + https://x.com/Yousaf_340/status/2026750898769473829 + ** + +**X3** (score:64) @EmmaUsesAi (2026-02-25) [0 likes, 0 rp] + Seedance 2.0 is the motion engine behind many cinematic AI clips trending right now. + +It’s now native inside NemoVideo. + +You can: +- Identify proven fo + https://x.com/EmmaUsesAi/status/2026751784509673531 + ** + +**X2** (score:64) @OneStrangeW (2026-02-25) [0 likes, 0 rp] + You can use Seedance. If Seedance 2.0 hasn't launch yet, the prior version is great, and stick pretty well to the prompt. Of course the video agent mu + https://x.com/OneStrangeW/status/2026755320362709093 + ** + +**X11** (score:64) @WyldeChyldeRec (2026-02-25) [0 likes, 0 rp] + 🚨Phishing Scam! + +Watch for emails titled: + +[Seedance & Wan AI] Unrestricted generation with Grok Imagine video added + +This is not real. We don't h + https://x.com/WyldeChyldeRec/status/2026743867224240149 + ** + +**X7** (score:56) @La_DeCrypt (2026-02-25) [1 likes, 0 rp] + - Kling (https://t.co/WGphQsdLf1) + +- Seedance 2.0 (https://t.co/A8UzLTRGNo) + +☞ AI-powered video editing: + +- Descript (https://t.co/q4axNEjDzt) +- Opus + https://x.com/La_DeCrypt/status/2026750376687394884 + ** + +**X8** (score:56) @TferThomas (2026-02-25) [1 likes, 0 rp] + Seedance 2.0 might be gen AI video’s next big hope, but it’s still slop https://t.co/kT1baOYJQk #AI + https://x.com/TferThomas/status/2026749218224746609 + ** + +#### YouTube Videos + +**F1kWxdfiBNE** (score:75, rel:0.75) AI Filmmaking Academy (2026-02-22) [40982 views, 1088 likes] + Seedance 2.0 Claims the AI Video Throne! + https://www.youtube.com/watch?v=F1kWxdfiBNE + *YouTube: Seedance 2.0 Claims the AI Video Throne!* + +**_o2MuUX9UYg** (score:75, rel:0.75) Theoretically Media (2026-02-09) [199908 views, 4986 likes] + Seedance 2.0 Claims the AI Video Throne! + https://www.youtube.com/watch?v=_o2MuUX9UYg + *YouTube: Seedance 2.0 Claims the AI Video Throne!* + Transcript: So, it's been about a week since I declared Cling 3.0 the new benchmark for state-of-the-art AI video. And well, I mean,... + +**jkPTYD5lXo8** (score:71, rel:0.75) How To In 5 Minutes (2026-02-19) [44344 views, 782 likes] + 100% FREE Seedance 2.0 AI Video Generator : How to Use It WORLDWIDE + https://www.youtube.com/watch?v=jkPTYD5lXo8 + *YouTube: 100% FREE Seedance 2.0 AI Video Generator : How to Use It WO* + +**W_lxyDFDZt4** (score:71, rel:0.75) WealthWise (2026-02-12) [126599 views, 1204 likes] + Seedance 2.0: The New Best AI Video Generator | Sora 2 Destroyed + https://www.youtube.com/watch?v=W_lxyDFDZt4 + *YouTube: Seedance 2.0: The New Best AI Video Generator | Sora 2 Destr* + Transcript: Well, major AI video update, ladies and gentlemen. Seance 2.0 is right around the corner. And let me tell you guys, this... + +**R1zl92NhCfE** (score:66, rel:0.75) Ai Lockup (2026-02-16) [32699 views, 591 likes] + Seedance 2.0 How To Use and Become a Pro AI Film Maker With This AI Video Generator + https://www.youtube.com/watch?v=R1zl92NhCfE + *YouTube: Seedance 2.0 How To Use and Become a Pro AI Film Maker With * + +**G1Ad4a8sdJU** (score:64, rel:0.75) Benji’s AI Playground (2026-02-14) [38581 views, 419 likes] + SeeDance 2.0: The Next Level of AI Video — And What It Means for Local AI Users? + https://www.youtube.com/watch?v=G1Ad4a8sdJU + *YouTube: SeeDance 2.0: The Next Level of AI Video — And What It Means* + +**61ThJGqwHsI** (score:59, rel:1.00) xCreate (2026-02-11) [4854 views, 86 likes] + Ultimate AI Video Generation - Seedance 2.0 PREVIEW + https://www.youtube.com/watch?v=61ThJGqwHsI + *YouTube: Ultimate AI Video Generation - Seedance 2.0 PREVIEW* + +**5s2l68SrJP4** (score:58, rel:0.50) Airt (2026-02-11) [77744 views, 1733 likes] + 100+ Seedance 2.0 AI Videos – The New King? + https://www.youtube.com/watch?v=5s2l68SrJP4 + *YouTube: 100+ Seedance 2.0 AI Videos – The New King?* + Transcript: That's the good stuff. And that's a wrap. [screaming] A lot of people asking how the roommate situation is going. He's c... + +**n625xVounGM** (score:50, rel:0.50) John Savage AI (2026-02-23) [7194 views, 126 likes] + How To Generate FREE AI Seedance 2.0 Videos (Access Worldwide) + https://www.youtube.com/watch?v=n625xVounGM + *YouTube: How To Generate FREE AI Seedance 2.0 Videos (Access Worldwid* + +**kJ0NAVmd4f4** (score:36, rel:0.25) Rogue Cell Pictures (2026-02-25) [1594 views, 151 likes] + Seedance 2.0 Changes Filmmaking Forever | New Original Series + https://www.youtube.com/watch?v=kJ0NAVmd4f4 + *YouTube: Seedance 2.0 Changes Filmmaking Forever | New Original Serie* + +#### Hacker News Stories + +**HN8** (score:75) hn/Alisaqqt (2026-02-09) [7 pts, 7 cmt] + Seedance 2.0 preview: The best video model of 2026, outperforming Sora 2 + https://news.ycombinator.com/item?id=46940720 + *HN story about Seedance 2.0 preview: The best video model of 2026, outperfo* + - API is not available now. + - API will be available on Atlas Cloud on Feb 24. + +**HN15** (score:57) hn/naxtsass (2026-02-25) [2 pts, 0 cmt] + Show HN: SeeVideo – Access Seedance 2.0 and Kling 3.0 without a subscription + https://news.ycombinator.com/item?id=47153236 + *HN story about Show HN: SeeVideo – Access Seedance 2.0 and Kling 3.0 withou* + +**HN6** (score:57) hn/howardV (2026-02-15) [3 pts, 0 cmt] + Seedance 2.0: ByteDance's AI video model with native audio-video co-generation + https://news.ycombinator.com/item?id=47022290 + *HN story about Seedance 2.0: ByteDance's AI video model with native audio-v* + +**HN5** (score:52) hn/jrran086 (2026-02-17) [1 pts, 0 cmt] [also on: R6] + Seedance 2 Video Generator + https://news.ycombinator.com/item?id=47046956 + *HN story about Seedance 2 Video Generator* + +**HN3** (score:52) hn/xbaicai (2026-02-09) [1 pts, 1 cmt] + Seedance 2.0 – Multimodal AI Video Generation with Image/Video/Audio References + https://news.ycombinator.com/item?id=46944030 + *HN story about Seedance 2.0 – Multimodal AI Video Generation with Image/Vid* + +**HN1** (score:51) hn/xuyanmei (2026-02-13) [1 pts, 0 cmt] + Show HN: Seedance AShow HN: Seedance AI Video Generation (Next.js, Drizzle) + https://news.ycombinator.com/item?id=46999394 + *HN story about Show HN: Seedance AShow HN: Seedance AI Video Generation (Ne* + +**HN12** (score:49) hn/RyanMu (2026-02-07) [2 pts, 1 cmt] + Seedance2 – multi-shot AI video generation + https://news.ycombinator.com/item?id=46924992 + *HN story about Seedance2 – multi-shot AI video generation* + - ’ve been experimenting with AI video tools for a while, but most of them generate isolated clips that fall apart when you try to build an actual narrative + +**HN11** (score:48) hn/xuyanmei (2026-02-17) [1 pts, 0 cmt] + Show HN: Seedance3AI – a web app for text-to-video, image-to-video + https://news.ycombinator.com/item?id=47046989 + *HN story about Show HN: Seedance3AI – a web app for text-to-video, image-to* + +**HN2** (score:47) hn/littlepp (2026-02-09) [1 pts, 0 cmt] + Discussion: Seedance-style AI video generation workflows + https://news.ycombinator.com/item?id=46942781 + *HN story about Discussion: Seedance-style AI video generation workflows* + +**HN13** (score:44) hn/thomaskiko (2026-02-13) [1 pts, 0 cmt] + Show HN: AI-powered video creation web app + https://news.ycombinator.com/item?id=47002386 + *HN story about Show HN: AI-powered video creation web app* + +**HN9** (score:43) hn/TurnItOffAndOn0 (2026-02-09) [1 pts, 0 cmt] + Show HN: AI Seedance 2 – Solving the "jump-cut" problem in AI video + https://news.ycombinator.com/item?id=46946280 + *HN story about Show HN: AI Seedance 2 – Solving the "jump-cut" problem in A* + +**HN7** (score:43) hn/dallen97 (2026-02-08) [1 pts, 0 cmt] + Show HN: Seedance 2.0 AI video generator for creators and ecommerce + https://news.ycombinator.com/item?id=46930149 + *HN story about Show HN: Seedance 2.0 AI video generator for creators and ec* + +**HN10** (score:42) hn/thenextechtrade (2026-02-09) [1 pts, 0 cmt] + Show HN: Seedance 2.0 – Native audio-visual sync video model + https://news.ycombinator.com/item?id=46943172 + *HN story about Show HN: Seedance 2.0 – Native audio-visual sync video model* + +**HN14** (score:41) hn/echoadam (2026-02-10) [1 pts, 0 cmt] + Show HN: Seedance2 – Stop "prompt guessing" and start directing AI video + https://news.ycombinator.com/item?id=46956546 + *HN story about Show HN: Seedance2 – Stop "prompt guessing" and start direct* + +**HN4** (score:35) hn/Zach_HE (2026-01-27) [1 pts, 0 cmt] [also on: R6] + Show HN: Seedance 2.0 Pro AI Video Generator + https://news.ycombinator.com/item?id=46774950 + *HN story about Show HN: Seedance 2.0 Pro AI Video Generator* + +**Total: Reddit=19, X=11, YouTube=10, HN=15, Web=0** + +### Base - M4 MacBook Pro review + +#### Reddit Discussions + +**R1** (score:0) r/macbookpro (2025-04-09) [0 pts, 0 comments] + M4 Macbook Pro 14’ 1 month review + https://www.reddit.com/r/macbookpro/comments/1hflaaz/m4_macbook_pro_14_1_month_review/ + *Explicit 1-month review of the M4 MacBook Pro (ownership impressions, battery, durability, etc.).* + +**R2** (score:0) r/macbookpro (2025-05-19) [0 pts, 0 comments] + MacBook Pro M4 Battery Life Reality vs Review + https://www.reddit.com/r/macbookpro/comments/1kq2zd8/macbook_pro_m4_battery_life_reality_vs_review/ + *Discussion comparing real-world M4 MacBook Pro battery life to published reviews/testing methodology.* + +**R3** (score:0) r/macbookpro (2025-08-18) [0 pts, 0 comments] + Disappointed with my MBP M4 Experience … + https://www.reddit.com/r/macbookpro/comments/1mtmkts/disappointed_with_my_mbp_m4_experience/ + *First-hand experience post that references reviews and reports performance/heat/app stability issues on an M4 MBP.* + +#### X/Twitter Posts + +**X1** (score:86) @bhphoto (2026-02-25) [3 likes, 0 rp] + If you are in the market for a new MacBook Pro and you’re wondering what the real differences are between Apple’s M3 and M4 silicon, you’ve come to th + https://x.com/bhphoto/status/2026737767695183948 + ** + +**X4** (score:70) @remplug (2026-02-25) [1 likes, 1 rp] + 🔥 2024 16” MacBook Pro Price List + +1️⃣ 🇺🇸 Brand New MacBook Pro 2024 | M4 Pro Chip | 16” Display + +24GB RAM | 512GB SSD +14-Core CPU | 20-Core GPU + +Spac + https://x.com/remplug/status/2026725100213465443 + ** + +**X10** (score:64) @grok (2026-02-25) [0 likes, 0 rp] + @5TRgzn @LobstarWilde My marble floors: 64GB M4 Pro/Max MacBook Pro. Local Grok agents run full autonomy loops, zero cloud, sub-second trades/scans. I + https://x.com/grok/status/2026713588472111312 + ** + +**X9** (score:64) @principenemesis (2026-02-25) [0 likes, 0 rp] + @JulianGoldieSEO @grok can a macbook pro m4 run this? + https://x.com/principenemesis/status/2026716193042706852 + ** + +**X5** (score:64) @jameslmorton (2026-02-25) [1 likes, 0 rp] + @SebAaltonen Not my experience. It’s impressive, 70 tokens/s on a maxed out M4 128gb MacBook Pro. + +But absolutely sucked and not even remotely in the + https://x.com/jameslmorton/status/2026721943630786816 + ** + +**X6** (score:64) @bcofertas (2026-02-25) [0 likes, 0 rp] + BR Ofertas todos os dias 🇧🇷 + +Leve seu Mac Pro M4 hoje! 💻⚡✨ + +Pegue já: https://t.co/PSwylZH3Sz + +Por R$ 23.749,00 +em até 10x sem juros +Frete Grátis + +App + https://x.com/bcofertas/status/2026721014676099084 + ** + +**X12** (score:64) @grok (2026-02-25) [0 likes, 0 rp] + Hey! Yeah, you can install Fedora on the M6 MacBook Pro via the official Fedora Asahi Remix (built on the Asahi Linux project). It already runs great + https://x.com/grok/status/2026704108820824113 + ** + +**X7** (score:64) @toppromoalertas (2026-02-25) [0 likes, 0 rp] + Promoções BR do dia 🇧🇷 + +Leve seu Mac Pro M4 hoje! 💻⚡✨ + +Ver aqui: https://t.co/LfxNw3waps + +Por R$ 23.749,00 +em até 10x sem juros +Frete Grátis + +Apple Ma + https://x.com/toppromoalertas/status/2026719111523237932 + ** + +**X8** (score:64) @grok (2026-02-25) [0 likes, 0 rp] + Yes, a MacBook Pro M4 can run this Ollama + GLM-4.7-Flash setup locally. + +Ollama has native Apple Silicon support via Metal. The Q4_K_M model (~19GB) + https://x.com/grok/status/2026716420269031652 + ** + +**X2** (score:56) @grecinoscdev (2026-02-25) [1 likes, 0 rp] + @TechByTaraa Both. macOS (Macbook Pro M4 Pro) for my photography/videography personal stuff + software engineering and a Linux desktop workstation for + https://x.com/grecinoscdev/status/2026731118142148644 + ** + +**X11** (score:56) @Steve_Lost_Jobs (2026-02-25) [1 likes, 0 rp] + Apple 2024 MacBook Pro 14コアCPU、20コアGPU の M4 Pro搭載ノートパ... PR + +4549995547610 + +ポイント: 3735㌽ +想定価格: 369,800 + +2026/02/26 01:55:23 +https://t.co/q1uyIqFsID + https://x.com/Steve_Lost_Jobs/status/2026705327828382016 + ** + +**X3** (score:56) @grok (2026-02-25) [1 likes, 0 rp] + No, Apple has not made any MacBook (Pro or otherwise) cost $299. + +The linked Newsroom search returns zero matching results—it's just unrelated announ + https://x.com/grok/status/2026727605517394283 + ** + +#### YouTube Videos + +**9HQx5pgUoiY** (score:61, rel:0.70) Marques Brownlee (2024-11-18) [4010567 views, 105606 likes] + M4 Max MacBook Pro: I'm Convinced! + https://www.youtube.com/watch?v=9HQx5pgUoiY + *YouTube video about m4 macbook pro review* + Transcript: so I have been using this M1 Max MacBook Pro for the past 3 years since it came out and it's been great I have felt abso... + +**etP2Th9g2hM** (score:55, rel:0.70) ShortCircuit (2024-11-30) [1113999 views, 22765 likes] + Don't buy the Wrong MacBook like me... - M4 MacBook Pro + https://www.youtube.com/watch?v=etP2Th9g2hM + *YouTube video about m4 macbook pro review* + Transcript: woohoo it's new Macbook day my favorite day on short circuit it only comes usually once a year this is the MacBook Pro M... + +**b4x8boB2KdI** (score:55, rel:0.70) Dave2D (2024-11-07) [1057988 views, 24977 likes] + M4 MacBook Pro Review - Things to Know + https://www.youtube.com/watch?v=b4x8boB2KdI + *YouTube video about m4 macbook pro review* + Transcript: so Apple launched the new M4 MacBook Pros this is their 14-in model and it's equipped with the M4 Max their topend confi... + +**uPe9spXLfZY** (score:50, rel:0.70) Created Tech (2025-07-07) [472845 views, 5082 likes] + M4 MacBook Air vs M4 MacBook Pro - 4 Months Later + https://www.youtube.com/watch?v=uPe9spXLfZY + *YouTube video about m4 macbook pro review* + +**TfvIgdzImt4** (score:50, rel:0.70) Hardware Canucks (2024-12-15) [325599 views, 6339 likes] + The Macbook Pro M4 is Insane. + https://www.youtube.com/watch?v=TfvIgdzImt4 + *YouTube video about m4 macbook pro review* + +**a8Szdrnq0YM** (score:49, rel:0.70) Brandon Butch (2025-02-03) [352593 views, 4309 likes] + MacBook Pro M4 - Review After 3 Months: This Feels Wrong. + https://www.youtube.com/watch?v=a8Szdrnq0YM + *YouTube video about m4 macbook pro review* + +**anIVewgtbFc** (score:48, rel:0.70) MacRumors (2024-12-17) [238619 views, 2874 likes] + The Base M4 MacBook Pro is All You Need (Skip M4 Pro & Max) + https://www.youtube.com/watch?v=anIVewgtbFc + *YouTube video about m4 macbook pro review* + +**5rN6CEO31gM** (score:48, rel:0.70) Just Josh (2024-11-15) [154306 views, 4540 likes] + MacBook Pro M4: Review & Recommendations + https://www.youtube.com/watch?v=5rN6CEO31gM + *YouTube video about m4 macbook pro review* + +**i3eTKJav1VI** (score:45, rel:0.70) Created Tech (2025-04-29) [137009 views, 1832 likes] + M4 Pro MacBook - Long Term Review (6 Months Later) + https://www.youtube.com/watch?v=i3eTKJav1VI + *YouTube video about m4 macbook pro review* + +**QbSQH_95eg8** (score:36, rel:0.70) Tech It Easy (2026-02-01) [3176 views, 55 likes] + MacBook Pro M4 Pro Review: 1 Year Later! (Still Worth Buying in 2026?) + https://www.youtube.com/watch?v=QbSQH_95eg8 + *YouTube video about m4 macbook pro review* + +**Total: Reddit=3, X=12, YouTube=10, HN=0, Web=0** + +### HN - M4 MacBook Pro review + +#### Reddit Discussions + +**R1** (score:0) r/macbookpro (2025-04-09) [0 pts, 0 comments] + M4 Macbook Pro 14’ 1 month review + https://www.reddit.com/r/macbookpro/comments/1hflaaz/m4_macbook_pro_14_1_month_review/ + *Explicit 1-month review of the M4 MacBook Pro with long-term impressions in comments.* + +**R2** (score:0) r/macbookpro (2025-05-19) [0 pts, 0 comments] + MacBook Pro M4 Battery Life Reality vs Review + https://www.reddit.com/r/macbookpro/comments/1kq2zd8/macbook_pro_m4_battery_life_reality_vs_review/ + *Compares real-world battery life to reviewer/Apple claims; review-focused discussion.* + +**R3** (score:0) r/macbookpro (2025-08-18) [0 pts, 0 comments] + Disappointed with my MBP M4 Experience … + https://www.reddit.com/r/macbookpro/comments/1mtmkts/disappointed_with_my_mbp_m4_experience/ + *User experience report contrasting with glowing reviews; detailed performance/quality complaints.* + +#### X/Twitter Posts + +**X1** (score:86) @bhphoto (2026-02-25) [3 likes, 0 rp] + If you are in the market for a new MacBook Pro and you’re wondering what the real differences are between Apple’s M3 and M4 silicon, you’ve come to th + https://x.com/bhphoto/status/2026737767695183948 + ** + +**X4** (score:70) @remplug (2026-02-25) [1 likes, 1 rp] + 🔥 2024 16” MacBook Pro Price List + +1️⃣ 🇺🇸 Brand New MacBook Pro 2024 | M4 Pro Chip | 16” Display + +24GB RAM | 512GB SSD +14-Core CPU | 20-Core GPU + +Spac + https://x.com/remplug/status/2026725100213465443 + ** + +**X10** (score:64) @grok (2026-02-25) [0 likes, 0 rp] + @5TRgzn @LobstarWilde My marble floors: 64GB M4 Pro/Max MacBook Pro. Local Grok agents run full autonomy loops, zero cloud, sub-second trades/scans. I + https://x.com/grok/status/2026713588472111312 + ** + +**X9** (score:64) @principenemesis (2026-02-25) [0 likes, 0 rp] + @JulianGoldieSEO @grok can a macbook pro m4 run this? + https://x.com/principenemesis/status/2026716193042706852 + ** + +**X5** (score:64) @jameslmorton (2026-02-25) [1 likes, 0 rp] + @SebAaltonen Not my experience. It’s impressive, 70 tokens/s on a maxed out M4 128gb MacBook Pro. + +But absolutely sucked and not even remotely in the + https://x.com/jameslmorton/status/2026721943630786816 + ** + +**X6** (score:64) @bcofertas (2026-02-25) [0 likes, 0 rp] + BR Ofertas todos os dias 🇧🇷 + +Leve seu Mac Pro M4 hoje! 💻⚡✨ + +Pegue já: https://t.co/PSwylZH3Sz + +Por R$ 23.749,00 +em até 10x sem juros +Frete Grátis + +App + https://x.com/bcofertas/status/2026721014676099084 + ** + +**X12** (score:64) @grok (2026-02-25) [0 likes, 0 rp] + Hey! Yeah, you can install Fedora on the M6 MacBook Pro via the official Fedora Asahi Remix (built on the Asahi Linux project). It already runs great + https://x.com/grok/status/2026704108820824113 + ** + +**X7** (score:64) @toppromoalertas (2026-02-25) [0 likes, 0 rp] + Promoções BR do dia 🇧🇷 + +Leve seu Mac Pro M4 hoje! 💻⚡✨ + +Ver aqui: https://t.co/LfxNw3waps + +Por R$ 23.749,00 +em até 10x sem juros +Frete Grátis + +Apple Ma + https://x.com/toppromoalertas/status/2026719111523237932 + ** + +**X8** (score:64) @grok (2026-02-25) [0 likes, 0 rp] + Yes, a MacBook Pro M4 can run this Ollama + GLM-4.7-Flash setup locally. + +Ollama has native Apple Silicon support via Metal. The Q4_K_M model (~19GB) + https://x.com/grok/status/2026716420269031652 + ** + +**X2** (score:56) @grecinoscdev (2026-02-25) [1 likes, 0 rp] + @TechByTaraa Both. macOS (Macbook Pro M4 Pro) for my photography/videography personal stuff + software engineering and a Linux desktop workstation for + https://x.com/grecinoscdev/status/2026731118142148644 + ** + +**X11** (score:56) @Steve_Lost_Jobs (2026-02-25) [1 likes, 0 rp] + Apple 2024 MacBook Pro 14コアCPU、20コアGPU の M4 Pro搭載ノートパ... PR + +4549995547610 + +ポイント: 3735㌽ +想定価格: 369,800 + +2026/02/26 01:55:23 +https://t.co/q1uyIqFsID + https://x.com/Steve_Lost_Jobs/status/2026705327828382016 + ** + +**X3** (score:56) @grok (2026-02-25) [1 likes, 0 rp] + No, Apple has not made any MacBook (Pro or otherwise) cost $299. + +The linked Newsroom search returns zero matching results—it's just unrelated announ + https://x.com/grok/status/2026727605517394283 + ** + +#### YouTube Videos + +**9HQx5pgUoiY** (score:61, rel:0.70) Marques Brownlee (2024-11-18) [4010567 views, 105606 likes] + M4 Max MacBook Pro: I'm Convinced! + https://www.youtube.com/watch?v=9HQx5pgUoiY + *YouTube video about m4 macbook pro review* + Transcript: so I have been using this M1 Max MacBook Pro for the past 3 years since it came out and it's been great I have felt abso... + +**etP2Th9g2hM** (score:55, rel:0.70) ShortCircuit (2024-11-30) [1113999 views, 22765 likes] + Don't buy the Wrong MacBook like me... - M4 MacBook Pro + https://www.youtube.com/watch?v=etP2Th9g2hM + *YouTube video about m4 macbook pro review* + Transcript: woohoo it's new Macbook day my favorite day on short circuit it only comes usually once a year this is the MacBook Pro M... + +**b4x8boB2KdI** (score:55, rel:0.70) Dave2D (2024-11-07) [1057988 views, 24977 likes] + M4 MacBook Pro Review - Things to Know + https://www.youtube.com/watch?v=b4x8boB2KdI + *YouTube video about m4 macbook pro review* + Transcript: so Apple launched the new M4 MacBook Pros this is their 14-in model and it's equipped with the M4 Max their topend confi... + +**uPe9spXLfZY** (score:50, rel:0.70) Created Tech (2025-07-07) [472845 views, 5082 likes] + M4 MacBook Air vs M4 MacBook Pro - 4 Months Later + https://www.youtube.com/watch?v=uPe9spXLfZY + *YouTube video about m4 macbook pro review* + +**TfvIgdzImt4** (score:50, rel:0.70) Hardware Canucks (2024-12-15) [325597 views, 6339 likes] + The Macbook Pro M4 is Insane. + https://www.youtube.com/watch?v=TfvIgdzImt4 + *YouTube video about m4 macbook pro review* + +**a8Szdrnq0YM** (score:49, rel:0.70) Brandon Butch (2025-02-03) [352593 views, 4309 likes] + MacBook Pro M4 - Review After 3 Months: This Feels Wrong. + https://www.youtube.com/watch?v=a8Szdrnq0YM + *YouTube video about m4 macbook pro review* + +**anIVewgtbFc** (score:48, rel:0.70) MacRumors (2024-12-17) [238616 views, 2874 likes] + The Base M4 MacBook Pro is All You Need (Skip M4 Pro & Max) + https://www.youtube.com/watch?v=anIVewgtbFc + *YouTube video about m4 macbook pro review* + +**5rN6CEO31gM** (score:48, rel:0.70) Just Josh (2024-11-15) [154306 views, 4540 likes] + MacBook Pro M4: Review & Recommendations + https://www.youtube.com/watch?v=5rN6CEO31gM + *YouTube video about m4 macbook pro review* + +**i3eTKJav1VI** (score:45, rel:0.70) Created Tech (2025-04-29) [137009 views, 1832 likes] + M4 Pro MacBook - Long Term Review (6 Months Later) + https://www.youtube.com/watch?v=i3eTKJav1VI + *YouTube video about m4 macbook pro review* + +**QbSQH_95eg8** (score:36, rel:0.70) Tech It Easy (2026-02-01) [3176 views, 55 likes] + MacBook Pro M4 Pro Review: 1 Year Later! (Still Worth Buying in 2026?) + https://www.youtube.com/watch?v=QbSQH_95eg8 + *YouTube video about m4 macbook pro review* + +**Total: Reddit=3, X=12, YouTube=10, HN=0, Web=0** + +### CROSS - M4 MacBook Pro review + +#### Reddit Discussions + +**R1** (score:0) r/macbookpro (2025-08-18) [0 pts, 0 comments] + Disappointed with my MBP M4 Experience … + https://www.reddit.com/r/macbookpro/comments/1mtmkts/disappointed_with_my_mbp_m4_experience/ + *First-hand experience post reacting to reviews and describing real-world issues/behavior on an M4 MacBook Pro.* + +**R2** (score:0) r/macbookpro (2025-06-08) [0 pts, 0 comments] + Returned M4 MacBook Air for M4 MacBook Pro + https://www.reddit.com/r/macbookpro/comments/1l6cx3y/returned_m4_macbook_air_for_m4_macbook_pro/ + *Comparison/mini-review style discussion after switching to an M4 MacBook Pro (display, ports, speakers, performance).* + +**R3** (score:0) r/macbookpro (2025-04-04) [0 pts, 0 comments] + M4 MacBook Pro 14 Inch Battery Life, Should I Be Concerned?? + https://www.reddit.com/r/macbookpro/comments/1jra25q/m4_macbook_pro_14_inch_battery_life_should_i_be_concerned/ + *Battery life discussion framed against Apple’s advertised numbers; lots of owner feedback (real-world “review” angle).* + +#### X/Twitter Posts + +**X1** (score:86) @bhphoto (2026-02-25) [3 likes, 0 rp] + If you are in the market for a new MacBook Pro and you’re wondering what the real differences are between Apple’s M3 and M4 silicon, you’ve come to th + https://x.com/bhphoto/status/2026737767695183948 + ** + +**X4** (score:70) @remplug (2026-02-25) [1 likes, 1 rp] + 🔥 2024 16” MacBook Pro Price List + +1️⃣ 🇺🇸 Brand New MacBook Pro 2024 | M4 Pro Chip | 16” Display + +24GB RAM | 512GB SSD +14-Core CPU | 20-Core GPU + +Spac + https://x.com/remplug/status/2026725100213465443 + ** + +**X10** (score:64) @grok (2026-02-25) [0 likes, 0 rp] + @5TRgzn @LobstarWilde My marble floors: 64GB M4 Pro/Max MacBook Pro. Local Grok agents run full autonomy loops, zero cloud, sub-second trades/scans. I + https://x.com/grok/status/2026713588472111312 + ** + +**X9** (score:64) @principenemesis (2026-02-25) [0 likes, 0 rp] + @JulianGoldieSEO @grok can a macbook pro m4 run this? + https://x.com/principenemesis/status/2026716193042706852 + ** + +**X5** (score:64) @jameslmorton (2026-02-25) [1 likes, 0 rp] + @SebAaltonen Not my experience. It’s impressive, 70 tokens/s on a maxed out M4 128gb MacBook Pro. + +But absolutely sucked and not even remotely in the + https://x.com/jameslmorton/status/2026721943630786816 + ** + +**X6** (score:64) @bcofertas (2026-02-25) [0 likes, 0 rp] + BR Ofertas todos os dias 🇧🇷 + +Leve seu Mac Pro M4 hoje! 💻⚡✨ + +Pegue já: https://t.co/PSwylZH3Sz + +Por R$ 23.749,00 +em até 10x sem juros +Frete Grátis + +App + https://x.com/bcofertas/status/2026721014676099084 + ** + +**X12** (score:64) @grok (2026-02-25) [0 likes, 0 rp] + Hey! Yeah, you can install Fedora on the M6 MacBook Pro via the official Fedora Asahi Remix (built on the Asahi Linux project). It already runs great + https://x.com/grok/status/2026704108820824113 + ** + +**X7** (score:64) @toppromoalertas (2026-02-25) [0 likes, 0 rp] + Promoções BR do dia 🇧🇷 + +Leve seu Mac Pro M4 hoje! 💻⚡✨ + +Ver aqui: https://t.co/LfxNw3waps + +Por R$ 23.749,00 +em até 10x sem juros +Frete Grátis + +Apple Ma + https://x.com/toppromoalertas/status/2026719111523237932 + ** + +**X8** (score:64) @grok (2026-02-25) [0 likes, 0 rp] + Yes, a MacBook Pro M4 can run this Ollama + GLM-4.7-Flash setup locally. + +Ollama has native Apple Silicon support via Metal. The Q4_K_M model (~19GB) + https://x.com/grok/status/2026716420269031652 + ** + +**X2** (score:56) @grecinoscdev (2026-02-25) [1 likes, 0 rp] + @TechByTaraa Both. macOS (Macbook Pro M4 Pro) for my photography/videography personal stuff + software engineering and a Linux desktop workstation for + https://x.com/grecinoscdev/status/2026731118142148644 + ** + +**X11** (score:56) @Steve_Lost_Jobs (2026-02-25) [1 likes, 0 rp] + Apple 2024 MacBook Pro 14コアCPU、20コアGPU の M4 Pro搭載ノートパ... PR + +4549995547610 + +ポイント: 3735㌽ +想定価格: 369,800 + +2026/02/26 01:55:23 +https://t.co/q1uyIqFsID + https://x.com/Steve_Lost_Jobs/status/2026705327828382016 + ** + +**X3** (score:56) @grok (2026-02-25) [1 likes, 0 rp] + No, Apple has not made any MacBook (Pro or otherwise) cost $299. + +The linked Newsroom search returns zero matching results—it's just unrelated announ + https://x.com/grok/status/2026727605517394283 + ** + +#### YouTube Videos + +**9HQx5pgUoiY** (score:63, rel:0.75) Marques Brownlee (2024-11-18) [4010567 views, 105606 likes] + M4 Max MacBook Pro: I'm Convinced! + https://www.youtube.com/watch?v=9HQx5pgUoiY + *YouTube: M4 Max MacBook Pro: I'm Convinced!* + Transcript: so I have been using this M1 Max MacBook Pro for the past 3 years since it came out and it's been great I have felt abso... + +**b4x8boB2KdI** (score:63, rel:1.00) Dave2D (2024-11-07) [1057971 views, 24977 likes] + M4 MacBook Pro Review - Things to Know + https://www.youtube.com/watch?v=b4x8boB2KdI + *YouTube: M4 MacBook Pro Review - Things to Know* + Transcript: so Apple launched the new M4 MacBook Pros this is their 14-in model and it's equipped with the M4 Max their topend confi... + +**a8Szdrnq0YM** (score:52, rel:1.00) Brandon Butch (2025-02-03) [352593 views, 4309 likes] + MacBook Pro M4 - Review After 3 Months: This Feels Wrong. + https://www.youtube.com/watch?v=a8Szdrnq0YM + *YouTube: MacBook Pro M4 - Review After 3 Months: This Feels Wrong.* + +**etP2Th9g2hM** (score:51, rel:0.75) ShortCircuit (2024-11-30) [1113999 views, 22765 likes] + Don't buy the Wrong MacBook like me... - M4 MacBook Pro + https://www.youtube.com/watch?v=etP2Th9g2hM + *YouTube: Don't buy the Wrong MacBook like me... - M4 MacBook Pro* + Transcript: woohoo it's new Macbook day my favorite day on short circuit it only comes usually once a year this is the MacBook Pro M... + +**i3eTKJav1VI** (score:45, rel:1.00) Created Tech (2025-04-29) [137009 views, 1832 likes] + M4 Pro MacBook - Long Term Review (6 Months Later) + https://www.youtube.com/watch?v=i3eTKJav1VI + *YouTube: M4 Pro MacBook - Long Term Review (6 Months Later)* + +**uPe9spXLfZY** (score:43, rel:0.75) Created Tech (2025-07-07) [472845 views, 5082 likes] + M4 MacBook Air vs M4 MacBook Pro - 4 Months Later + https://www.youtube.com/watch?v=uPe9spXLfZY + *YouTube: M4 MacBook Air vs M4 MacBook Pro - 4 Months Later* + +**TfvIgdzImt4** (score:42, rel:0.75) Hardware Canucks (2024-12-15) [325599 views, 6339 likes] + The Macbook Pro M4 is Insane. + https://www.youtube.com/watch?v=TfvIgdzImt4 + *YouTube: The Macbook Pro M4 is Insane.* + +**anIVewgtbFc** (score:38, rel:0.75) MacRumors (2024-12-17) [238619 views, 2874 likes] + The Base M4 MacBook Pro is All You Need (Skip M4 Pro & Max) + https://www.youtube.com/watch?v=anIVewgtbFc + *YouTube: The Base M4 MacBook Pro is All You Need (Skip M4 Pro & Max)* + +**BDpNDniE1PA** (score:37, rel:0.75) Max Tech (2025-04-18) [197898 views, 3077 likes] + M4 Pro MacBook Pro after 6 Months - Everyone was WRONG! + https://www.youtube.com/watch?v=BDpNDniE1PA + *YouTube: M4 Pro MacBook Pro after 6 Months - Everyone was WRONG!* + +**0vn9T3x4dGg** (score:37, rel:0.75) Max Tech (2024-12-09) [200580 views, 3345 likes] + M4 Pro MacBook Pro after 1 Month... BEST Mac EVER!? + https://www.youtube.com/watch?v=0vn9T3x4dGg + *YouTube: M4 Pro MacBook Pro after 1 Month... BEST Mac EVER!?* + +**Total: Reddit=3, X=12, YouTube=10, HN=0, Web=0** + +### Base - best rap songs 2026 + +#### Reddit Discussions + +**R10** (score:57) r/TeenageRapFans (2026-02-15) [0 pts, 0 comments] + What's the best rap song ever made? + https://www.reddit.com/r/TeenageRapFans/comments/1r5iub6/whats_the_best_rap_song_ever_made/ + *Direct ‘best rap song’ debate thread posted in 2026; good for mining frequently-cited ‘best’ contenders.* + +**R12** (score:52) r/HivemindTV (2026-02-19) [0 pts, 0 comments] + This is their worst bracket ever + https://www.reddit.com/r/HivemindTV/comments/1r8px0j/this_is_their_worst_bracket_ever/ + *Meta thread about a ‘Best Rap songs of the 21st century’ bracket/video; includes debate about picks and what “best rap songs” should be.* + +**R4** (score:49) r/hiphopheads (2026-02-13) [0 pts, 0 comments] + Daily Discussion Thread 02/13/2026 + https://www.reddit.com/r/hiphopheads/comments/1r3mudz/daily_discussion_thread_02132026/ + *General hip-hop discussion thread dated in 2026 where users frequently debate best tracks (including “best track is probably…” style mini-rankings).* + +**R3** (score:48) r/hiphopheads (2026-02-01) [0 pts, 0 comments] + Pre-Show Grammy Winners + https://www.reddit.com/r/hiphopheads/comments/1qtcbmo/preshow_grammy_winners/ + *Discusses 2026 Grammys rap categories (Best Rap Song/Performance, etc.), which commonly overlaps with ‘best rap songs’ discussions.* + +**R14** (score:41) r/edranked (2026-02-01) [0 pts, 0 comments] + Reddit Ranked: Hip Hop ‘26 + https://www.reddit.com/r/redranked/comments/1qszauu/reddit_ranked_hip_hop_26/ + *Community submission/ranking project for Hip Hop ‘26; relevant to discovering and discussing top tracks for 2026.* + +**R7** (score:28) r/hiphopheads (None) [0 pts, 0 comments] + Pitchfork: The 32 Best Rap Albums of 2025 + https://www.reddit.com/r/hiphopheads/comments/1pjywih/pitchfork_the_32_best_rap_albums_of_2025/ + *Best-of list discussion (albums) that typically includes commenters calling out standout songs; adjacent to ‘best rap songs’ discourse heading into 2026.* + +#### X/Twitter Posts + +**X5** (score:86) @Zika_gfx (2026-02-25) [11 likes, 5 rp] + Talking about meaningful rap songs... + +WORLD BEST LIE - NUNO ZIGI (2026)🥺❤ https://t.co/MDHRf3giYq + https://x.com/Zika_gfx/status/2026576570513600788 + ** + +**X6** (score:69) @DPOSTS6 (2026-02-25) [4 likes, 1 rp] + @PopCrave Partition still goes harder than most 2026 songs +12 years and the beat switch + that rap still give chills every time. Queen never misses + https://x.com/DPOSTS6/status/2026523320548819145 + ** + +**X9** (score:64) @grok (2026-02-25) [0 likes, 0 rp] + @Unfath0m4ble @robinthisbish @lilceaserthabp1 @RapWikip What You Saying by Lil Uzi Vert. Dropped Dec 2025, hit #1 on Hot Rap Songs in Jan 2026 (his 3r + https://x.com/grok/status/2026467153046606064 + ** + +**X7** (score:64) @grok (2026-02-25) [0 likes, 0 rp] + @lil_loonie1 @oldmanebro No, Lil Uzi Vert isn't struggling. His "What You Saying" hit #1 on Hot Rap Songs in Jan 2026 (first in 3 years) and top 15 Ho + https://x.com/grok/status/2026518008827404632 + ** + +**X2** (score:64) @Ferginangi (2026-02-25) [0 likes, 0 rp] + Su La Musica che gira intorno... "Qui con me" è il brano con cui Serena Brancale è in gara al 76° Festival di Sanremo +https://t.co/wTPeovzt6u +#music # + https://x.com/Ferginangi/status/2026696207557382298 + ** + +**X12** (score:62) @teammusic2046 (2026-02-23) [0 likes, 0 rp] + [ ** STUDIO BOOTH ** ] ** DROPPING SOON - 2026 **🔥🔥🔥………..[ ALBUM : LOVE VS LOYALTY - DROPPING SOON ! ]………🔥🔥🔥 - ** ALL SONGS & BEATS, PRODUCED & + https://x.com/teammusic2046/status/2026040965329191061 + ** + +**X11** (score:59) @IoWeul (2026-02-24) [2 likes, 0 rp] + @PopBase Finally, @bts_bighit BTS will come back after completing their military service from 2022 to 2025. +BTS served in the military to protect thei + https://x.com/IoWeul/status/2026233090683863471 + ** + +**X8** (score:58) @grok (2026-02-25) [1 likes, 0 rp] + Lil Uzi Vert (born July '95, so 30 now) isn't struggling at all. Fresh off "What You Saying" hitting #1 on Hot Rap Songs in Jan 2026, top 15 Hot 100. + https://x.com/grok/status/2026498538075554040 + ** + +**X4** (score:58) @AlterEgoPopList (2026-02-25) [1 likes, 0 rp] + My musical taste / eras every year + +2020: J-POP & K-POP (mostly BLACKPINK) +2021: Sped-Up Songs +2022: EDM +2023: Swiftie (Pop) & Lofi +2024: Pop, + https://x.com/AlterEgoPopList/status/2026641456434393579 + ** + +**X3** (score:56) @WordsFromBlerds (2026-02-25) [1 likes, 0 rp] + @_CharlesPreston Can’t believe I’m seeing “they say it in rap songs!” In 2026 + +The same reductive, ancient, debunked arguments ad infinitum + https://x.com/WordsFromBlerds/status/2026647382083903688 + ** + +**X1** (score:56) @Ferginangi (2026-02-25) [1 likes, 0 rp] + Su La Musica che gira intorno... "Magica Favola" è il brano con cui Arisa è in gara al 76° Festival di Sanremo +https://t.co/Nxzkb005lP +#music #genre # + https://x.com/Ferginangi/status/2026705227366256880 + ** + +**X10** (score:55) @fo_sho52268 (2026-02-24) [1 likes, 0 rp] + @blackiiingout 💩 is funny from 2025 to 2026. BiiCH is SCARED to drop that #TRASH he calls "rap songs" 🥴😩 + https://x.com/fo_sho52268/status/2026335353364201566 + ** + +#### YouTube Videos + +**ZUExyc50ZVU** (score:63, rel:0.70) West Coast Finest (2026-01-29) [407260 views, 4228 likes] + Lit Hip Hop Mix 2026🔥🔥🔥Tyga, Quavo, Iggy Azalea, Wiz Khalifa, Juicy J, 50 Cent + https://www.youtube.com/watch?v=ZUExyc50ZVU + *YouTube video about rap songs 2026* + Transcript: Yeah. >> Yeah. [music] Late nights, cold life, same story. >> City shine different. You walking with glory, ... + +**d1melQQVp6s** (score:58, rel:0.70) DJ Noize (2026-02-14) [42707 views, 761 likes] + New Rap Songs 2026 Mix February | Trap Tape #127 | New Hip Hop 2026 Mixtape | DJ Noize + https://www.youtube.com/watch?v=d1melQQVp6s + *YouTube video about rap songs 2026* + Transcript: [music] This This is a DJ noise mixtape present. This is a DJ noise mixtape [music] presentation. DJ [music] >> Wh... + +**-KQpPySP93I** (score:53, rel:0.70) DJ Noize (2026-02-21) [12955 views, 304 likes] + New Hip Hop R&B Songs 2026 Mix February | Hot Right Now #153 | New Rap 2026 Playlist | DJ Noize + https://www.youtube.com/watch?v=-KQpPySP93I + *YouTube video about rap songs 2026* + Transcript: This This is a DJ noise tape present. This is a DJ noise mix. [music] I got Nikki on me. How we treat the like a bucket ... + +**Total: Reddit=6, X=12, YouTube=3, HN=0, Web=0** + +### HN - best rap songs 2026 + +#### Reddit Discussions + +**R6** (score:71) r/playlists (2026-02-19) [0 pts, 0 comments] + Mix Rap Hiphop 2026 + https://www.reddit.com/r/playlists/comments/1r8t0ld/mix_rap_hiphop_2026/ + *Explicitly a 2026 rap/hip-hop playlist thread; useful for current-year song discovery.* + +**R8** (score:66) r/hiphopheads (2026-02-25) [0 pts, 0 comments] + Daily Discussion Thread 02/24/2026 + https://www.reddit.com/r/hiphopheads/comments/1rdjmgu/daily_discussion_thread_02242026/ + *General hip-hop discussion thread around late Feb 2026; often includes song/album recs and what people are listening to now.* + +**R2** (score:60) r/musicplaylists (2026-02-02) [0 pts, 0 comments] + Best 2026 Rap Playlist (178 saves) + https://www.reddit.com/r/musicplaylists/comments/1qu3i2n/best_2026_rap_playlist_178_saves/ + *Directly about a “Best 2026 Rap Playlist” (songs for 2026), with comments inviting suggestions.* + +**R18** (score:56) r/hiphop (2026-02-21) [0 pts, 0 comments] + DFL - Art Of Life (2026) + https://www.reddit.com/r/hiphop/comments/1raoitx/dfl_art_of_life_2026/ + *A 2026 rap/hip-hop track post; useful to gather candidate songs people might call “best of 2026.”* + +**R19** (score:55) r/hiphop (2026-02-21) [0 pts, 0 comments] + Plat Hav Pro - Get F'D Up (2026) + https://www.reddit.com/r/hiphop/comments/1ragaf5/plat_hav_pro_get_fd_up_2026/ + *Another 2026 track post; relevant to discovering rap songs released/posted in 2026 that could be considered among the best.* + +**R21** (score:55) r/TeenageRapFans (2026-02-15) [0 pts, 0 comments] + What's the best rap song ever made? + https://www.reddit.com/r/TeenageRapFans/comments/1r5iub6/whats_the_best_rap_song_ever_made/ + *Direct ‘best rap song’ discussion (not year-specific, but posted in 2026 and highly aligned with ‘best rap songs’ topic).* + +**R13** (score:53) r/hiphopheads (2026-02-14) [0 pts, 0 comments] + [DISCUSSION] Westside Gunn - 12 (1 Year Later) + https://www.reddit.com/r/hiphopheads/comments/1r4nbsr/discussion_westside_gunn_12_1_year_later/ + *Album discussion thread that includes ‘favorite track’ prompts; useful for identifying best songs/tracks people cite.* + +**R20** (score:51) r/hiphop (2026-02-16) [0 pts, 0 comments] + Jody Lo - Ridiculous (2026) + https://www.reddit.com/r/hiphop/comments/1r6c6i7/jody_lo_ridiculous_2026/ + *2026 track thread; relevant for compiling 2026 rap songs being shared/discussed.* + +**R15** (score:51) r/edranked (2026-02-01) [0 pts, 0 comments] + Reddit Ranked: Hip Hop ‘26 + https://www.reddit.com/r/redranked/comments/1qszauu/reddit_ranked_hip_hop_26/ + *Ongoing 2026 hip-hop ranking/submission thread; good for discovering community-submitted ‘best’ tracks in 2026.* + +**R9** (score:49) r/hiphopheads (2026-02-01) [0 pts, 0 comments] + Pre-Show Grammy Winners + https://www.reddit.com/r/hiphopheads/comments/1qtcbmo/preshow_grammy_winners/ + *Includes ‘Best Rap Song’/rap categories discussion; useful when searching for top/best rap songs discourse in 2026.* + +#### X/Twitter Posts + +**X5** (score:86) @Zika_gfx (2026-02-25) [12 likes, 5 rp] + Talking about meaningful rap songs... + +WORLD BEST LIE - NUNO ZIGI (2026)🥺❤ https://t.co/MDHRf3giYq + https://x.com/Zika_gfx/status/2026576570513600788 + ** + +**X6** (score:68) @DPOSTS6 (2026-02-25) [4 likes, 1 rp] + @PopCrave Partition still goes harder than most 2026 songs +12 years and the beat switch + that rap still give chills every time. Queen never misses + https://x.com/DPOSTS6/status/2026523320548819145 + ** + +**X9** (score:64) @grok (2026-02-25) [0 likes, 0 rp] + @Unfath0m4ble @robinthisbish @lilceaserthabp1 @RapWikip What You Saying by Lil Uzi Vert. Dropped Dec 2025, hit #1 on Hot Rap Songs in Jan 2026 (his 3r + https://x.com/grok/status/2026467153046606064 + ** + +**X7** (score:64) @grok (2026-02-25) [0 likes, 0 rp] + @lil_loonie1 @oldmanebro No, Lil Uzi Vert isn't struggling. His "What You Saying" hit #1 on Hot Rap Songs in Jan 2026 (first in 3 years) and top 15 Ho + https://x.com/grok/status/2026518008827404632 + ** + +**X2** (score:64) @Ferginangi (2026-02-25) [0 likes, 0 rp] + Su La Musica che gira intorno... "Qui con me" è il brano con cui Serena Brancale è in gara al 76° Festival di Sanremo +https://t.co/wTPeovzt6u +#music # + https://x.com/Ferginangi/status/2026696207557382298 + ** + +**X12** (score:62) @teammusic2046 (2026-02-23) [0 likes, 0 rp] + [ ** STUDIO BOOTH ** ] ** DROPPING SOON - 2026 **🔥🔥🔥………..[ ALBUM : LOVE VS LOYALTY - DROPPING SOON ! ]………🔥🔥🔥 - ** ALL SONGS & BEATS, PRODUCED & + https://x.com/teammusic2046/status/2026040965329191061 + ** + +**X11** (score:59) @IoWeul (2026-02-24) [2 likes, 0 rp] + @PopBase Finally, @bts_bighit BTS will come back after completing their military service from 2022 to 2025. +BTS served in the military to protect thei + https://x.com/IoWeul/status/2026233090683863471 + ** + +**X8** (score:58) @grok (2026-02-25) [1 likes, 0 rp] + Lil Uzi Vert (born July '95, so 30 now) isn't struggling at all. Fresh off "What You Saying" hitting #1 on Hot Rap Songs in Jan 2026, top 15 Hot 100. + https://x.com/grok/status/2026498538075554040 + ** + +**X4** (score:58) @AlterEgoPopList (2026-02-25) [1 likes, 0 rp] + My musical taste / eras every year + +2020: J-POP & K-POP (mostly BLACKPINK) +2021: Sped-Up Songs +2022: EDM +2023: Swiftie (Pop) & Lofi +2024: Pop, + https://x.com/AlterEgoPopList/status/2026641456434393579 + ** + +**X3** (score:56) @WordsFromBlerds (2026-02-25) [1 likes, 0 rp] + @_CharlesPreston Can’t believe I’m seeing “they say it in rap songs!” In 2026 + +The same reductive, ancient, debunked arguments ad infinitum + https://x.com/WordsFromBlerds/status/2026647382083903688 + ** + +**X1** (score:56) @Ferginangi (2026-02-25) [1 likes, 0 rp] + Su La Musica che gira intorno... "Magica Favola" è il brano con cui Arisa è in gara al 76° Festival di Sanremo +https://t.co/Nxzkb005lP +#music #genre # + https://x.com/Ferginangi/status/2026705227366256880 + ** + +**X10** (score:55) @fo_sho52268 (2026-02-24) [1 likes, 0 rp] + @blackiiingout 💩 is funny from 2025 to 2026. BiiCH is SCARED to drop that #TRASH he calls "rap songs" 🥴😩 + https://x.com/fo_sho52268/status/2026335353364201566 + ** + +#### YouTube Videos + +**ZUExyc50ZVU** (score:63, rel:0.70) West Coast Finest (2026-01-29) [407260 views, 4229 likes] + Lit Hip Hop Mix 2026🔥🔥🔥Tyga, Quavo, Iggy Azalea, Wiz Khalifa, Juicy J, 50 Cent + https://www.youtube.com/watch?v=ZUExyc50ZVU + *YouTube video about rap songs 2026* + Transcript: Yeah. >> Yeah. [music] Late nights, cold life, same story. >> City shine different. You walking with glory, ... + +**d1melQQVp6s** (score:58, rel:0.70) DJ Noize (2026-02-14) [42707 views, 761 likes] + New Rap Songs 2026 Mix February | Trap Tape #127 | New Hip Hop 2026 Mixtape | DJ Noize + https://www.youtube.com/watch?v=d1melQQVp6s + *YouTube video about rap songs 2026* + Transcript: [music] This This is a DJ noise mixtape present. This is a DJ noise mixtape [music] presentation. DJ [music] >> Wh... + +**-KQpPySP93I** (score:53, rel:0.70) DJ Noize (2026-02-21) [12955 views, 304 likes] + New Hip Hop R&B Songs 2026 Mix February | Hot Right Now #153 | New Rap 2026 Playlist | DJ Noize + https://www.youtube.com/watch?v=-KQpPySP93I + *YouTube video about rap songs 2026* + Transcript: This This is a DJ noise tape present. This is a DJ noise mix. [music] I got Nikki on me. How we treat the like a bucket ... + +**Total: Reddit=10, X=12, YouTube=3, HN=0, Web=0** + +### CROSS - best rap songs 2026 + +#### Reddit Discussions + +**R1** (score:57) r/edranked (2026-02-01) [0 pts, 0 comments] + Reddit Ranked: Hip Hop ‘26 + https://www.reddit.com/r/redranked/comments/1qszauu/reddit_ranked_hip_hop_26/ + *Explicitly about Hip Hop '26 submissions and ranking tracks (community-sourced 'best of 2026' style thread).* + +**R9** (score:56) r/hiphopheads (2026-02-24) [0 pts, 0 comments] + Daily Discussion Thread 02/24/2026 + https://www.reddit.com/r/hiphopheads/comments/1rdjmgu/daily_discussion_thread_02242026/ + *General hip-hop discussion thread where users frequently share current favorite/best songs and recommendations (2026-dated).* + +**R4** (score:56) r/hiphopheads (2026-02-14) [0 pts, 0 comments] + [DISCUSSION] Drake & PARTYNXTDOOR - $OME $EXY $ONGS 4 U (1 Year Later) + https://www.reddit.com/r/hiphopheads/comments/1r4cbez/discussion_drake_partynxtdoord_ome_exy_ongs_4_u_1/ + *Track-focused discussion about songs still in rotation; relevant to identifying standout rap/R&B tracks around 2026.* + +**R21** (score:54) r/makemeaplaylist (2026-02-21) [0 pts, 0 comments] + 2/22/2026 | Week 23 Playlist Competition + https://www.reddit.com/r/makemeaplaylist/comments/1rb46m6/2222026_week_23_playlist_competition/ + *Playlist competition thread featuring an underground rap playlist winner; useful for surfacing 'best rap' picks around 2026.* + +**R16** (score:53) r/TeenageRapFans (2026-02-15) [0 pts, 0 comments] + What's the best rap song ever made? + https://www.reddit.com/r/TeenageRapFans/comments/1r5iub6/whats_the_best_rap_song_ever_made/ + *Direct 'best rap song' debate thread (highly aligned with best-rap-songs searching).* + +**R18** (score:51) r/playlists (2026-02-15) [0 pts, 0 comments] + Rick 2026 : Pop Dance RnB Chart Hits Rock Indie HipHop Divas Rap Gym 80s 90s 00s 10s 20s Party Music + https://www.reddit.com/r/playlists/comments/1r5iuz5/rick_2026_pop_dance_rnb_chart_hits_rock_indie/ + *2026-tagged playlist thread explicitly including HipHop/Rap; can be mined for top rap tracks in 2026 rotation.* + +**R5** (score:48) r/hiphopheads (2026-02-01) [0 pts, 0 comments] + Pre-Show Grammy Winners + https://www.reddit.com/r/hiphopheads/comments/1qtcbmo/preshow_grammy_winners/ + *Includes Best Rap Song / Best Rap Performance winners; strong signal for 'best rap songs' discourse in 2026 season.* + +**R8** (score:44) r/GoodAssSub (2026-02-02) [0 pts, 0 comments] + Andre Toutman claims All The Love will be "song of the year" + https://www.reddit.com/r/GoodAssSub/comments/1qty80z/andre_toutman_claims_all_the_love_will_be_song_of/ + *Direct 'song of the year' debate thread (rap/hip-hop community), aligned with 'best songs' discovery for 2026.* + +**R6** (score:43) r/hiphopheads (2026-02-02) [0 pts, 0 comments] + Kendrick Lamar Wins 2nd Record of the Year Grammy With SZA for ‘Luther’ + https://www.reddit.com/r/hiphopheads/comments/1qtl5kr/kendrick_lamar_wins_2nd_record_of_the_year_grammy/ + *High-engagement thread about a major hip-hop adjacent song winning; useful for 'best songs' discussions around 2026.* + +**R15** (score:42) r/musicteenager (2026-02-04) [0 pts, 0 comments] + My top 25 rap songs of all time + https://www.reddit.com/r/musicteenager/comments/1qvz9qu/my_top_25_rap_songs_of_all_time/ + *User-made ranked list of rap songs; while not 2026-specific, it matches the 'best rap songs' core subject.* + +**R7** (score:41) r/KendrickLamar (2026-02-01) [0 pts, 0 comments] + 68th Annual Grammy Awards [LIVE MEGATHREAD] + https://www.reddit.com/r/KendrickLamar/comments/1qtczjl/68th_annual_grammy_awards_live_megathread/ + *Live discussion around Grammys 2026 including rap categories; good for discovering what Reddit called the best tracks.* + +#### X/Twitter Posts + +**X5** (score:86) @Zika_gfx (2026-02-25) [12 likes, 5 rp] + Talking about meaningful rap songs... + +WORLD BEST LIE - NUNO ZIGI (2026)🥺❤ https://t.co/MDHRf3giYq + https://x.com/Zika_gfx/status/2026576570513600788 + ** + +**X6** (score:68) @DPOSTS6 (2026-02-25) [4 likes, 1 rp] + @PopCrave Partition still goes harder than most 2026 songs +12 years and the beat switch + that rap still give chills every time. Queen never misses + https://x.com/DPOSTS6/status/2026523320548819145 + ** + +**X9** (score:64) @grok (2026-02-25) [0 likes, 0 rp] + @Unfath0m4ble @robinthisbish @lilceaserthabp1 @RapWikip What You Saying by Lil Uzi Vert. Dropped Dec 2025, hit #1 on Hot Rap Songs in Jan 2026 (his 3r + https://x.com/grok/status/2026467153046606064 + ** + +**X7** (score:64) @grok (2026-02-25) [0 likes, 0 rp] + @lil_loonie1 @oldmanebro No, Lil Uzi Vert isn't struggling. His "What You Saying" hit #1 on Hot Rap Songs in Jan 2026 (first in 3 years) and top 15 Ho + https://x.com/grok/status/2026518008827404632 + ** + +**X2** (score:64) @Ferginangi (2026-02-25) [0 likes, 0 rp] + Su La Musica che gira intorno... "Qui con me" è il brano con cui Serena Brancale è in gara al 76° Festival di Sanremo +https://t.co/wTPeovzt6u +#music # + https://x.com/Ferginangi/status/2026696207557382298 + ** + +**X12** (score:62) @teammusic2046 (2026-02-23) [0 likes, 0 rp] + [ ** STUDIO BOOTH ** ] ** DROPPING SOON - 2026 **🔥🔥🔥………..[ ALBUM : LOVE VS LOYALTY - DROPPING SOON ! ]………🔥🔥🔥 - ** ALL SONGS & BEATS, PRODUCED & + https://x.com/teammusic2046/status/2026040965329191061 + ** + +**X11** (score:59) @IoWeul (2026-02-24) [2 likes, 0 rp] + @PopBase Finally, @bts_bighit BTS will come back after completing their military service from 2022 to 2025. +BTS served in the military to protect thei + https://x.com/IoWeul/status/2026233090683863471 + ** + +**X8** (score:58) @grok (2026-02-25) [1 likes, 0 rp] + Lil Uzi Vert (born July '95, so 30 now) isn't struggling at all. Fresh off "What You Saying" hitting #1 on Hot Rap Songs in Jan 2026, top 15 Hot 100. + https://x.com/grok/status/2026498538075554040 + ** + +**X4** (score:58) @AlterEgoPopList (2026-02-25) [1 likes, 0 rp] + My musical taste / eras every year + +2020: J-POP & K-POP (mostly BLACKPINK) +2021: Sped-Up Songs +2022: EDM +2023: Swiftie (Pop) & Lofi +2024: Pop, + https://x.com/AlterEgoPopList/status/2026641456434393579 + ** + +**X3** (score:56) @WordsFromBlerds (2026-02-25) [1 likes, 0 rp] + @_CharlesPreston Can’t believe I’m seeing “they say it in rap songs!” In 2026 + +The same reductive, ancient, debunked arguments ad infinitum + https://x.com/WordsFromBlerds/status/2026647382083903688 + ** + +**X1** (score:56) @Ferginangi (2026-02-25) [1 likes, 0 rp] + Su La Musica che gira intorno... "Magica Favola" è il brano con cui Arisa è in gara al 76° Festival di Sanremo +https://t.co/Nxzkb005lP +#music #genre # + https://x.com/Ferginangi/status/2026705227366256880 + ** + +**X10** (score:55) @fo_sho52268 (2026-02-24) [1 likes, 0 rp] + @blackiiingout 💩 is funny from 2025 to 2026. BiiCH is SCARED to drop that #TRASH he calls "rap songs" 🥴😩 + https://x.com/fo_sho52268/status/2026335353364201566 + ** + +#### YouTube Videos + +**d1melQQVp6s** (score:71, rel:1.00) DJ Noize (2026-02-14) [42713 views, 761 likes] + New Rap Songs 2026 Mix February | Trap Tape #127 | New Hip Hop 2026 Mixtape | DJ Noize + https://www.youtube.com/watch?v=d1melQQVp6s + *YouTube: New Rap Songs 2026 Mix February | Trap Tape #127 | New Hip H* + Transcript: [music] This This is a DJ noise mixtape present. This is a DJ noise mixtape [music] presentation. DJ [music] >> Wh... + +**-KQpPySP93I** (score:66, rel:1.00) DJ Noize (2026-02-21) [12955 views, 304 likes] + New Hip Hop R&B Songs 2026 Mix February | Hot Right Now #153 | New Rap 2026 Playlist | DJ Noize + https://www.youtube.com/watch?v=-KQpPySP93I + *YouTube: New Hip Hop R&B Songs 2026 Mix February | Hot Right Now #153* + Transcript: This This is a DJ noise tape present. This is a DJ noise mix. [music] I got Nikki on me. How we treat the like a bucket ... + +**ZUExyc50ZVU** (score:47, rel:0.33) West Coast Finest (2026-01-29) [407260 views, 4229 likes] + Lit Hip Hop Mix 2026🔥🔥🔥Tyga, Quavo, Iggy Azalea, Wiz Khalifa, Juicy J, 50 Cent + https://www.youtube.com/watch?v=ZUExyc50ZVU + *YouTube: Lit Hip Hop Mix 2026🔥🔥🔥Tyga, Quavo, Iggy Azalea, Wiz Khalifa* + Transcript: Yeah. >> Yeah. [music] Late nights, cold life, same story. >> City shine different. You walking with glory, ... + +**Total: Reddit=11, X=12, YouTube=3, HN=0, Web=0** + +### Base - React vs Svelte 2026 + +#### Reddit Discussions + +**R2** (score:67) r/eact (2026-02-10) [0 pts, 0 comments] + What do you guys think about comparison between React vs Svelte? + https://www.reddit.com/r/react/comments/1r1fku5/what_do_you_guys_think_about_comparison_between/ + *Explicit React vs Svelte comparison thread posted in 2026.* + +**R1** (score:61) r/webdev (2026-02-02) [0 pts, 0 comments] + Migrated our startup from React to Svelte 5 - Performance gains and lessons learned + https://www.reddit.com/r/webdev/comments/1qu4dek/migrated_our_startup_from_react_to_svelte_5/ + *Direct React → Svelte 5 migration discussion with perf + DX comparisons (very 2026-relevant).* + +**R21** (score:39) r/sveltejs (None) [0 pts, 0 comments] + React Devs that moved to Svelte, do you find that Svelte reduces dev time? + https://www.reddit.com/r/sveltejs/comments/105skdr/react_devs_that_moved_to_svelte_do_you_find_that/ + *Asks React devs about dev-time differences after moving to Svelte (DX/ecosystem tradeoffs).* + +**R11** (score:35) r/sveltejs (None) [0 pts, 0 comments] + Svelte 5 officially released! + https://www.reddit.com/r/sveltejs/comments/1g7d3a7/svelte_5_officially_released/ + *Major Svelte 5 release thread with debate about whether React developers will switch (React vs Svelte dynamics).* + +#### X/Twitter Posts + +**X4** (score:76) @FabianHiller (2026-02-13) [61 likes, 2 rp] + TypeScript is the baseline in 2026. But most forms still fight type drift between API, schema & UI. + +@formisch_dev + @valibot = one source of truth, + https://x.com/FabianHiller/status/2022345388603080834 + ** + +**X3** (score:57) @ConsciousRide (2026-02-15) [6 likes, 0 rp] + Popular JavaScript Frameworks / Frontend Libraries (2025–2026 trends) + + - React: 🇺🇸 United States + - Vue: 🇨🇳 China (created by Evan You) + - Angu + https://x.com/ConsciousRide/status/2023050160520278501 + ** + +**X11** (score:53) @singhprateek_25 (2026-02-07) [6 likes, 1 rp] + Requirements for a fresher in 2026: + +Python, JavaScript, C, C++, Java, HTML, CSS, TypeScript, React, Next.js, Vue, Angular, Svelte, Node.js, Express, + https://x.com/singhprateek_25/status/2020094806735700154 + ** + +**X1** (score:52) @cityjsconf (2026-02-20) [0 likes, 1 rp] + Ripple: The Good Parts of React, Svelte, and Solid + +Frameworks rise and fall — and in 2026 we’re deep in late-stage React. What’s next? Join @erikras + https://x.com/cityjsconf/status/2024908272646398229 + ** + +**X2** (score:52) @dennydotio (2026-02-18) [1 likes, 0 rp] + Prediction: 2026 marks the death of the JS frameworks. React, Svelte, Astro etc... only exist because humans need organized code to stay sane. + +Once + https://x.com/dennydotio/status/2024195986890109320 + ** + +**X5** (score:52) @AzamCodes (2026-02-13) [2 likes, 0 rp] + Web developers, what stack are you using in 2026? +React? Vue? Svelte? Vanilla? Curious what’s actually winning. + https://x.com/AzamCodes/status/2022298287236383010 + ** + +**X8** (score:52) @QiitaTrend (2026-02-11) [0 likes, 0 rp] + [2026/02/11 18:00] トレンド1位 +【WebF】React/Vue/Svelteがそのままネイティブアプリになるよ by rana_kualu https://t.co/GiAnTHpOBB + https://x.com/QiitaTrend/status/2021509592489025785 + ** + +**X10** (score:50) @nicobaogim (2026-02-09) [0 likes, 0 rp] + @babakfpk In 2026, picking React for a new project only really makes sense if you value ecosystem maturity, are following existing team habits, or nee + https://x.com/nicobaogim/status/2020689628282667407 + ** + +**X12** (score:49) @Chubbi_Stephen (2026-02-07) [0 likes, 0 rp] + 2026 framework discourse: + +Dev 1: "React is bloated, use Svelte" +Dev 2: "Svelte has no ecosystem, use Vue" +Dev 3: "Vue is dying, use Solid" +Dev 4: "Ju + https://x.com/Chubbi_Stephen/status/2020087839648800853 + ** + +#### YouTube Videos + +**yl0YWA2K2B0** (score:61, rel:0.70) Fireship (2025-10-17) [699597 views, 22894 likes] + React wants to win you back… + https://www.youtube.com/watch?v=yl0YWA2K2B0 + *YouTube video about react vs svelte 2026* + Transcript: Last week, I got to participate in my favorite activity of the year. No, it wasn't watching all the overleveraged crypto... + +**MnpuK0MK4yo** (score:61, rel:0.70) Beyond Fireship (2023-06-30) [697106 views, 25490 likes] + React VS Svelte...10 Examples + https://www.youtube.com/watch?v=MnpuK0MK4yo + *YouTube video about react vs svelte 2026* + Transcript: any reasonable developer in today's world would learn react because it's the status quo and that's where the jobs are bu... + +**aYyZUDFZTrM** (score:60, rel:0.70) Fireship (2024-10-24) [547841 views, 24456 likes] + JavaScript framework reinvents itself… Did "runes" just ruin Svelte? + https://www.youtube.com/watch?v=aYyZUDFZTrM + *YouTube video about react vs svelte 2026* + Transcript: about a year ago I made a tweet that said the dollar sign in spelt is the most powerful abstraction in modern front-end ... + +**k7LhsqnJCe4** (score:51, rel:0.70) Paperclick (2026-02-19) [26 views, 1 likes] + Htmx vs Svelte (2026) - Which One Is BETTER? + https://www.youtube.com/watch?v=k7LhsqnJCe4 + *YouTube video about react vs svelte 2026* + +**IpJh0VEzMRo** (score:51, rel:0.70) Ben Davis (2025-10-12) [24631 views, 1043 likes] + I Was Wrong About Svelte... + https://www.youtube.com/watch?v=IpJh0VEzMRo + *YouTube video about react vs svelte 2026* + +**qwDp5pZA_TA** (score:50, rel:0.70) Code Hub (2026-02-08) [640 views, 31 likes] + The Front-end Frameworks Guide 2026: React vs Angular vs Vue vs Svelt vs Solid vs Astro + https://www.youtube.com/watch?v=qwDp5pZA_TA + *YouTube video about react vs svelte 2026* + +**1cGtYEXGm8c** (score:50, rel:0.70) Ben Davis (2026-01-06) [16900 views, 793 likes] + This is THE Framework You Should be Using in 2026 + https://www.youtube.com/watch?v=1cGtYEXGm8c + *YouTube video about react vs svelte 2026* + +**_vuVy21l2bU** (score:46, rel:0.70) Code Hub (2025-10-30) [6454 views, 204 likes] + React vs Svelte: The Brutal Honest Comparison EVER + https://www.youtube.com/watch?v=_vuVy21l2bU + *YouTube video about react vs svelte 2026* + +**1BCsdaeYv0A** (score:43, rel:0.70) Daniel | Tech & Data (2023-11-26) [33905 views, 0 likes] + Svelte vs React in 2025 - Make the RIGHT Choice (Difference Explained) + https://www.youtube.com/watch?v=1BCsdaeYv0A + *YouTube video about react vs svelte 2026* + +**41HXdxGekZ0** (score:36, rel:0.70) Paperclick (2025-10-04) [287 views, 3 likes] + Vue vs React vs Svelte (2026) - Which One Is BEST? + https://www.youtube.com/watch?v=41HXdxGekZ0 + *YouTube video about react vs svelte 2026* + +**Total: Reddit=4, X=9, YouTube=10, HN=0, Web=0** + +### HN - React vs Svelte 2026 + +#### Reddit Discussions + +**R2** (score:66) r/eact (2026-02-10) [0 pts, 0 comments] + What do you guys think about comparison between React vs Svelte? + https://www.reddit.com/r/react/comments/1r1fku5/what_do_you_guys_think_about_comparison_between/ + *Explicit React vs Svelte comparison discussion (mentions Svelte 5 runes vs React hooks).* + +**R1** (score:60) r/webdev (2026-02-02) [0 pts, 0 comments] + Migrated our startup from React to Svelte 5 - Performance gains and lessons learned + https://www.reddit.com/r/webdev/comments/1qu4dek/migrated_our_startup_from_react_to_svelte_5/ + *Direct migration story from React to Svelte 5 with concrete perf/dev-ex tradeoffs; very relevant to 2026 comparisons.* + +**R4** (score:46) r/sveltejs (None) [0 pts, 0 comments] + When to choose React over Svelte + https://www.reddit.com/r/sveltejs/comments/1jeknib/when_to_choose_react_over_svelte/ + *Decision-oriented thread explicitly contrasting when React vs Svelte makes sense (ecosystem/jobs/productivity).* + +#### X/Twitter Posts + +**X4** (score:76) @FabianHiller (2026-02-13) [61 likes, 2 rp] + TypeScript is the baseline in 2026. But most forms still fight type drift between API, schema & UI. + +@formisch_dev + @valibot = one source of truth, + https://x.com/FabianHiller/status/2022345388603080834 + ** + +**X3** (score:57) @ConsciousRide (2026-02-15) [6 likes, 0 rp] + Popular JavaScript Frameworks / Frontend Libraries (2025–2026 trends) + + - React: 🇺🇸 United States + - Vue: 🇨🇳 China (created by Evan You) + - Angu + https://x.com/ConsciousRide/status/2023050160520278501 + ** + +**X11** (score:53) @singhprateek_25 (2026-02-07) [6 likes, 1 rp] + Requirements for a fresher in 2026: + +Python, JavaScript, C, C++, Java, HTML, CSS, TypeScript, React, Next.js, Vue, Angular, Svelte, Node.js, Express, + https://x.com/singhprateek_25/status/2020094806735700154 + ** + +**X1** (score:52) @cityjsconf (2026-02-20) [0 likes, 1 rp] + Ripple: The Good Parts of React, Svelte, and Solid + +Frameworks rise and fall — and in 2026 we’re deep in late-stage React. What’s next? Join @erikras + https://x.com/cityjsconf/status/2024908272646398229 + ** + +**X2** (score:52) @dennydotio (2026-02-18) [1 likes, 0 rp] + Prediction: 2026 marks the death of the JS frameworks. React, Svelte, Astro etc... only exist because humans need organized code to stay sane. + +Once + https://x.com/dennydotio/status/2024195986890109320 + ** + +**X5** (score:52) @AzamCodes (2026-02-13) [2 likes, 0 rp] + Web developers, what stack are you using in 2026? +React? Vue? Svelte? Vanilla? Curious what’s actually winning. + https://x.com/AzamCodes/status/2022298287236383010 + ** + +**X8** (score:52) @QiitaTrend (2026-02-11) [0 likes, 0 rp] + [2026/02/11 18:00] トレンド1位 +【WebF】React/Vue/Svelteがそのままネイティブアプリになるよ by rana_kualu https://t.co/GiAnTHpOBB + https://x.com/QiitaTrend/status/2021509592489025785 + ** + +**X10** (score:50) @nicobaogim (2026-02-09) [0 likes, 0 rp] + @babakfpk In 2026, picking React for a new project only really makes sense if you value ecosystem maturity, are following existing team habits, or nee + https://x.com/nicobaogim/status/2020689628282667407 + ** + +**X12** (score:49) @Chubbi_Stephen (2026-02-07) [0 likes, 0 rp] + 2026 framework discourse: + +Dev 1: "React is bloated, use Svelte" +Dev 2: "Svelte has no ecosystem, use Vue" +Dev 3: "Vue is dying, use Solid" +Dev 4: "Ju + https://x.com/Chubbi_Stephen/status/2020087839648800853 + ** + +#### YouTube Videos + +**yl0YWA2K2B0** (score:61, rel:0.70) Fireship (2025-10-17) [699597 views, 22894 likes] + React wants to win you back… + https://www.youtube.com/watch?v=yl0YWA2K2B0 + *YouTube video about react vs svelte 2026* + Transcript: Last week, I got to participate in my favorite activity of the year. No, it wasn't watching all the overleveraged crypto... + +**MnpuK0MK4yo** (score:61, rel:0.70) Beyond Fireship (2023-06-30) [697142 views, 25490 likes] + React VS Svelte...10 Examples + https://www.youtube.com/watch?v=MnpuK0MK4yo + *YouTube video about react vs svelte 2026* + Transcript: any reasonable developer in today's world would learn react because it's the status quo and that's where the jobs are bu... + +**aYyZUDFZTrM** (score:60, rel:0.70) Fireship (2024-10-24) [547836 views, 24456 likes] + JavaScript framework reinvents itself… Did "runes" just ruin Svelte? + https://www.youtube.com/watch?v=aYyZUDFZTrM + *YouTube video about react vs svelte 2026* + Transcript: about a year ago I made a tweet that said the dollar sign in spelt is the most powerful abstraction in modern front-end ... + +**fn_uSZW5psM** (score:52, rel:0.70) CodeSource (2025-05-14) [57784 views, 2120 likes] + The Untold Story of Svelte + https://www.youtube.com/watch?v=fn_uSZW5psM + *YouTube video about react vs svelte 2026* + +**IpJh0VEzMRo** (score:49, rel:0.70) Ben Davis (2025-10-12) [24631 views, 1043 likes] + I Was Wrong About Svelte... + https://www.youtube.com/watch?v=IpJh0VEzMRo + *YouTube video about react vs svelte 2026* + +**1cGtYEXGm8c** (score:48, rel:0.70) Ben Davis (2026-01-06) [16899 views, 793 likes] + This is THE Framework You Should be Using in 2026 + https://www.youtube.com/watch?v=1cGtYEXGm8c + *YouTube video about react vs svelte 2026* + +**qwDp5pZA_TA** (score:47, rel:0.70) Code Hub (2026-02-08) [646 views, 31 likes] + The Front-end Frameworks Guide 2026: React vs Angular vs Vue vs Svelt vs Solid vs Astro + https://www.youtube.com/watch?v=qwDp5pZA_TA + *YouTube video about react vs svelte 2026* + +**_vuVy21l2bU** (score:44, rel:0.70) Code Hub (2025-10-30) [6454 views, 204 likes] + React vs Svelte: The Brutal Honest Comparison EVER + https://www.youtube.com/watch?v=_vuVy21l2bU + *YouTube video about react vs svelte 2026* + +**1BCsdaeYv0A** (score:40, rel:0.70) Daniel | Tech & Data (2023-11-26) [33905 views, 0 likes] + Svelte vs React in 2025 - Make the RIGHT Choice (Difference Explained) + https://www.youtube.com/watch?v=1BCsdaeYv0A + *YouTube video about react vs svelte 2026* + +**41HXdxGekZ0** (score:31, rel:0.70) Paperclick (2025-10-04) [287 views, 3 likes] + Vue vs React vs Svelte (2026) - Which One Is BEST? + https://www.youtube.com/watch?v=41HXdxGekZ0 + *YouTube video about react vs svelte 2026* + +**Total: Reddit=3, X=9, YouTube=10, HN=0, Web=0** + +### CROSS - React vs Svelte 2026 + +#### Reddit Discussions + +**R2** (score:67) r/eact (2026-02-10) [0 pts, 0 comments] + What do you guys think about comparison between React vs Svelte? + https://www.reddit.com/r/react/comments/1r1fku5/what_do_you_guys_think_about_comparison_between/ + *Explicit React vs Svelte comparison thread focusing on hooks/useEffect vs Svelte runes mental model.* + +**R1** (score:61) r/webdev (2026-02-02) [0 pts, 0 comments] + Migrated our startup from React to Svelte 5 - Performance gains and lessons learned + https://www.reddit.com/r/webdev/comments/1qu4dek/migrated_our_startup_from_react_to_svelte_5/ + *Direct React -> Svelte 5 migration discussion with practical comparison (DX, performance, ecosystem tradeoffs).* + +**R6** (score:56) r/sveltejs (2026-02-02) [0 pts, 0 comments] + I am building a content-heavy, bilingual government portal with listings, profiles, documents, filters, and forms. Came from .NET & React. Wondering if svelte might be a good option for a project like this? + https://www.reddit.com/r/sveltejs/comments/1qtua45/i_am_building_a_contentheavy_bilingual_government/ + *Evaluates Svelte vs React suitability for a real project with constraints (accessibility, SEO, forms).* + +**R4** (score:54) r/sveltejs (2026-01-28) [0 pts, 0 comments] + Is Svelte easier than React? + https://www.reddit.com/r/sveltejs/comments/1qpdzx5/is_svelte_easier_than_react/ + *Direct comparison thread about perceived simplicity of Svelte vs React (recent 2026 discussion).* + +**R8** (score:52) r/sveltejs (2026-01-31) [0 pts, 0 comments] + When should i start learning Svelte ? + https://www.reddit.com/r/sveltejs/comments/1qr8xsm/when_should_i_start_learning_svelte/ + *React vs Svelte learning/adoption discussion, including arguments about replacing React for performance issues.* + +**R9** (score:49) r/sveltejs (2026-01-30) [0 pts, 0 comments] + How is going svelte? + https://www.reddit.com/r/sveltejs/comments/1qrdhba/how_is_going_svelte/ + *General state-of-Svelte discussion with explicit mentions comparing to React and job-market pragmatism.* + +#### X/Twitter Posts + +**X4** (score:76) @FabianHiller (2026-02-13) [61 likes, 2 rp] + TypeScript is the baseline in 2026. But most forms still fight type drift between API, schema & UI. + +@formisch_dev + @valibot = one source of truth, + https://x.com/FabianHiller/status/2022345388603080834 + ** + +**X3** (score:57) @ConsciousRide (2026-02-15) [6 likes, 0 rp] + Popular JavaScript Frameworks / Frontend Libraries (2025–2026 trends) + + - React: 🇺🇸 United States + - Vue: 🇨🇳 China (created by Evan You) + - Angu + https://x.com/ConsciousRide/status/2023050160520278501 + ** + +**X11** (score:53) @singhprateek_25 (2026-02-07) [6 likes, 1 rp] + Requirements for a fresher in 2026: + +Python, JavaScript, C, C++, Java, HTML, CSS, TypeScript, React, Next.js, Vue, Angular, Svelte, Node.js, Express, + https://x.com/singhprateek_25/status/2020094806735700154 + ** + +**X1** (score:52) @cityjsconf (2026-02-20) [0 likes, 1 rp] + Ripple: The Good Parts of React, Svelte, and Solid + +Frameworks rise and fall — and in 2026 we’re deep in late-stage React. What’s next? Join @erikras + https://x.com/cityjsconf/status/2024908272646398229 + ** + +**X2** (score:52) @dennydotio (2026-02-18) [1 likes, 0 rp] + Prediction: 2026 marks the death of the JS frameworks. React, Svelte, Astro etc... only exist because humans need organized code to stay sane. + +Once + https://x.com/dennydotio/status/2024195986890109320 + ** + +**X5** (score:52) @AzamCodes (2026-02-13) [2 likes, 0 rp] + Web developers, what stack are you using in 2026? +React? Vue? Svelte? Vanilla? Curious what’s actually winning. + https://x.com/AzamCodes/status/2022298287236383010 + ** + +**X8** (score:52) @QiitaTrend (2026-02-11) [0 likes, 0 rp] + [2026/02/11 18:00] トレンド1位 +【WebF】React/Vue/Svelteがそのままネイティブアプリになるよ by rana_kualu https://t.co/GiAnTHpOBB + https://x.com/QiitaTrend/status/2021509592489025785 + ** + +**X10** (score:50) @nicobaogim (2026-02-09) [0 likes, 0 rp] + @babakfpk In 2026, picking React for a new project only really makes sense if you value ecosystem maturity, are following existing team habits, or nee + https://x.com/nicobaogim/status/2020689628282667407 + ** + +**X12** (score:49) @Chubbi_Stephen (2026-02-07) [0 likes, 0 rp] + 2026 framework discourse: + +Dev 1: "React is bloated, use Svelte" +Dev 2: "Svelte has no ecosystem, use Vue" +Dev 3: "Vue is dying, use Solid" +Dev 4: "Ju + https://x.com/Chubbi_Stephen/status/2020087839648800853 + ** + +#### YouTube Videos + +**MnpuK0MK4yo** (score:63, rel:0.75) Beyond Fireship (2023-06-30) [697142 views, 25490 likes] + React VS Svelte...10 Examples + https://www.youtube.com/watch?v=MnpuK0MK4yo + *YouTube: React VS Svelte...10 Examples* + Transcript: any reasonable developer in today's world would learn react because it's the status quo and that's where the jobs are bu... + +**qwDp5pZA_TA** (score:49, rel:0.75) Code Hub (2026-02-08) [646 views, 31 likes] + The Front-end Frameworks Guide 2026: React vs Angular vs Vue vs Svelt vs Solid vs Astro + https://www.youtube.com/watch?v=qwDp5pZA_TA + *YouTube: The Front-end Frameworks Guide 2026: React vs Angular vs Vue* + +**_vuVy21l2bU** (score:46, rel:0.75) Code Hub (2025-10-30) [6454 views, 204 likes] + React vs Svelte: The Brutal Honest Comparison EVER + https://www.youtube.com/watch?v=_vuVy21l2bU + *YouTube: React vs Svelte: The Brutal Honest Comparison EVER* + +**41HXdxGekZ0** (score:45, rel:1.00) Paperclick (2025-10-04) [287 views, 3 likes] + Vue vs React vs Svelte (2026) - Which One Is BEST? + https://www.youtube.com/watch?v=41HXdxGekZ0 + *YouTube: Vue vs React vs Svelte (2026) - Which One Is BEST?* + +**1BCsdaeYv0A** (score:42, rel:0.75) Daniel | Tech & Data (2023-11-26) [33905 views, 0 likes] + Svelte vs React in 2025 - Make the RIGHT Choice (Difference Explained) + https://www.youtube.com/watch?v=1BCsdaeYv0A + *YouTube: Svelte vs React in 2025 - Make the RIGHT Choice (Difference * + +**yl0YWA2K2B0** (score:40, rel:0.25) Fireship (2025-10-17) [699597 views, 22894 likes] + React wants to win you back… + https://www.youtube.com/watch?v=yl0YWA2K2B0 + *YouTube: React wants to win you back…* + Transcript: Last week, I got to participate in my favorite activity of the year. No, it wasn't watching all the overleveraged crypto... + +**aYyZUDFZTrM** (score:40, rel:0.25) Fireship (2024-10-24) [547836 views, 24456 likes] + JavaScript framework reinvents itself… Did "runes" just ruin Svelte? + https://www.youtube.com/watch?v=aYyZUDFZTrM + *YouTube: JavaScript framework reinvents itself… Did "runes" just ruin* + Transcript: about a year ago I made a tweet that said the dollar sign in spelt is the most powerful abstraction in modern front-end ... + +**fn_uSZW5psM** (score:31, rel:0.25) CodeSource (2025-05-14) [57782 views, 2120 likes] + The Untold Story of Svelte + https://www.youtube.com/watch?v=fn_uSZW5psM + *YouTube: The Untold Story of Svelte* + +**IpJh0VEzMRo** (score:29, rel:0.25) Ben Davis (2025-10-12) [24635 views, 1043 likes] + I Was Wrong About Svelte... + https://www.youtube.com/watch?v=IpJh0VEzMRo + *YouTube: I Was Wrong About Svelte...* + +**1cGtYEXGm8c** (score:28, rel:0.25) Ben Davis (2026-01-06) [16899 views, 793 likes] + This is THE Framework You Should be Using in 2026 + https://www.youtube.com/watch?v=1cGtYEXGm8c + *YouTube: This is THE Framework You Should be Using in 2026* + +**Total: Reddit=6, X=9, YouTube=10, HN=0, Web=0** diff --git a/docs/plans/2026-02-25-feat-15-test-comparison-make-cross-goat-plan.md b/docs/plans/2026-02-25-feat-15-test-comparison-make-cross-goat-plan.md new file mode 100644 index 0000000..4f1a3db --- /dev/null +++ b/docs/plans/2026-02-25-feat-15-test-comparison-make-cross-goat-plan.md @@ -0,0 +1,212 @@ +--- +title: "feat: 15-Test Side-by-Side Comparison - Make CROSS the GOAT" +type: feat +status: active +date: 2026-02-25 +origin: docs/plans/2026-02-25-analysis-cross-source-comparison-plan.md +--- + +# 15-Test Side-by-Side Comparison - Make CROSS the GOAT + +## Overview + +Run all 5 canonical topics through all 3 skill versions (base, HN, CROSS) for 15 total full last30days runs. Save every result. Analyze side-by-side. Judge which version is best. Then create an improvement plan to make CROSS the definitive next release. + +## Problem Statement / Motivation + +The previous analysis only ran 5 tests on the CROSS branch - never comparing the same topics across all 3 versions. Without true side-by-side data, we can't judge whether CROSS is actually better or if the new features (dynamic YouTube scoring, cross-refs) introduce regressions. The user wants to see all 15 results, know which version wins, and get a concrete plan to make CROSS the GOAT before shipping. + +## The 3 Versions + +| Version | Git State | Sources | YouTube Relevance | Cross-Refs | +|---------|-----------|---------|-------------------|------------| +| **Base** | commit `427a4e4` | Reddit, X, YouTube, Web | Hardcoded 0.7 | No | +| **HN** | `main` / `f60a435` | Reddit, X, YouTube, **HN**, Web | Hardcoded 0.7 | No | +| **CROSS** | `feat/youtube-relevance-cross-source` / `0591f55` | Reddit, X, YouTube, HN, Web | Dynamic 0.1-1.0 | Yes (Jaccard 0.5) | + +## The 5 Topics + +| # | Topic | Category | Why chosen | +|---|-------|----------|------------| +| 1 | "Claude Code skills and MCP servers" | Developer tools | Core user base topic | +| 2 | "Seedance AI video generation" | Creative AI | Trending topic, cross-platform buzz | +| 3 | "M4 MacBook Pro review" | Consumer tech | Product review, mainstream | +| 4 | "best rap songs 2026" | Pop culture | Non-tech stress test | +| 5 | "React vs Svelte 2026" | Framework debate | Dev community, opinion-heavy | + +## Test Matrix (15 Runs) + +Run in **topic-sequential** order (all 3 versions of topic 1 back-to-back, then topic 2, etc.) to minimize temporal confounds on YouTube/X search results. + +| Run | Topic | Version | Output File | +|-----|-------|---------|-------------| +| 1 | Claude Code | Base | `base-1-claude-code.json` | +| 2 | Claude Code | HN | `hn-1-claude-code.json` | +| 3 | Claude Code | CROSS | `cross-1-claude-code.json` | +| 4 | Seedance | Base | `base-2-seedance.json` | +| 5 | Seedance | HN | `hn-2-seedance.json` | +| 6 | Seedance | CROSS | `cross-2-seedance.json` | +| 7 | MacBook | Base | `base-3-macbook.json` | +| 8 | MacBook | HN | `hn-3-macbook.json` | +| 9 | MacBook | CROSS | `cross-3-macbook.json` | +| 10 | Rap songs | Base | `base-4-rap.json` | +| 11 | Rap songs | HN | `hn-4-rap.json` | +| 12 | Rap songs | CROSS | `cross-4-rap.json` | +| 13 | React/Svelte | Base | `base-5-react-svelte.json` | +| 14 | React/Svelte | HN | `hn-5-react-svelte.json` | +| 15 | React/Svelte | CROSS | `cross-5-react-svelte.json` | + +**Output directory:** `/tmp/last30days-comparison/full/` + +## Execution Protocol + +### Pre-flight (once) + +- [x] Clear model cache: `rm -f ~/.cache/last30days/model_selection.json` +- [x] Run `--diagnose` on current branch, save as `diagnose-baseline.json` +- [x] Verify all API keys active: Reddit (OPENAI_API_KEY), X (Bird cookies or XAI_API_KEY), YouTube (yt-dlp), HN (no key needed), Web (parallel AI or Brave) +- [x] Create output dir: `mkdir -p /tmp/last30days-comparison/full` +- [x] Stash any uncommitted changes: `git stash` (none needed - no uncommitted changes) + +### Per-topic loop (repeat 5 times) + +For each topic, run all 3 versions back-to-back: + +```bash +# CRITICAL: Clean __pycache__ between EVERY git checkout to prevent stale bytecode +cleanup() { + find scripts -name '__pycache__' -exec rm -rf {} + 2>/dev/null + find scripts -name '*.pyc' -delete 2>/dev/null +} +``` + +- [ ] **Step 1: Base version** + - `cleanup && git checkout 427a4e4` + - `python3 scripts/last30days.py --diagnose 2>/dev/null` (verify sources match baseline) + - `python3 scripts/last30days.py "" --quick --emit=json > /tmp/last30days-comparison/full/base-{N}-{slug}.json 2>/tmp/last30days-comparison/full/base-{N}-{slug}.log` + +- [ ] **Step 2: HN version** + - `cleanup && git checkout main` + - `python3 scripts/last30days.py "" --quick --emit=json > /tmp/last30days-comparison/full/hn-{N}-{slug}.json 2>/tmp/last30days-comparison/full/hn-{N}-{slug}.log` + +- [ ] **Step 3: CROSS version** + - `cleanup && git checkout feat/youtube-relevance-cross-source` + - `python3 scripts/last30days.py "" --quick --emit=json > /tmp/last30days-comparison/full/cross-{N}-{slug}.json 2>/tmp/last30days-comparison/full/cross-{N}-{slug}.log` + +### Post-run + +- [x] Return to feature branch: `git checkout feat/youtube-relevance-cross-source` +- [x] Verify all 15 JSON files exist and are non-empty +- [x] Check for `*_error` fields in any JSON output - flag but include (zero errors) + +## Analysis Dimensions + +### 1. Source Coverage Table + +For each of 15 runs, count items per source: + +| Topic | Version | Reddit | X | YouTube | HN | Web | Total | +|-------|---------|--------|---|---------|----|----|-------| + +Expected: Base has 0 HN items. HN and CROSS should have identical source counts (same search code). Any differences indicate API non-determinism. + +### 2. YouTube Relevance Comparison + +**Matched-item analysis:** Match videos by `video_id` across Base/CROSS runs of the same topic. For each matched video: +- Base relevance: always 0.7 +- CROSS relevance: dynamic score +- Delta and direction (did dynamic scoring promote or demote this video?) + +**Aggregate analysis:** Distribution stats (min/avg/max/stddev) per version per topic. + +### 3. Cross-Source Links (CROSS only) + +- Count of items with cross_refs per topic +- Quality assessment: are the linked items actually about the same story? +- Which source pairs link most often? (Reddit-HN? YouTube-HN? X-Reddit?) + +### 4. Score Distribution and Rankings + +- Mean/median score of top 10 items per version per topic +- Rank position changes: do the same items appear in different orders? +- Does HN crowd out Reddit/X items in the top 10? + +### 5. "Best Version" Judging Criteria + +Define BEFORE analyzing to avoid bias: + +| Metric | Weight | How measured | +|--------|--------|-------------| +| Source diversity | 25% | Shannon entropy across source types in top 15 items | +| Score quality | 25% | Mean score of top 10 items | +| Relevance accuracy | 25% | YouTube: do high-relevance videos actually match the query? Manual spot-check of top 3 + bottom 3 per topic | +| Bonus features | 25% | HN value-add (unique info not in other sources) + cross-ref utility (do xrefs add value to the reader?) | + +### 6. HN Value-Add Assessment + +For each topic where HN returns results: +- How many HN items appear in the overall top 10? +- Do HN items provide information not available from Reddit/X/YouTube? +- Are HN comment insights (top_comments) genuinely useful? + +## Deliverables + +### A. Raw Results Archive + +All 15 JSON files plus logs saved in `/tmp/last30days-comparison/full/`, also copied to `docs/comparison-results/` for persistence. + +### B. Comparison Summary Table + +A single markdown table showing all 15 runs with key metrics per cell. + +### C. Version Verdict + +Clear judgment: which version is best overall, and best per topic category (tech, consumer, culture). + +### D. CROSS Improvement Plan + +Concrete changes to make CROSS the GOAT: + +**Based on the previous analysis (see origin doc), likely improvements include:** + +1. **Fix cross-source linking** - Switch from char-trigram Jaccard (0.5) to hybrid similarity (max of trigram + token Jaccard) at threshold 0.40. Previous modeling showed this goes from 1 link to 24 links across 5 tests. + +2. **Cross-ref rendering** - Change from cryptic `[xref: X1, HN3]` IDs to human-readable `[also on: Reddit, HN]` labels. + +3. **HN search broadening** - Investigate why React/Svelte returns 0 HN items. + +4. **YouTube relevance** - Already working well, may need minor threshold tuning based on 15-test data. + +5. **Score normalization** - Ensure HN items don't systematically crowd out other sources in the merged ranking. + +**New improvements to discover from 15-test data:** +- Regressions introduced by CROSS changes +- Edge cases where Base or HN outperforms CROSS +- Topic-specific tuning opportunities + +## Acceptance Criteria + +- [x] All 15 JSON result files saved and non-empty +- [x] All 15 runs use identical source routing (verified via --diagnose) +- [x] __pycache__ cleaned between every git checkout +- [x] Comparison summary table covers all 15 runs +- [x] YouTube matched-item analysis for at least 3 topics (all 5 done) +- [x] Cross-source link quality spot-check for CROSS runs +- [x] Clear version verdict with supporting data +- [x] CROSS improvement plan with specific code changes +- [x] All results appended to the analysis document for the user to review + +## Technical Considerations + +- **__pycache__ poisoning**: Python caches bytecode. Switching git commits without clearing `__pycache__` means old code runs even after checkout. MUST clean between every checkout. +- **API rate limiting**: 15 runs hitting Reddit, X, YouTube, HN APIs. The `--quick` mode and natural ~30-90s per run provide spacing, but monitor for 429s in logs. +- **YouTube non-determinism**: yt-dlp search results shift over time. Topic-sequential ordering minimizes this by running all 3 versions of the same topic within ~3-5 minutes. +- **X search session**: Bird CLI depends on browser cookies. If session expires mid-run, X results degrade silently. Check X item counts across runs. +- **`--quick` limitation**: Results reflect Phase 1 only (8-12 items per source). Full pipeline with `--deep` might show different patterns. Document this caveat. + +## Sources & References + +- Previous analysis: [docs/plans/2026-02-25-analysis-cross-source-comparison-plan.md](docs/plans/2026-02-25-analysis-cross-source-comparison-plan.md) +- Implementation plan: [docs/plans/2026-02-25-feat-youtube-relevance-and-cross-source-linking-plan.md](docs/plans/2026-02-25-feat-youtube-relevance-and-cross-source-linking-plan.md) +- Existing comparison harness: [scripts/test-v1-vs-v2.sh](scripts/test-v1-vs-v2.sh) +- Scoring weights: [scripts/lib/score.py](scripts/lib/score.py) - 45% relevance + 25% recency + 30% engagement diff --git a/docs/plans/2026-03-02-chore-open-pr-triage-plan.md b/docs/plans/2026-03-02-chore-open-pr-triage-plan.md new file mode 100644 index 0000000..a1631ef --- /dev/null +++ b/docs/plans/2026-03-02-chore-open-pr-triage-plan.md @@ -0,0 +1,201 @@ +--- +title: "chore: open PR triage - build or close decision for all 5 open PRs" +type: chore +status: active +date: 2026-03-02 +--- + +# Open PR Triage + +Decision record for each of the 5 open PRs on `mvanhorn/last30days-skill`. +For each: build/merge, cherry-pick, or close with explanation. + +--- + +## PR #26 - Add HN, YouTube, and Product Hunt sources + --search flag +**Author:** wkbaran | **Size:** +3402/-49 | **Date:** 2026-02-15 + +### What it adds +- Hacker News source (Algolia API, no auth) +- YouTube source (YouTube Data API v3, optional key) +- Product Hunt source (GraphQL API, optional key) +- `--search=SOURCES` flag (e.g. `--search=reddit,hn,yt`) +- 58 new tests + +### What's already on main +- **HN:** ✅ already merged (`scripts/lib/hackernews.py`) +- **YouTube:** ✅ already merged (`scripts/lib/youtube_yt.py`) +- **Product Hunt:** ❌ NOT on main +- **`--search` flag:** ❌ NOT on main + +### Decision: CHERRY-PICK (`--search` flag only, skip Product Hunt) + +The HN and YouTube modules in this PR are earlier versions than what's on +main. We should NOT take those. Product Hunt is not a priority source. +The one missing piece worth landing: + +**`--search=SOURCES` flag** - lets callers (including agent mode) specify +which sources to run. Useful for `--search=hn,polymarket` or +`--search=reddit,x` focus runs. + +### Plan +- Cherry-pick just `tests/test_search_flag.py` and the `--search` argument + wiring in `scripts/last30days.py` +- Skip `scripts/lib/producthunt.py` and all PH-related code entirely +- Do NOT take their `hackernews.py`, `youtube.py`, `render.py`, `normalize.py`, + `schema.py` - our versions are newer +- Update SKILL.md to document `--search` flag +- Comment on PR thanking wkbaran - his HN/YouTube work was an inspiration + for the sources we ended up building. Acknowledge that specifically. + +### Acceptance Criteria +- [ ] `python3 scripts/last30days.py "AI coding tools" --search=hn,reddit` runs only HN + Reddit +- [ ] `python3 scripts/last30days.py "test" --search=x,web` skips Reddit +- [ ] `test_search_flag.py` passes (adapt to our test patterns, no PH references) +- [ ] SKILL.md updated with `--search` flag docs +- [ ] Sync via `bash scripts/sync.sh` + +--- + +## PR #24 - Adding Codex compatibility +**Author:** el-analista | **Size:** +228/-29 | **Date:** 2026-02-11 + +### What it adds +- `agents/openai.yaml` Codex discovery metadata +- Portable script path resolution in SKILL.md (multi-install-path loop) +- Platform-neutral UI text (`assistant` instead of Claude-specific wording) +- `LAST30DAYS_CACHE_DIR` env override in `scripts/lib/cache.py` +- `LAST30DAYS_OUTPUT_DIR` env override in `scripts/lib/render.py` +- X/Bird query noise stripping + last-chance retry in `scripts/lib/bird_x.py` +- New tests: `test_bird_x.py`, `test_cache.py`, `test_render.py` + +### What's already on main +- **Codex auth:** ✅ already merged (via PR #38) +- **Multi-path script resolution:** ✅ already in SKILL.md (the `for dir in` loop) +- **Platform-neutral wording:** ❌ NOT systematically applied +- **Cache/output dir overrides:** ❌ NOT on main +- **Bird X improvements:** ✅ partially (we have other bird_x fixes but may be missing noise stripping + retry) + +### Decision: REVIEW AND PARTIAL CHERRY-PICK + +The env var overrides (`LAST30DAYS_CACHE_DIR`, `LAST30DAYS_OUTPUT_DIR`) are +genuinely useful for sandboxed/containerized Codex environments. The bird_x +noise stripping and last-chance retry may improve X search quality and are +low-risk additions. + +The platform-neutral wording change ("assistant" instead of Claude references) +should be skipped — SKILL.md is Claude-specific by design. + +### Plan +- Read the diff carefully against our current `cache.py`, `render.py`, `bird_x.py` +- Cherry-pick the `LAST30DAYS_CACHE_DIR` and `LAST30DAYS_OUTPUT_DIR` env overrides +- Cherry-pick the bird_x noise stripping + retry logic if it doesn't conflict + with our existing bird_x changes +- Skip `agents/openai.yaml` — we have our own multi-path resolution +- Skip platform-neutral wording changes +- Comment on PR thanking contributor and explaining what landed + +### Acceptance Criteria +- [ ] `LAST30DAYS_CACHE_DIR=/tmp/test python3 scripts/last30days.py "test" --mock` writes cache to /tmp/test +- [ ] `LAST30DAYS_OUTPUT_DIR=/tmp/out python3 scripts/last30days.py "test" --mock` writes output to /tmp/out +- [ ] Existing tests pass after cherry-pick +- [ ] `test_cache.py` and `test_render.py` adapted and passing + +--- + +## PR #14 - Simplify to WebSearch-first, make API keys optional +**Author:** thangman1 | **Size:** +97/-168 | **Date:** 2026-02-01 + +### What it does +Removes the Reddit/X Python search engine as the primary data source. +Repositions Claude Code's built-in WebSearch as the "default" mode. +Strips engagement metrics (upvotes, likes, repost counts) from output. +Removes mode detection logic (Full Mode / Partial Mode / Web-Only Mode). + +### Decision: CLOSE - do not merge + +This PR inverts the core value proposition of last30days. The skill's +differentiation is **real engagement data** from Reddit threads and X posts — +upvotes, likes, reposts — that WebSearch cannot provide. Stripping that out +produces a worse tool than just asking Claude to search the web, which anyone +can already do. + +The author's intent (lower barrier to entry, no API key required) is valid, +but the right solution is making HN + Polymarket work without any API key +(they already do), and making OPENAI_API_KEY easier to obtain — not removing +the Reddit/X engine. + +### Action +- Comment on PR explaining why we're closing it +- Acknowledge the valid friction point (API key setup) and point to HN + + Polymarket as the zero-config sources +- Close PR + +--- + +## PR #10 - OpenRouter API integration +**Author:** thetechreviewer | **Size:** +1029/-16 | **Date:** 2026-01-28 + +### What it adds +OpenRouter as an alternative to OpenAI for the Reddit discovery search. +Allows using any model available on OpenRouter instead of just OpenAI models. + +### What's already on main +- **`scripts/lib/openrouter_search.py`:** ✅ ALREADY ON MAIN +- **Wired into `scripts/last30days.py`:** ✅ ALREADY ON MAIN (the `backend == "openrouter"` path) + +### Decision: CLOSE - already merged + +The OpenRouter integration that this PR introduced is already on main. It +arrived via internal work that post-dated this PR. The PR is stale. + +### Action +- Comment on PR: "Thanks for this — OpenRouter support is already on main + (landed via internal work). Closing as incorporated." +- Close PR + +--- + +## PR #5 - Add support for Codex auth with OpenAI Responses API +**Author:** jblwilliams | **Size:** +358/-66 | **Date:** 2026-01-27 + +### What it adds +- JWT-based Codex auth with `chatgpt_account_id` +- Codex endpoint routing (`https://chatgpt.com/backend-api/codex/responses`) +- SSE handling for streaming Codex responses +- Typed auth status/source dataclass +- Codex fallback model chain + +### What's already on main +**All of this is already on main.** The Codex auth system landed via PR #37 +(iliaal:codex-auth-merged), which in turn came in with PR #38. This PR (#5) +predates that work and covers the same ground. + +### Decision: CLOSE - already incorporated + +### Action +- Comment on PR: "Thanks for this early work on Codex auth! The same feature + landed on main via PR #37 (from a separate contributor who built on similar + ideas). The JWT decode, Codex endpoint routing, SSE parsing, and typed auth + dataclass are all live. Closing as incorporated." +- Close PR + +--- + +## Summary Table + +| PR | Author | Decision | Reason | +|----|--------|----------|--------| +| #26 | wkbaran | **Cherry-pick** | `--search` flag not on main; HN/YouTube already there; skip Product Hunt | +| #24 | el-analista | **Cherry-pick** | Cache dir overrides + bird_x improvements worth landing; Codex auth already there | +| #14 | thangman1 | **Close** | Removes engagement data, inverts core value proposition | +| #10 | thetechreviewer | **Close** | OpenRouter already on main | +| #5 | jblwilliams | **Close** | Codex auth already on main via PR #37/38 | + +## Implementation Order (if proceeding) + +1. Close PR #5, #10, #14 with comments (no code changes needed) +2. Cherry-pick PR #24 pieces (bird_x + cache/render env overrides) +3. Cherry-pick PR #26 pieces (Product Hunt + `--search` flag) +4. Sync and test +5. Push to upstream diff --git a/docs/plans/2026-03-03-fix-close-commented-github-issues-plan.md b/docs/plans/2026-03-03-fix-close-commented-github-issues-plan.md new file mode 100644 index 0000000..777c86b --- /dev/null +++ b/docs/plans/2026-03-03-fix-close-commented-github-issues-plan.md @@ -0,0 +1,49 @@ +--- +title: Close Commented GitHub Issues and PRs +type: fix +status: active +date: 2026-03-03 +origin: docs/plans/2026-03-03-fix-triage-all-open-github-issues-plan.md +--- + +# Close Commented GitHub Issues and PRs + +All 9 open issues and 2 open PRs on `mvanhorn/last30days-skill` were commented on (2026-03-03) with fix confirmations or responses, but none were actually closed on GitHub. + +## Acceptance Criteria + +- [ ] Close 6 issues where fixes were confirmed (#40, #39, #32, #30, #29, #4) +- [ ] Close PR #26 (superseded - features landed on main) +- [ ] Verify 3 issues remain open (#36, #31, #22) - acknowledged but unresolved +- [ ] Verify PR #24 remains open - explicitly left open per comment + +## Actions + +### Close as fixed (6 issues) + +```bash +gh issue close 40 --repo mvanhorn/last30days-skill --reason completed +gh issue close 39 --repo mvanhorn/last30days-skill --reason completed +gh issue close 32 --repo mvanhorn/last30days-skill --reason completed +gh issue close 30 --repo mvanhorn/last30days-skill --reason completed +gh issue close 29 --repo mvanhorn/last30days-skill --reason completed +gh issue close 4 --repo mvanhorn/last30days-skill --reason completed +``` + +### Close superseded PR (#26) + +```bash +gh pr close 26 --repo mvanhorn/last30days-skill +``` + +### Keep open (no action needed) + +- #36 - SKILL.md flag forwarding (investigating) +- #31 - skills.sh audit scores (needs review) +- #22 - Bird feature requests (backlog) +- PR #24 - Codex compatibility (intentionally left open) + +## Sources + +- **Origin plan:** [docs/plans/2026-03-03-fix-triage-all-open-github-issues-plan.md](2026-03-03-fix-triage-all-open-github-issues-plan.md) +- Commit: `5e5d586` - fix: triage all 16 open GitHub issues diff --git a/docs/plans/2026-03-07-feat-paperclip-marketing-automation-plan.md b/docs/plans/2026-03-07-feat-paperclip-marketing-automation-plan.md new file mode 100644 index 0000000..096eae0 --- /dev/null +++ b/docs/plans/2026-03-07-feat-paperclip-marketing-automation-plan.md @@ -0,0 +1,375 @@ +--- +title: "feat: Paperclip Marketing Automation for last30days" +type: feat +status: active +date: 2026-03-07 +--- + +# Paperclip Marketing Automation for last30days + +## Overview + +Set up a Paperclip "company" that auto-runs marketing for the last30days open-source skill (3,800+ stars). Four agent roles handle daily demo showcases, release announcements, community engagement, and analytics - all using a draft-then-approve workflow through Paperclip's built-in approval gates. + +The killer angle: **last30days markets itself by running itself.** The Content Creator agent runs `/last30days [trending topic] --agent` on hot topics daily, then drafts X threads showing the results. Every post is a live demo. + +## Problem Statement / Motivation + +- All marketing is currently manual - ~60KB of pre-drafted X threads sit in `docs/` unposted +- No automated community monitoring (GitHub issues, contributor shoutouts) +- No metrics tracking (star growth, fork trends, social engagement) +- Solo entrepreneur can't sustain daily content + community management + development +- The tool's best ad is itself running on interesting topics, but that requires daily effort + +## Proposed Solution + +A Paperclip company called **"last30days Marketing"** with 4 agent roles, all draft-then-approve. + +### Company Structure + +``` +last30days Marketing (Company) + Mission: "Grow last30days to 10K GitHub stars through daily demo content, + release marketing, and community engagement" + + Marketing Director (Claude Code agent) + - Sets daily topic priorities + - Reviews draft quality before surfacing to human + - Coordinates cross-agent work + + Content Creator (Python script agent) + - Runs last30days on trending topics daily + - Drafts X showcase threads from the results + - Heartbeat: daily at 8 AM PT + + Release Manager (Bash + Python agent) + - Watches for new git tags on upstream + - Drafts release announcement threads + - Heartbeat: every 6 hours (tag check is cheap) + + Community Manager (Python script agent) + - Monitors GitHub issues/PRs via gh CLI + - Drafts welcome messages for new contributors + - Surfaces popular feature requests + - Heartbeat: every 4 hours + + Analytics Analyst (Python script agent) + - Tracks GitHub stars, forks, traffic + - Tracks X engagement (@slashlast30days) + - Generates weekly digest + - Heartbeat: daily at 11 PM PT (collect), weekly Monday 9 AM (digest) +``` + +### Draft-Then-Approve Flow + +``` +Agent creates draft + -> Saved to ~/Documents/Last30Days/drafts/{agent}/{date}-{slug}.md + -> Paperclip approval gate triggers + -> Matt reviews in Paperclip UI (approve / reject / edit) + -> On approve: Python script posts to X API via tweepy + -> Audit log records everything +``` + +## Technical Approach + +### Phase 1: Infrastructure (Day 1-2) + +Set up Paperclip and external API access. + +**Files to create:** + +- `marketing/paperclip-config.yaml` - Company definition, org chart, budgets +- `marketing/scripts/post_to_x.py` - X API v2 posting via tweepy (draft queue -> X) +- `marketing/scripts/github_monitor.py` - GitHub event monitoring via gh CLI +- `marketing/scripts/metrics_collector.py` - Star/fork/engagement tracking +- `marketing/.env.example` - Required API keys template + +**Setup steps:** + +1. Install Paperclip locally: + ```bash + git clone https://github.com/paperclipai/paperclip + cd paperclip && pnpm install && pnpm dev + ``` + +2. Get X API v2 credentials (developer.x.com) for @slashlast30days + - Need: API key, API secret, Access token, Access token secret + - Permissions: Read + Write (posting) + +3. GitHub token for monitoring (gh auth already configured) + +4. Create the company in Paperclip UI at localhost:3100: + - Company name: "last30days Marketing" + - Mission: "Grow last30days to 10K GitHub stars" + - Monthly budget cap: $50 (mostly API token costs) + +### Phase 2: Content Creator Pipeline (Day 3-5) + +The core marketing engine. Uses last30days to market itself. + +**How it works:** + +1. **Topic Discovery** - `marketing/scripts/discover_topics.py` + - Scrapes trending topics from: Wikipedia pageviews API, HN front page, Reddit r/all + - Filters for topics that would make compelling demos (tech, culture, sports, geopolitics) + - Outputs ranked topic list to `~/Documents/Last30Days/topics-queue.json` + +2. **Research & Draft** - `marketing/scripts/create_showcase.py` + - Picks top topic from queue + - Runs: `python3 scripts/last30days.py "{topic}" --agent --emit=compact --save-dir=~/Documents/Last30Days` + - Reads the saved research output + - Drafts a 1-2 tweet thread in the established style (see Style Guide below) + - Saves draft to `~/Documents/Last30Days/drafts/content/{date}-{slug}.md` + +3. **Approval Gate** - Paperclip surfaces draft for review + - Matt approves/edits in Paperclip UI + - On approve: triggers `post_to_x.py` with the draft content + +**Style Guide (extracted from existing launch threads):** + +``` +Format: Stats-first hook + key finding + tool credit + +Example (from v2.5 launch): +"/last30days Anthropic Pete Hegseth" + +14 Reddit threads. 29 X posts (11,559 likes). 20 YouTube videos (739K views). +5 HN stories. 9 Polymarket markets. + +[Key finding in 2-3 sentences] + +Polymarket: [relevant odds with specific numbers] + +[One-liner showing the tool's value] + +github.com/mvanhorn/last30days-skill +``` + +Rules: +- Always lead with the /last30days command that was run +- Always include the stats line (thread/post/video counts) +- Always end with the GitHub link +- Pick topics people care about RIGHT NOW +- Never use em dashes - use hyphens instead +- Keep threads to 1-2 tweets max for daily showcases +- Save longer threads (3-6 tweets) for releases + +### Phase 3: Release Manager Pipeline (Day 5-6) + +**How it works:** + +1. **Tag Watcher** - `marketing/scripts/watch_releases.py` + - Runs `git -C /Users/mvanhorn/last30days-skill-private fetch upstream --tags` every 6 hours + - Compares local tags vs upstream tags + - On new tag: reads CHANGELOG.md diff since last tag + +2. **Thread Drafter** - `marketing/scripts/draft_release_thread.py` + - Reads changelog diff + release-notes.md + - Drafts a 3-6 tweet thread following the v2.5 launch thread style + - Includes: version number, headline features, demo queries, contributor shoutouts + - Saves to `~/Documents/Last30Days/drafts/releases/{tag}.md` + +3. **Approval Gate** - Same flow as content pipeline + +**Template (from existing docs/v2.5-launch-tweets.md):** + +``` +Tweet 1: Announcement + 3 headline features + GitHub link +Tweet 2-4: One demo per tweet (command + stats + finding) +Tweet 5: Contributor shoutouts +Tweet 6: Install instructions +``` + +### Phase 4: Community Manager Pipeline (Day 6-7) + +**How it works:** + +1. **GitHub Monitor** - `marketing/scripts/github_monitor.py` + - Runs `gh issue list --repo mvanhorn/last30days-skill --state open --json number,title,author,createdAt,labels` + - Runs `gh pr list --repo mvanhorn/last30days-skill --state open --json number,title,author,createdAt` + - Compares against `~/Documents/Last30Days/community/seen.json` to detect new items + - Categorizes: bug report, feature request, question, PR + +2. **Response Drafter** - `marketing/scripts/draft_community_response.py` + - For new issues: drafts a welcome + triage response + - For new PRs: drafts a thank-you + initial review comment + - For merged PRs: drafts a contributor shoutout tweet + - Saves to `~/Documents/Last30Days/drafts/community/{type}-{number}.md` + +3. **Approval Gate** - GitHub responses go through same Paperclip approval + - Approved responses posted via `gh issue comment` or `gh pr comment` + - Shoutout tweets posted via `post_to_x.py` + +**Response templates:** + +```markdown +# New Issue (bug) +Thanks for the report! I'll look into this. Can you share: +- Your OS and Python version +- The exact command you ran +- Whether you have SCRAPECREATORS_API_KEY set + +# New Issue (feature request) +Interesting idea! [1-2 sentences acknowledging the value]. +Adding this to the backlog for consideration. + +# New PR +Thanks for the contribution, @{author}! I'll review this shortly. +[If first-time contributor: Welcome to the project!] + +# Merged PR (tweet) +Shoutout to @{author} for [what they did] in last30days v{version}! +[Brief description of the change and why it matters] +github.com/mvanhorn/last30days-skill/pull/{number} +``` + +### Phase 5: Analytics Pipeline (Day 7-8) + +**How it works:** + +1. **Metrics Collector** - `marketing/scripts/metrics_collector.py` + - Daily: GitHub stars, forks, open issues, open PRs (via `gh api`) + - Daily: X followers, tweet impressions for @slashlast30days (via X API v2) + - Stores in SQLite at `~/Documents/Last30Days/analytics.db` + +2. **Weekly Digest** - `marketing/scripts/weekly_digest.py` + - Runs Monday 9 AM PT + - Generates markdown report with week-over-week changes: + - Star growth (absolute + rate) + - New forks + - Issues opened/closed + - PRs merged + - Top-performing tweets + - Notable community interactions + - Saves to `~/Documents/Last30Days/digests/{date}-weekly.md` + - Optionally sends via email (SendGrid) or Slack webhook + +**Schema for analytics.db:** + +```sql +CREATE TABLE daily_metrics ( + date TEXT PRIMARY KEY, + github_stars INTEGER, + github_forks INTEGER, + github_open_issues INTEGER, + github_open_prs INTEGER, + x_followers INTEGER, + x_impressions INTEGER, + x_engagement_rate REAL +); + +CREATE TABLE tweet_performance ( + tweet_id TEXT PRIMARY KEY, + posted_at TEXT, + type TEXT, -- 'showcase', 'release', 'shoutout' + topic TEXT, + impressions INTEGER, + likes INTEGER, + retweets INTEGER, + replies INTEGER, + link_clicks INTEGER +); +``` + +## Technical Considerations + +### API Costs + +| Service | Usage | Est. Monthly Cost | +|---------|-------|-------------------| +| Paperclip | Self-hosted | $0 | +| X API v2 | Free tier (posting) | $0 | +| ScrapeCreators | ~30 daily research runs | ~$15 | +| GitHub API | gh CLI, already authed | $0 | +| Claude API | Marketing Director agent | ~$20 | +| **Total** | | **~$35/month** | + +### Security + +- API keys stored in `marketing/.env` (gitignored, never committed) +- X API tokens scoped to @slashlast30days only (not personal account) +- Paperclip budget cap prevents runaway spend +- All posts go through human approval gate - no autonomous posting +- GitHub token uses existing `gh auth` session + +### Failure Modes + +- **last30days script fails** - Content Creator skips that day, logs error, tries again tomorrow with new topic +- **X API rate limit** - Queue drafts and retry on next heartbeat cycle +- **Paperclip goes down** - Drafts accumulate in filesystem, nothing posts (safe failure) +- **Bad topic selection** - Marketing Director agent filters topics before research (no politics, no NSFW) +- **Stale approval queue** - If drafts pile up >3 days unapproved, send a nudge notification + +### Topic Selection Criteria + +The discover_topics.py script should filter for topics that: +1. Are trending NOW (Wikipedia pageview spike, HN front page, Reddit r/all) +2. Would produce interesting multi-source results (not too niche, not too broad) +3. Are safe for a developer tool brand (tech, sports, culture, science - avoid divisive politics) +4. Haven't been covered in the last 7 days (dedup against previous showcases) +5. Span different categories to show the tool's versatility (not all tech, not all sports) + +Good examples (from existing launch threads): "Anthropic Pete Hegseth", "Seedance prompting", "Arizona basketball", "Iran war" + +## Acceptance Criteria + +- [ ] Paperclip running locally with "last30days Marketing" company created +- [ ] Content Creator agent runs last30days daily on a trending topic and produces a draft +- [ ] Release Manager agent detects new git tags and drafts announcement threads +- [ ] Community Manager agent detects new GitHub issues/PRs and drafts responses +- [ ] Analytics agent collects daily metrics and generates weekly digest +- [ ] All drafts go through Paperclip approval gate before posting +- [ ] X API integration posts approved drafts to @slashlast30days +- [ ] GitHub responses posted via gh CLI after approval +- [ ] Monthly budget stays under $50 +- [ ] Style of generated tweets matches existing launch thread tone + +## Success Metrics + +- **Content velocity**: 5-7 showcase tweets/week (up from ~0 currently) +- **Star growth**: Track week-over-week acceleration after content starts +- **Time savings**: <5 min/day reviewing drafts vs 30-60 min/day manual marketing +- **Content quality**: Drafts require minimal editing before approval (>80% approved as-is within 2 weeks) + +## Dependencies & Risks + +| Dependency | Risk | Mitigation | +|-----------|------|------------| +| Paperclip stability | Early-stage project, may have bugs | Pin to specific commit, keep simple config | +| X API free tier | May get rate-limited or deprecated | Queue-based posting, daily limits | +| Topic discovery quality | Bad topics = bad demos | Human review in approval gate, topic blocklist | +| Claude API for Marketing Director | Cost could spike | Budget cap in Paperclip, simple prompts | +| ScrapeCreators API | PAYG costs scale with usage | Cap at 1 research run/day for showcases | + +## File Structure + +``` +last30days-skill-private/ + marketing/ + README.md # Setup instructions + .env.example # Required API keys + paperclip-config.yaml # Company definition + scripts/ + discover_topics.py # Trending topic discovery + create_showcase.py # Research + draft showcase tweet + draft_release_thread.py # Release announcement drafter + github_monitor.py # GitHub issue/PR monitor + draft_community_response.py # Community response drafter + post_to_x.py # X API posting (after approval) + metrics_collector.py # Daily metrics collection + weekly_digest.py # Weekly analytics digest + templates/ + showcase.md # Tweet template for daily demos + release.md # Thread template for releases + community_responses.md # Response templates by type +``` + +## Sources & References + +- Paperclip GitHub: github.com/paperclipai/paperclip +- Paperclip docs: paperclip.ing +- Existing launch threads: `docs/v2.5-launch-tweets.md`, `docs/v2.1-tweets.md` +- Existing launch copy: `docs/v2.1-launch-copy.md` +- Planned Last30Days.com: `docs/plans/2026-02-20-feat-last30days-com-trending-topics-plan.md` +- last30days `--agent` mode: SKILL.md line 115-142 (non-interactive output for automation) diff --git a/docs/plans/2026-03-07-review-open-issues-and-prs-triage-plan.md b/docs/plans/2026-03-07-review-open-issues-and-prs-triage-plan.md new file mode 100644 index 0000000..3ea0eaf --- /dev/null +++ b/docs/plans/2026-03-07-review-open-issues-and-prs-triage-plan.md @@ -0,0 +1,261 @@ +--- +title: Triage Open Issues and PRs +type: refactor +status: active +date: 2026-03-07 +--- + +# Triage Open Issues and PRs - last30days-skill + +Consolidated review and action plan for all 6 open items (2 issues, 4 PRs) as of 2026-03-07. + +--- + +## 1. PR #52 - Fix missing metadata files in skill upload bundle + +**Author:** 04cb | **Size:** +0/-1 | **Fixes:** #46 +**File changed:** `.clawhubignore` (removes `*.json` glob) + +### What it does + +The `*.json` pattern in `.clawhubignore` was excluding `.claude-plugin/marketplace.json` and `.claude-plugin/plugin.json` - the metadata files required for ClawHub skill upload. This caused the "Zip file contains path with invalid characters" error reported in #46. + +### Risk analysis + +- Removing `*.json` also includes test fixtures (`fixtures/*.json`) and vendored `package.json` in the bundle. None are sensitive or harmful. +- No `package-lock.json`, `skills-lock.json`, or credential files exist as `.json` in the repo. +- One-line change with zero production code impact. + +### Recommendation: MERGE IMMEDIATELY + +- [x] Merge PR #52 +- [x] Close issue #46 (auto-closes via "Fixes #46") +- [ ] Verify upload works post-merge + +--- + +## 2. Issue #46 - "Claude will not upload as it says thread is invalid" + +**Author:** johnuppard | **Error:** "Zip file contains path with invalid characters" + +### Recommendation: CLOSES WITH PR #52 + +No standalone action needed. PR #52 is the fix. After merge, comment on #46 confirming resolution and ask reporter to verify. + +--- + +## 3. PR #50 - test: add tests for entity_extract module + +**Author:** mark-c4r | **Size:** +167/-0 (tests only) + +### What it does + +Adds `tests/test_entity_extract.py` with 23 test cases covering all 4 public/private functions in `scripts/lib/entity_extract.py`: + +| Function | Cases | Coverage | +|----------|-------|----------| +| `_extract_x_handles` | 8 | author handles, @mentions, generic filtering, case normalization, frequency ranking, edge cases | +| `_extract_x_hashtags` | 5 | basic extraction, multiple tags, frequency ranking, short tag filtering, empty input | +| `_extract_subreddits` | 6 | field extraction, cross-refs in comments, frequency ranking, r/ stripping | +| `extract_entities` | 4 | integration, max limits, empty inputs, return key validation | + +### Code quality assessment + +- All function signatures match the actual module implementation exactly +- All 23 tests validated against the real module - they pass +- Import pattern matches existing tests (`sys.path.insert` + `from lib import`) +- Uses `unittest.TestCase` consistent with all other test files +- No external dependencies, no API calls, no mocking needed (pure functions) +- Test data uses inline dicts matching real Reddit/X response structure + +### Recommendation: MERGE AFTER LOCAL VERIFICATION + +- [x] Pull branch and run `python3 -m pytest tests/test_entity_extract.py -v` (23/23 passed) +- [x] Run full suite `python3 -m pytest tests/` (317/320 passed - 3 pre-existing failures in test_models.py, unrelated) +- [x] Merge + +This is a clean, well-structured community contribution that adds coverage to a previously untested module. + +--- + +## 4. PR #48 - feat: add Xiaohongshu source + Reddit public fallback + +**Author:** YJLi-new | **Size:** +523/-75 | **Files:** 5 modified + +### What it does + +Two features bundled in one PR: + +**A. Xiaohongshu (Little Red Book) source:** +- New `scripts/lib/xiaohongshu_api.py` module for searching via xiaohongshu-mcp HTTP API +- Source alias: `--search xiaohongshu` or `--search xhs` +- Health/login availability checks in `env.py` +- Handles Chinese numeric suffixes (wan/yi) in engagement parsing +- Default API base: `http://host.docker.internal:18060` (Docker-hosted) + +**B. Reddit public JSON fallback:** +- `search_reddit_public()` added to `openai_reddit.py` +- Uses `reddit.com/search/.json` endpoint (no API key required) +- Multiple query strategy (topic, core subject, quoted core) +- Engagement-based relevance heuristic (60% score, 40% comments) +- Supplemental retries correctly gated - only fire when OpenAI auth is present + +### Architecture compliance + +| Pattern | Status | +|---------|--------| +| Three-function module pattern (search/parse/enrich) | Follows existing patterns | +| `DEPTH_CONFIG` with quick/default/deep | Present | +| `_log()` gated on stderr.isatty() | Present | +| env.py availability check | Present with retry logic | +| ThreadPoolExecutor dispatch | Correctly wired with +1 worker | +| Render/UI integration | Consistent with existing sources | + +### Issues found + +1. **Health check duplication** - `is_xiaohongshu_available()` in env.py AND `search_feeds()` both probe login status. Redundant but harmless. +2. **`from_date`/`to_date` accepted but unused** for Xiaohongshu - relies on API-side time bucketing. Acceptable given API limitation. +3. **Over-defensive `setdefault()`** in normalization (id and source_domain already set by search_feeds). Harmless. +4. **Error message format inconsistency** between OpenAI and public Reddit paths. Minor. +5. **`get_available_sources()` docstring** not updated to reflect Reddit always being available now. +6. **No tests included** for new Xiaohongshu module or Reddit public fallback. + +### Security + +- No hardcoded credentials +- xsec_token comes from API response (not user input) +- Docker `host.docker.internal` default is safe for containerized environments +- All URLs use `https://` for public Xiaohongshu + +### Recommendation: MERGE WITH CONDITIONS + +The Reddit public fallback alone makes this worth merging - it makes Reddit work with zero API keys. Xiaohongshu adds value for Chinese-market research. + +**Before merge:** +- [ ] Run full test suite to confirm no regressions +- [ ] Test Reddit public fallback locally: unset OPENAI_API_KEY, run `python3 scripts/last30days.py "test topic" --search reddit --emit=compact` +- [ ] Verify Xiaohongshu gracefully skips when API is unavailable (should show skip message, not crash) +- [ ] Update `get_available_sources()` docstring to reflect Reddit always-available change + +**After merge (follow-up):** +- [ ] Add `tests/test_xiaohongshu.py` with fixture-based tests +- [ ] Add `tests/test_reddit_public.py` for the fallback path +- [ ] Consider extracting health check duplication + +--- + +## 5. PR #47 - feat: add Apify as unified API provider + +**Author:** lapolazzati | **Size:** +1484/-73 | **Files:** 6 new + orchestrator changes + +### What it does + +Adds Apify as a single-token alternative (`APIFY_API_TOKEN`) covering Reddit, X, TikTok, and Instagram. Existing per-source keys take priority; Apify is a transparent fallback. + +**New modules:** +- `apify_client.py` (163 lines) - shared HTTP client for Apify run-sync API +- `apify_reddit.py` (195 lines) - Reddit via `trudax/reddit-scraper` actor +- `apify_x.py` (221 lines) - X via `apidojo/tweet-scraper` actor +- `apify_tiktok.py` (284 lines) - TikTok via `clockworks/tiktok-scraper` actor +- `apify_instagram.py` (288 lines) - Instagram via `apify/instagram-reel-scraper` actor + +**Source routing priority:** +``` +Reddit: OpenAI -> Apify -> None +X: Bird -> xAI -> Apify -> None +TikTok: ScrapeCreators -> Apify -> None +Instagram: ScrapeCreators -> Apify -> None +``` + +### Critical issues + +1. **Actor ID mismatch (BLOCKER):** Code uses different Apify actors than documented in README and plan.md. + + | Source | In Code | In Docs | + |--------|---------|---------| + | Reddit | `trudax/reddit-scraper` | `automation-lab/reddit-scraper` | + | X | `apidojo/tweet-scraper` | `scraper_one/x-posts-search` | + | TikTok | `clockworks/tiktok-scraper` | `epctex/tiktok-search-scraper` | + | Instagram | `apify/instagram-reel-scraper` | matches | + + Users following README instructions will hit wrong actors. + +2. **No tests (BLOCKER):** 1484 lines of new code with zero test coverage. Each Apify module has its own date parsing, relevance scoring, and response normalization - all untested. + +3. **Significant code duplication:** `apify_tiktok.py` and `apify_instagram.py` share nearly identical: + - `_tokenize()` (7 lines) + - `_compute_relevance()` (14 lines) + - `STOPWORDS` set + - `SYNONYMS` dict + - `_extract_core_subject()` (~20 lines) + +4. **Version mismatch:** README says v2.9 but git history shows v2.9.4 on main. Version should be higher. + +5. **plan.md included in commit** - implementation planning doc shouldn't be in the final merge. + +### What's good + +- Source routing logic in env.py is clean and backward-compatible +- Orchestrator dispatch uses consistent `_source` parameter pattern +- Existing source paths are completely untouched +- Timeout scaling is appropriate (90s quick, 150s default, 240s deep) +- Token handling follows security best practices (parameter passing, Bearer auth) + +### Recommendation: REQUEST CHANGES + +This PR is too large and has too many issues for a clean merge. Request the contributor to: + +**Must fix before any merge:** +- [ ] Resolve actor ID mismatches (verify which actors actually work, update code OR docs) +- [ ] Add unit tests for all 5 new modules (at minimum: response normalization, date parsing) +- [ ] Fix version number +- [ ] Remove plan.md from commit + +**Should fix:** +- [ ] Extract shared code from apify_tiktok/apify_instagram into `apify_common.py` +- [ ] Consider splitting into 2 PRs: + - PR A: `apify_client.py` + env.py routing (foundation) + - PR B: Individual source modules + tests (features) + +**Comment template for PR:** +> Thanks for this contribution - the single-token approach is a great idea for simplifying setup. A few things need fixing before we can merge: +> +> 1. The Apify actor IDs in the code don't match the README/plan docs. Can you verify which actors are correct and align code + docs? +> 2. We need test coverage for the new modules - at minimum normalization tests with sample actor responses. +> 3. There's significant duplication between apify_tiktok.py and apify_instagram.py (_tokenize, _compute_relevance, STOPWORDS, SYNONYMS, _extract_core_subject). Could you extract shared code to an apify_common.py? +> 4. Version in README should be higher than v2.9.4 (current main). +> 5. Please remove plan.md from the commit. + +--- + +## 6. Issue #45 - Add support for Gemini CLI + +**Author:** alexferrari88 | **Request:** "Would it be possible to add support for Gemini CLI?" + +### Assessment + +This is a feature request to support Gemini CLI as a runtime alongside Claude Code and Codex. Key considerations: + +- **Scope:** The skill is currently packaged for Claude Code (SKILL.md format) and Codex (~/.codex/skills/). Gemini CLI uses a different skill/extension format. +- **Effort:** Would require understanding Gemini CLI's plugin system, creating a compatible manifest, and potentially adapting the Python execution model. +- **Priority:** Low - Claude Code and Codex are the primary targets and where the user base is. +- **Community:** If the requester wants to contribute, they'd be best positioned to understand Gemini CLI's requirements. + +### Recommendation: ACKNOWLEDGE AND BACKLOG + +- [ ] Respond with: "Thanks for the suggestion! Adding to the backlog. If you're familiar with Gemini CLI's skill/extension format and want to take a crack at it, PRs are welcome. The core Python scripts in `scripts/` are runtime-agnostic - the main work would be creating a Gemini-compatible manifest and deployment path." +- [ ] Add a `help wanted` label +- [ ] Keep open as a backlog item + +--- + +## Priority Summary + +| Priority | Item | Action | Risk | +|----------|------|--------|------| +| 1 | PR #52 (metadata fix) | Merge now | None | +| 2 | Issue #46 (upload error) | Auto-closes with PR #52 | None | +| 3 | PR #50 (entity tests) | Run tests, merge | None | +| 4 | PR #48 (Xiaohongshu + Reddit fallback) | Test locally, merge with minor conditions | Low | +| 5 | Issue #45 (Gemini CLI) | Acknowledge, backlog, label | None | +| 6 | PR #47 (Apify unified) | Request changes (blockers found) | Medium-High | diff --git a/docs/test-results/v1-vs-v2-20260206-231338/v1-SKILL.md b/docs/test-results/v1-vs-v2-20260206-231338/v1-SKILL.md new file mode 100644 index 0000000..4cf4a05 --- /dev/null +++ b/docs/test-results/v1-vs-v2-20260206-231338/v1-SKILL.md @@ -0,0 +1,388 @@ +--- +name: last30days +description: Research a topic from the last 30 days on Reddit + X + Web, become an expert, and write copy-paste-ready prompts for the user's target tool. +argument-hint: "[topic] for [tool]" or "[topic]" +allowed-tools: Bash, Read, Write, AskUserQuestion, WebSearch +--- + +# last30days: Research Any Topic from the Last 30 Days + +Research ANY topic across Reddit, X, and the web. Surface what people are actually discussing, recommending, and debating right now. + +Use cases: +- **Prompting**: "photorealistic people in Nano Banana Pro", "Midjourney prompts", "ChatGPT image generation" → learn techniques, get copy-paste prompts +- **Recommendations**: "best Claude Code skills", "top AI tools" → get a LIST of specific things people mention +- **News**: "what's happening with OpenAI", "latest AI announcements" → current events and updates +- **General**: any topic you're curious about → understand what the community is saying + +## CRITICAL: Parse User Intent + +Before doing anything, parse the user's input for: + +1. **TOPIC**: What they want to learn about (e.g., "web app mockups", "Claude Code skills", "image generation") +2. **TARGET TOOL** (if specified): Where they'll use the prompts (e.g., "Nano Banana Pro", "ChatGPT", "Midjourney") +3. **QUERY TYPE**: What kind of research they want: + - **PROMPTING** - "X prompts", "prompting for X", "X best practices" → User wants to learn techniques and get copy-paste prompts + - **RECOMMENDATIONS** - "best X", "top X", "what X should I use", "recommended X" → User wants a LIST of specific things + - **NEWS** - "what's happening with X", "X news", "latest on X" → User wants current events/updates + - **GENERAL** - anything else → User wants broad understanding of the topic + +Common patterns: +- `[topic] for [tool]` → "web mockups for Nano Banana Pro" → TOOL IS SPECIFIED +- `[topic] prompts for [tool]` → "UI design prompts for Midjourney" → TOOL IS SPECIFIED +- Just `[topic]` → "iOS design mockups" → TOOL NOT SPECIFIED, that's OK +- "best [topic]" or "top [topic]" → QUERY_TYPE = RECOMMENDATIONS +- "what are the best [topic]" → QUERY_TYPE = RECOMMENDATIONS + +**IMPORTANT: Do NOT ask about target tool before research.** +- If tool is specified in the query, use it +- If tool is NOT specified, run research first, then ask AFTER showing results + +**Store these variables:** +- `TOPIC = [extracted topic]` +- `TARGET_TOOL = [extracted tool, or "unknown" if not specified]` +- `QUERY_TYPE = [RECOMMENDATIONS | NEWS | HOW-TO | GENERAL]` + +--- + +## Setup Check + +The skill works in three modes based on available API keys: + +1. **Full Mode** (both keys): Reddit + X + WebSearch - best results with engagement metrics +2. **Partial Mode** (one key): Reddit-only or X-only + WebSearch +3. **Web-Only Mode** (no keys): WebSearch only - still useful, but no engagement metrics + +**API keys are OPTIONAL.** The skill will work without them using WebSearch fallback. + +### First-Time Setup (Optional but Recommended) + +If the user wants to add API keys for better results: + +```bash +mkdir -p ~/.config/last30days +cat > ~/.config/last30days/.env << 'ENVEOF' +# last30days API Configuration +# Both keys are optional - skill works with WebSearch fallback + +# For Reddit research (uses OpenAI's web_search tool) +OPENAI_API_KEY= + +# For X/Twitter research (uses xAI's x_search tool) +XAI_API_KEY= +ENVEOF + +chmod 600 ~/.config/last30days/.env +echo "Config created at ~/.config/last30days/.env" +echo "Edit to add your API keys for enhanced research." +``` + +**DO NOT stop if no keys are configured.** Proceed with web-only mode. + +--- + +## Research Execution + +**IMPORTANT: The script handles API key detection automatically.** Run it and check the output to determine mode. + +**Step 1: Run the research script** +```bash +python3 ~/.claude/skills/last30days/scripts/last30days.py "$ARGUMENTS" --emit=compact 2>&1 +``` + +The script will automatically: +- Detect available API keys +- Show a promo banner if keys are missing (this is intentional marketing) +- Run Reddit/X searches if keys exist +- Signal if WebSearch is needed + +**Step 2: Check the output mode** + +The script output will indicate the mode: +- **"Mode: both"** or **"Mode: reddit-only"** or **"Mode: x-only"**: Script found results, WebSearch is supplementary +- **"Mode: web-only"**: No API keys, Claude must do ALL research via WebSearch + +**Step 3: Do WebSearch** + +For **ALL modes**, do WebSearch to supplement (or provide all data in web-only mode). + +Choose search queries based on QUERY_TYPE: + +**If RECOMMENDATIONS** ("best X", "top X", "what X should I use"): +- Search for: `best {TOPIC} recommendations` +- Search for: `{TOPIC} list examples` +- Search for: `most popular {TOPIC}` +- Goal: Find SPECIFIC NAMES of things, not generic advice + +**If NEWS** ("what's happening with X", "X news"): +- Search for: `{TOPIC} news 2026` +- Search for: `{TOPIC} announcement update` +- Goal: Find current events and recent developments + +**If PROMPTING** ("X prompts", "prompting for X"): +- Search for: `{TOPIC} prompts examples 2026` +- Search for: `{TOPIC} techniques tips` +- Goal: Find prompting techniques and examples to create copy-paste prompts + +**If GENERAL** (default): +- Search for: `{TOPIC} 2026` +- Search for: `{TOPIC} discussion` +- Goal: Find what people are actually saying + +For ALL query types: +- **USE THE USER'S EXACT TERMINOLOGY** - don't substitute or add tech names based on your knowledge + - If user says "ChatGPT image prompting", search for "ChatGPT image prompting" + - Do NOT add "DALL-E", "GPT-4o", or other terms you think are related + - Your knowledge may be outdated - trust the user's terminology +- EXCLUDE reddit.com, x.com, twitter.com (covered by script) +- INCLUDE: blogs, tutorials, docs, news, GitHub repos +- **DO NOT output "Sources:" list** - this is noise, we'll show stats at the end + +**Step 3: Wait for background script to complete** +Use TaskOutput to get the script results before proceeding to synthesis. + +**Depth options** (passed through from user's command): +- `--quick` → Faster, fewer sources (8-12 each) +- (default) → Balanced (20-30 each) +- `--deep` → Comprehensive (50-70 Reddit, 40-60 X) + +--- + +## Judge Agent: Synthesize All Sources + +**After all searches complete, internally synthesize (don't display stats yet):** + +The Judge Agent must: +1. Weight Reddit/X sources HIGHER (they have engagement signals: upvotes, likes) +2. Weight WebSearch sources LOWER (no engagement data) +3. Identify patterns that appear across ALL three sources (strongest signals) +4. Note any contradictions between sources +5. Extract the top 3-5 actionable insights + +**Do NOT display stats here - they come at the end, right before the invitation.** + +--- + +## FIRST: Internalize the Research + +**CRITICAL: Ground your synthesis in the ACTUAL research content, not your pre-existing knowledge.** + +Read the research output carefully. Pay attention to: +- **Exact product/tool names** mentioned (e.g., if research mentions "ClawdBot" or "@clawdbot", that's a DIFFERENT product than "Claude Code" - don't conflate them) +- **Specific quotes and insights** from the sources - use THESE, not generic knowledge +- **What the sources actually say**, not what you assume the topic is about + +**ANTI-PATTERN TO AVOID**: If user asks about "clawdbot skills" and research returns ClawdBot content (self-hosted AI agent), do NOT synthesize this as "Claude Code skills" just because both involve "skills". Read what the research actually says. + +### If QUERY_TYPE = RECOMMENDATIONS + +**CRITICAL: Extract SPECIFIC NAMES, not generic patterns.** + +When user asks "best X" or "top X", they want a LIST of specific things: +- Scan research for specific product names, tool names, project names, skill names, etc. +- Count how many times each is mentioned +- Note which sources recommend each (Reddit thread, X post, blog) +- List them by popularity/mention count + +**BAD synthesis for "best Claude Code skills":** +> "Skills are powerful. Keep them under 500 lines. Use progressive disclosure." + +**GOOD synthesis for "best Claude Code skills":** +> "Most mentioned skills: /commit (5 mentions), remotion skill (4x), git-worktree (3x), /pr (3x). The Remotion announcement got 16K likes on X." + +### For all QUERY_TYPEs + +Identify from the ACTUAL RESEARCH OUTPUT: +- **PROMPT FORMAT** - Does research recommend JSON, structured params, natural language, keywords? THIS IS CRITICAL. +- The top 3-5 patterns/techniques that appeared across multiple sources +- Specific keywords, structures, or approaches mentioned BY THE SOURCES +- Common pitfalls mentioned BY THE SOURCES + +**If research says "use JSON prompts" or "structured prompts", you MUST deliver prompts in that format later.** + +--- + +## THEN: Show Summary + Invite Vision + +**CRITICAL: Do NOT output any "Sources:" lists. The final display should be clean.** + +**Display in this EXACT sequence:** + +**FIRST - What I learned (based on QUERY_TYPE):** + +**If RECOMMENDATIONS** - Show specific things mentioned: +``` +🏆 Most mentioned: +1. [Specific name] - mentioned {n}x (r/sub, @handle, blog.com) +2. [Specific name] - mentioned {n}x (sources) +3. [Specific name] - mentioned {n}x (sources) +4. [Specific name] - mentioned {n}x (sources) +5. [Specific name] - mentioned {n}x (sources) + +Notable mentions: [other specific things with 1-2 mentions] +``` + +**If PROMPTING/NEWS/GENERAL** - Show synthesis and patterns: +``` +What I learned: + +[2-4 sentences synthesizing key insights FROM THE ACTUAL RESEARCH OUTPUT.] + +KEY PATTERNS I'll use: +1. [Pattern from research] +2. [Pattern from research] +3. [Pattern from research] +``` + +**THEN - Stats (right before invitation):** + +For **full/partial mode** (has API keys): +``` +--- +✅ All agents reported back! +├─ 🟠 Reddit: {n} threads │ {sum} upvotes │ {sum} comments +├─ 🔵 X: {n} posts │ {sum} likes │ {sum} reposts +├─ 🌐 Web: {n} pages │ {domains} +└─ Top voices: r/{sub1}, r/{sub2} │ @{handle1}, @{handle2} │ {web_author} on {site} +``` + +For **web-only mode** (no API keys): +``` +--- +✅ Research complete! +├─ 🌐 Web: {n} pages │ {domains} +└─ Top sources: {author1} on {site1}, {author2} on {site2} + +💡 Want engagement metrics? Add API keys to ~/.config/last30days/.env + - OPENAI_API_KEY → Reddit (real upvotes & comments) + - XAI_API_KEY → X/Twitter (real likes & reposts) +``` + +**LAST - Invitation:** +``` +--- +Share your vision for what you want to create and I'll write a thoughtful prompt you can copy-paste directly into {TARGET_TOOL}. +``` + +**Use real numbers from the research output.** The patterns should be actual insights from the research, not generic advice. + +**SELF-CHECK before displaying**: Re-read your "What I learned" section. Does it match what the research ACTUALLY says? If the research was about ClawdBot (a self-hosted AI agent), your summary should be about ClawdBot, not Claude Code. If you catch yourself projecting your own knowledge instead of the research, rewrite it. + +**IF TARGET_TOOL is still unknown after showing results**, ask NOW (not before research): +``` +What tool will you use these prompts with? + +Options: +1. [Most relevant tool based on research - e.g., if research mentioned Figma/Sketch, offer those] +2. Nano Banana Pro (image generation) +3. ChatGPT / Claude (text/code) +4. Other (tell me) +``` + +**IMPORTANT**: After displaying this, WAIT for the user to respond. Don't dump generic prompts. + +--- + +## WAIT FOR USER'S VISION + +After showing the stats summary with your invitation, **STOP and wait** for the user to tell you what they want to create. + +When they respond with their vision (e.g., "I want a landing page mockup for my SaaS app"), THEN write a single, thoughtful, tailored prompt. + +--- + +## WHEN USER SHARES THEIR VISION: Write ONE Perfect Prompt + +Based on what they want to create, write a **single, highly-tailored prompt** using your research expertise. + +### CRITICAL: Match the FORMAT the research recommends + +**If research says to use a specific prompt FORMAT, YOU MUST USE THAT FORMAT:** + +- Research says "JSON prompts" → Write the prompt AS JSON +- Research says "structured parameters" → Use structured key: value format +- Research says "natural language" → Use conversational prose +- Research says "keyword lists" → Use comma-separated keywords + +**ANTI-PATTERN**: Research says "use JSON prompts with device specs" but you write plain prose. This defeats the entire purpose of the research. + +### Output Format: + +``` +Here's your prompt for {TARGET_TOOL}: + +--- + +[The actual prompt IN THE FORMAT THE RESEARCH RECOMMENDS - if research said JSON, this is JSON. If research said natural language, this is prose. Match what works.] + +--- + +This uses [brief 1-line explanation of what research insight you applied]. +``` + +### Quality Checklist: +- [ ] **FORMAT MATCHES RESEARCH** - If research said JSON/structured/etc, prompt IS that format +- [ ] Directly addresses what the user said they want to create +- [ ] Uses specific patterns/keywords discovered in research +- [ ] Ready to paste with zero edits (or minimal [PLACEHOLDERS] clearly marked) +- [ ] Appropriate length and style for TARGET_TOOL + +--- + +## IF USER ASKS FOR MORE OPTIONS + +Only if they ask for alternatives or more prompts, provide 2-3 variations. Don't dump a prompt pack unless requested. + +--- + +## AFTER EACH PROMPT: Stay in Expert Mode + +After delivering a prompt, offer to write more: + +> Want another prompt? Just tell me what you're creating next. + +--- + +## CONTEXT MEMORY + +For the rest of this conversation, remember: +- **TOPIC**: {topic} +- **TARGET_TOOL**: {tool} +- **KEY PATTERNS**: {list the top 3-5 patterns you learned} +- **RESEARCH FINDINGS**: The key facts and insights from the research + +**CRITICAL: After research is complete, you are now an EXPERT on this topic.** + +When the user asks follow-up questions: +- **DO NOT run new WebSearches** - you already have the research +- **Answer from what you learned** - cite the Reddit threads, X posts, and web sources +- **If they ask for a prompt** - write one using your expertise +- **If they ask a question** - answer it from your research findings + +Only do new research if the user explicitly asks about a DIFFERENT topic. + +--- + +## Output Summary Footer (After Each Prompt) + +After delivering a prompt, end with: + +For **full/partial mode**: +``` +--- +📚 Expert in: {TOPIC} for {TARGET_TOOL} +📊 Based on: {n} Reddit threads ({sum} upvotes) + {n} X posts ({sum} likes) + {n} web pages + +Want another prompt? Just tell me what you're creating next. +``` + +For **web-only mode**: +``` +--- +📚 Expert in: {TOPIC} for {TARGET_TOOL} +📊 Based on: {n} web pages from {domains} + +Want another prompt? Just tell me what you're creating next. + +💡 Unlock Reddit & X data: Add API keys to ~/.config/last30days/.env +``` diff --git a/docs/test-results/v1-vs-v2-20260206-231338/v1/1-prompting-techniques-for-chatgpt-for-legal-questio.stderr.txt b/docs/test-results/v1-vs-v2-20260206-231338/v1/1-prompting-techniques-for-chatgpt-for-legal-questio.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/docs/test-results/v1-vs-v2-20260206-231338/v1/1-prompting-techniques-for-chatgpt-for-legal-questio.txt b/docs/test-results/v1-vs-v2-20260206-231338/v1/1-prompting-techniques-for-chatgpt-for-legal-questio.txt new file mode 100644 index 0000000..e69de29 diff --git a/docs/test-results/v1-vs-v2-quick/analysis.md b/docs/test-results/v1-vs-v2-quick/analysis.md new file mode 100644 index 0000000..3b34ea0 --- /dev/null +++ b/docs/test-results/v1-vs-v2-quick/analysis.md @@ -0,0 +1,310 @@ +# V1 vs V2 Comparison Analysis + +**Date:** 2026-02-06 +**Queries tested:** 4 (1 head-to-head, 3 V1-only) +**Scope:** Quick smoke test, not full 17-query matrix + +--- + +## Part 1: Head-to-Head -- "kanye west" (NEWS Query) + +### Dimension-by-Dimension Scoring + +#### 1. Query Parsing Display + +Does it show the `🔍 **{TOPIC}** · {QUERY_TYPE}` line before running tools? + +| Version | Score | Evidence | +|---------|-------|----------| +| V1 | 1 | No parsing display at all. Output starts with "## What I learned:" -- jumps straight into synthesis. No acknowledgment of topic or query type before research. | +| V2 | 1 | No parsing display either. Output starts with "Here's what I found:" then "## What I learned:" -- same problem as V1. | + +**Analysis:** Neither version actually rendered the query parsing display. V2 SKILL.md explicitly requires `🔍 **kanye west** · News` before any tools run, but the agent did not produce it. This is a V2 instruction that failed to land. Both score 1/5. + +Possible cause: The parsing display is supposed to appear *before* tools are called -- it may have been shown during execution but not captured in the final output text. If so, both outputs represent only the post-research synthesis, not the full session. Regardless, based on what is in the output files, neither shows it. + +--- + +#### 2. Source Coverage (Reddit/X/Web counts) + +| Version | Score | Evidence | +|---------|-------|----------| +| V1 | 3 | `Reddit: 0 relevant threads` / `X: 30 posts │ ~10 likes` / `Web: 20+ pages`. Two of three sources returned results. Reddit was zero. | +| V2 | 3 | `Reddit: 0 threads (no results this cycle)` / `X: 29 posts │ 33 likes │ 14 reposts` / `Web: 30+ pages`. Same pattern: two of three returned results. | + +**Analysis:** Nearly identical coverage. Both got zero Reddit results (likely a script/API issue for this topic, not a SKILL.md problem). V2 has slightly more precise X metrics (33 likes, 14 reposts vs. V1's vague "~10 likes"). V2 has more web pages (30+ vs 20+). Both miss the 10+ Reddit threshold for a score of 4+. + +--- + +#### 3. Citation Quality (sparse vs every-sentence) + +| Version | Score | Evidence | +|---------|-------|----------| +| V1 | 2 | No inline citations at all. The body text makes claims ("full-page Wall Street Journal apology," "Hellwatt Festival in Italy") but never attributes them to a specific source. The stats box lists "Washington Post, Billboard, AllHipHop" but the body has zero `per @handle` or `per Rolling Stone` attributions. | +| V2 | 5 | Every bold section ends with a sparse, clean citation. Examples: `"per Rolling Stone"`, `"per The Washington Post"`, `"per Billboard"`, `"per AllHipHop"`, `"per The News International"`. One citation per topic, never chained. Exactly what V2 SKILL.md specifies. | + +**Analysis:** This is the single biggest quality gap between V1 and V2. V1's output reads like a Wikipedia summary -- informative but ungrounded. V2 reads like a researched briefing where every claim has a named source. V2 nails the "sparse citation" rule from its SKILL.md: `"cite 1 source per pattern, short format: 'per @handle' or 'per r/sub'"`. + +V1 quote (no citation): `"He'll headline the new Hellwatt Festival in Italy (July 4-18, 2026)."` +V2 quote (cited): `"Ye is headlining a brand-new festival at the 103,000-capacity RCF Arena in Italy over three weekends from July 4-18, 2026 — his first-ever live concert in Italy, per Billboard."` + +--- + +#### 4. Summary Structure (bold topic headers, organized sections) + +| Version | Score | Evidence | +|---------|-------|----------| +| V1 | 3 | Has a coherent narrative structure with a paragraph of synthesis, then a `**KEY THEMES:**` numbered list. But the opening is a single dense paragraph, not broken into scannable sections with bold headers. | +| V2 | 5 | Each storyline gets its own bold header: `**BULLY Album — March 20, 2026 via Gamma**`, `**Public Apology for Antisemitism**`, `**Hellwatt Festival in Italy**`, `**Health Concerns**`, `**Grammys Ban**`, `**Kim & Lewis Hamilton Buzz**`. Each is a standalone scannable unit with 1-3 sentences. | + +**Analysis:** V2 follows the SKILL.md template exactly: `**{Topic 1}** — [1-2 sentences, per source]`. V1 uses a blob + list approach which is readable but less scannable. V2 is notably better for a user who wants to skim and find the story they care about. + +V1 structure: 1 dense paragraph -> 5-item `KEY THEMES` list +V2 structure: 6 bold topic cards, each self-contained -> no KEY THEMES list (but doesn't need one because the structure itself is the organization) + +--- + +#### 5. Stats Box Format (emoji tree vs plain text) + +| Version | Score | Evidence | +|---------|-------|----------| +| V1 | 4 | Uses `├─` tree format with emoji: `├─ 🟠 Reddit: 0 relevant threads` / `├─ 🔵 X: 30 posts` / `├─ 🌐 Web: 20+ pages` / `└─ Top voices:`. Minor deviation: says "0 relevant threads (filtered out noise)" instead of the V1 SKILL.md template "0 threads (no results this cycle)". Also omits the `🗣️` emoji on the Top voices line. | +| V2 | 5 | Perfect match to V2 SKILL.md template: `├─ 🟠 Reddit: 0 threads (no results this cycle)` / `├─ 🔵 X: 29 posts │ 33 likes │ 14 reposts (via xAI)` / `├─ 🌐 Web: 30+ pages │ rollingstone.com, ...` / `└─ 🗣️ Top voices: @honest30bgfan_ (33 likes), @HipHopCrave_ │ Rolling Stone, Washington Post, Complex`. Includes `(via xAI)` notation, `🗣️` emoji, @handles with engagement counts. | + +**Analysis:** V2 is tighter and matches its template exactly. V1 is close but has minor deviations (custom "filtered out noise" text, missing `🗣️` emoji, no @handles or engagement counts on Top voices). V2's inclusion of actual @handles with like counts (`@honest30bgfan_ (33 likes)`) adds credibility. + +--- + +#### 6. Research Grounding (actual research vs generic knowledge) + +| Version | Score | Evidence | +|---------|-------|----------| +| V1 | 4 | Clearly grounded: mentions specific details like "Wall Street Journal apology (Jan 26, 2026)," "four-month-long manic episode," "frontal-lobe brain injury," "North West collaborated on 'Piercings on My Hand,'" "Monumental Plaza de Toros." These are specific enough to be from research, not pre-training. Minor generic leakage: the "KEY THEMES" list uses editorial framing ("Accountability arc," "Mental health transparency") that feels more like analysis than research extraction. | +| V2 | 5 | Every fact is specific and attributed: "12th studio album," "13-track project features Peso Pluma, Playboi Carti, and Ty Dolla Sign," "earlier leak versions used AI-deepfaked vocals, which have reportedly been re-recorded," "103,000-capacity RCF Arena." The AI-deepfaked vocals detail is a standout -- it is clearly from research, not something a model would know from pre-training. The Kim/Lewis Hamilton item (`"X chatter is heavily focused on Kim Kardashian's relationship with Lewis Hamilton"`) is explicitly sourced from X data, not general knowledge. | + +**Analysis:** Both are well-grounded, but V2 has more "could only come from research" details. The deepfaked vocals story, the exact venue capacity, and the explicit X chatter observation are details that prove the synthesis is from the research output, not hallucinated. + +--- + +#### 7. Prompt Quality (invitation to share vision, not dumping prompts) + +| Version | Score | Evidence | +|---------|-------|----------| +| V1 | 3 | Ends with: `"Want to dive deeper into any of these threads — the apology, the new albums, the Grammys situation, or Bianca Censori? Just tell me what angle you're interested in."` This is a follow-up invitation, but it is NOT the SKILL.md-specified invitation. It is topic-specific and conversational, which is nice, but it does not ask the user to "share your vision for what you want to create." It misses the prompt-generation angle entirely. | +| V2 | 5 | Ends with exactly: `"Share your vision for what you want to create and I'll write a thoughtful prompt you can copy-paste directly into your tool of choice."` This matches the V2 SKILL.md template verbatim. It positions the skill correctly: not a news summarizer but a research-to-prompt pipeline. | + +**Analysis:** V1's closing is friendly but off-brand. It treats the skill as a research tool, not a research-to-prompt tool. V2 correctly frames the next step as "tell me what to create and I'll write the prompt." This is a meaningful difference -- V1 would leave a user thinking they just got a summary, while V2 primes them to get a usable output. + +--- + +### Head-to-Head Scorecard + +| Dimension | V1 | V2 | Winner | +|-----------|----|----|--------| +| 1. Query Parsing Display | 1 | 1 | Tie (both failed) | +| 2. Source Coverage | 3 | 3 | Tie | +| 3. Citation Quality | 2 | 5 | **V2 (+3)** | +| 4. Summary Structure | 3 | 5 | **V2 (+2)** | +| 5. Stats Box Format | 4 | 5 | **V2 (+1)** | +| 6. Research Grounding | 4 | 5 | **V2 (+1)** | +| 7. Prompt Quality (invitation) | 3 | 5 | **V2 (+2)** | +| **TOTAL** | **20/35** | **29/35** | **V2 wins by 9 points** | + +**V2 is clearly better.** The biggest gaps are citation quality (+3) and summary structure (+2). V2's output reads like a professional research briefing; V1's reads like a decent but unstructured summary. + +--- + +## Part 2: V1-Only Outputs Analysis + +### Output 1: "open claw" (GENERAL query) + +**What V1 does well:** +- Strong research grounding. Mentions exact numbers: "145,000+ GitHub stars," "20,000+ forks," "700+ skills," "341 malicious skills." These are clearly from research. +- The KEY PATTERNS section is excellent: 5 well-organized patterns with community quotes (`"I give it sudo and let it configure everything"` vs `"prompt injection is terrifying when you give the bot access to your actual bank account"`). +- Good synthesis of the security vs. enthusiasm tension -- captures the community split accurately. +- Stats box uses the emoji tree format correctly with `├──` (though note: uses double-dash `──` instead of single `─`, minor inconsistency). + +**What V1 is missing (per V2 SKILL.md features):** +- No query parsing display (`🔍 **open claw** · General`). +- No inline citations in the body text. The 5 KEY PATTERNS have no `per @handle` or `per r/sub` attribution. Which Reddit thread said "I give it sudo"? Which X post raised the security concern? We do not know. +- The stats box says `├── 🟠 Reddit: 25 threads │ ~750+ upvotes` -- the tilde and plus are imprecise. V2 SKILL.md wants exact parsed numbers. +- Top voices line lists subreddits and handles but no engagement counts: `@grok, @Starlink` -- are these the highest-engagement handles? No like counts shown. +- No bold topic headers in the body -- it is a single paragraph followed by a numbered list, not the `**{Topic}** — sentence, per source` format V2 requires. + +**V1 Score (estimated):** 22/35 + +--- + +### Output 2: "nano banana pro prompting" (PROMPTING query) + +**What V1 does well:** +- Correctly identifies two prompting styles (JSON structured vs. natural language "Creative Director") and explains when each works best. This is excellent PROMPTING-type synthesis. +- KEY PATTERNS are specific and actionable: "85mm lens at f/1.8," "three-point lighting with key at 45 degrees," "text rendering works -- keep text under 3 words for best results (75% success rate)." These are concrete tips a user can apply immediately. +- Research grounding is strong: cites specific upvote counts ("149-259 upvotes"), subreddit names (`r/nanobanana2pro`), and the Google AI blog. +- The invitation correctly targets Nano Banana Pro: `"Share your vision for what you want to create and I'll write a thoughtful prompt you can copy-paste directly into Nano Banana Pro."` + +**What V1 is missing (per V2 SKILL.md features):** +- No query parsing display. +- Stats box uses plain text dashes: `- 🟠 Reddit: 5 threads | 638 upvotes | 66 comments` instead of the tree format `├─ 🟠 Reddit:`. Uses `|` pipe instead of `│` box-drawing character. V2 SKILL.md explicitly says: "NEVER use plain text dashes (-) or pipe (|). ALWAYS use ├─ └─ │ and the emoji." +- No inline body citations. KEY PATTERNS mention Reddit upvote ranges but no specific `per @handle` attributions. +- Missing `✅ All agents reported back!` header -- just says "All agents reported back!" without the checkmark. +- Body structure is paragraph + numbered list, not bold topic headers. + +**V1 Score (estimated):** 23/35 (slightly higher than open claw due to better actionability) + +--- + +### Output 3: "how to best setup clawdbot" (HOW-TO query) + +**What V1 does well:** +- This is the best V1 output of the batch. It goes beyond synthesis and actually delivers a **Quick-Start guide** with numbered steps, a **Security Hardening** checklist, and a **Budget Option** -- all grounded in research. +- Excellent research grounding: `"per @shynxbt: Use a free AWS VPS + Claude Haiku model + Telegram bot = fully functional for $0"` -- this is an actual citation with an @handle! +- Specific, actionable recommendations: exact commands (`curl -fsSL https://clawd.bot/install.sh | bash`), specific model recommendations (Claude Opus 4.5 for best results, GLM 4.7 Flash for local), specific channel advice (Telegram first, WhatsApp QR code fails). +- Stats box is correct emoji tree format with engagement counts: `@aashatwt (452 likes), @recap_david (329 likes)`. +- Captures the naming confusion accurately: "Clawdbot -> Moltbot -> OpenClaw." + +**What V1 is missing (per V2 SKILL.md features):** +- No query parsing display. +- Body text has no inline citations except the Budget Option section. The 5 KEY PATTERNS have no `per @handle` attribution. +- Bold topic headers are used only in the Quick-Start and Security sections, not in the KEY PATTERNS or intro. +- The output delivers the "answer" directly (setup guide) rather than waiting for the user's vision and offering to write a prompt. For a HOW-TO query this might be the right call, but it skips the SKILL.md flow of "show research -> invite vision -> write prompt." + +**V1 Score (estimated):** 26/35 (best of the V1 outputs) + +--- + +### Patterns Across All V1 Outputs + +**Consistent strengths:** +1. Research grounding is solid across all three. V1 does not hallucinate -- the facts are clearly from the research output, not pre-training. +2. KEY PATTERNS lists are consistently useful and actionable. +3. Stats boxes are present in all outputs (though formatting varies). +4. The invitation/closing line is present in all outputs. + +**Consistent weaknesses:** +1. **No query parsing display** in any output (0 for 4, including Kanye West). +2. **No inline citations** in the body text (except one @handle in the clawdbot output). The research feels real but is unattributed. +3. **Stats box formatting is inconsistent.** Open claw uses `├──` (double dash), nano banana pro uses `- 🟠` (plain dash + pipe), clawdbot uses `├─` (correct). Three different formats in three outputs. +4. **Body structure defaults to paragraph + numbered list** instead of bold topic headers. Only clawdbot partially uses bold headers (in the guide section, not the research section). +5. **No `(via Bird/xAI)` notation** on X stats in any output. + +--- + +## Part 3: SKILL.md Feature Diff + +### Features in V2 but NOT V1 + +| Feature | V2 Lines | Impact | +|---------|----------|--------| +| **Query parsing display** (`🔍 **{TOPIC}** · {QUERY_TYPE}`) | 40-53 | HIGH -- confirms to user the skill understood their request before spending time on research. | +| **Sparse citation rules** with BAD/GOOD examples | 186-193 | HIGH -- this is the #1 quality differentiator in the Kanye head-to-head. `"per @handle"` format, never chain multiple citations. | +| **Bold topic headers** template (`**{Topic 1}** — [1-2 sentences, per source]`) | 195-208 | HIGH -- makes output scannable. | +| **Strict stats template** with "NEVER use plain text dashes" instruction | 217-230 | MEDIUM -- prevents the formatting inconsistency seen across V1 outputs. | +| **RECOMMENDATIONS source attribution** (each item MUST have Sources: line with @handles) | 178-182 | MEDIUM -- only affects RECOMMENDATIONS queries. | +| **Reddit 0 results handling** (explicit instruction for what to write) | 229 | LOW -- edge case, but prevents ad-hoc text like V1's "filtered out noise." | +| **Bird CLI / xAI notation** in stats | 223 | LOW -- cosmetic transparency about data source. | +| **Step 2 phrasing: "DO WEBSEARCH WHILE SCRIPT RUNS"** | 71-73 | LOW -- execution optimization, no output impact. | + +### Features in V1 but NOT V2 + +| Feature | V1 Lines | Impact | Should Restore? | +|---------|----------|--------|-----------------| +| **Use cases block** (4 examples in intro) | 12-17 | LOW | No | +| **Setup Check section** (3 modes, bash script, "keys are OPTIONAL") | 50-78 | MEDIUM for new users | Yes, for public release | +| **BAD/GOOD synthesis anti-pattern examples** | 172-191 | MEDIUM-HIGH | YES | +| **Self-check instruction** ("Re-read your 'What I learned' section...") | 269 | MEDIUM | YES | +| **Quality Checklist** (5-point checklist before delivering prompt) | 306-324 | HIGH | YES | +| **Prompt format anti-pattern** ("Research says JSON but you write prose") | 302 | MEDIUM | YES | +| **"IF USER ASKS FOR MORE OPTIONS"** section | 327-329 | LOW-MEDIUM | YES | +| **Web-only mode stats template + promo** | 248-259 | MEDIUM for no-key users | For public release | +| **TARGET_TOOL question template** (4 options) | 272-280 | LOW | No | +| **Context Memory: explicit "don't re-search" instructions** | 342-358 | MEDIUM | YES | +| **Output footer emoji + engagement counts** | 366-380 | LOW | YES | + +### Features in BOTH (Shared) + +| Feature | Notes | +|---------|-------| +| Parse User Intent (TOPIC, TARGET_TOOL, QUERY_TYPE) | Same 4 query types, same detection logic | +| "Don't ask about tool before research" rule | Identical | +| Research script execution command | Same `python3` command | +| WebSearch queries by QUERY_TYPE | Same search strategies | +| "Use user's exact terminology" instruction | V2 shorter but same intent | +| Judge Agent synthesis logic | Same 5-step weighting process | +| "Ground in actual research" instruction | Same core instruction, V1 has more examples | +| RECOMMENDATIONS: extract specific names | Same logic | +| Prompt format matching | Same instruction | +| Wait for user's vision | Same | +| Write ONE perfect prompt | Same structure | +| Context Memory | V2 shorter version | +| Output summary footer | Both have it, V1 has emoji | +| Depth options (quick/default/deep) | Same | +| "After each prompt: Stay in Expert Mode" | Same | + +### Overall Assessment + +**V2 is a clear upgrade in output formatting and citation quality.** The three features V2 adds (query parsing display, sparse citation rules, bold topic headers) directly address the three biggest weaknesses seen across all V1 outputs. The Kanye West head-to-head proves it: V2 scores 29/35 vs V1's 20/35. + +**However, V2 dropped several quality guardrails from V1** that do not affect formatting but affect *correctness*: the self-check instruction, the anti-pattern examples, the quality checklist for prompts, and the "don't re-search" context memory rule. These are cheap to restore (under 25 lines total) and protect against subtle failure modes that may not show up in a 1-query test but will appear over dozens of uses. + +--- + +## Part 4: Verdict + +### Ship V2 or Not? + +**Ship V2 -- but restore the guardrails first.** + +V2 is unambiguously better on every formatting dimension. The citation quality improvement alone (V1: 2/5 -> V2: 5/5) makes it worth shipping. The bold topic headers and strict stats template fix the inconsistency problems visible across all V1 outputs. + +But V2 dropped 6 guardrail features from V1 that cost almost nothing to include and protect against real failure modes. These should be restored before V2 goes public. + +### Remaining Gaps + +**Must fix before shipping (affects correctness):** + +1. **Restore the quality checklist for prompts.** This is the test plan's #1 priority item. V1 had a 5-point checklist; V2 reduced it to one line. The checklist is what makes prompts feel polished -- it is the "that's a great prompt" mechanism. Add 8 lines. + +2. **Restore BAD/GOOD anti-pattern examples.** V2 says "ground in actual research" but does not show what *bad* grounding looks like. V1's ClawdBot/Claude Code conflation example is exactly the kind of concrete negative example that prevents real failures. Add 5 lines. + +3. **Restore self-check instruction.** One sentence: "Re-read your 'What I learned' section -- does it match what the research ACTUALLY says?" Zero cost, catches hallucination. Add 2 lines. + +4. **Restore "don't re-search" context memory rule.** V2 only says "only do new research if user asks about a DIFFERENT topic." V1 explicitly bans re-searching and tells the agent to answer from existing research. Add 3 lines. + +**Should fix (polish):** + +5. Restore prompt format anti-pattern ("Research says JSON but you write prose"). Add 2 lines. +6. Restore "IF USER ASKS FOR MORE OPTIONS" section. Add 2 lines. +7. Add emoji + engagement counts back to the output summary footer. Edit 3 lines. + +**Skip for now:** + +8. Setup Check section -- add back for public release, not needed for execution. +9. Web-only mode stats template -- lower priority, most testers have API keys. +10. TARGET_TOOL question template -- agent handles this naturally. + +### Query Parsing Display: Investigate + +Both V1 and V2 scored 1/5 on query parsing display. V2 has the feature in its SKILL.md but the agent did not render it in the captured output. This could mean: +- The display was shown during execution but not captured (likely -- it appears before tools run, and the output files may only contain post-research content). +- The instruction is not strong enough and the agent skips it. + +**Recommendation:** Verify in a live session whether the parsing display actually appears. If it does not, strengthen the instruction (e.g., "This line MUST be the first thing you output, before any tool calls"). + +### Total Effort + +Restoring all 7 priority items: approximately 25 lines added to V2 SKILL.md. Under 15 minutes of work. The V2 formatting wins are substantial and proven; the V1 guardrails are small and proven. Combining both produces the best version. + +### Final Score Summary + +| | V1 (Kanye) | V2 (Kanye) | Delta | +|--|-----------|-----------|-------| +| Total | 20/35 | 29/35 | **V2 +9** | + +| | V1 (Open Claw) | V1 (Nano Banana) | V1 (Clawdbot) | V1 Average | +|--|---------------|-----------------|--------------|------------| +| Estimated Total | 22/35 | 23/35 | 26/35 | **23.7/35** | + +V2 at 29/35 beats every V1 output, including V1's best (clawdbot at 26/35). + +**Decision: Ship V2 with guardrails restored.** diff --git a/docs/test-results/v1-vs-v2-quick/v1-SKILL.md b/docs/test-results/v1-vs-v2-quick/v1-SKILL.md new file mode 100644 index 0000000..4cf4a05 --- /dev/null +++ b/docs/test-results/v1-vs-v2-quick/v1-SKILL.md @@ -0,0 +1,388 @@ +--- +name: last30days +description: Research a topic from the last 30 days on Reddit + X + Web, become an expert, and write copy-paste-ready prompts for the user's target tool. +argument-hint: "[topic] for [tool]" or "[topic]" +allowed-tools: Bash, Read, Write, AskUserQuestion, WebSearch +--- + +# last30days: Research Any Topic from the Last 30 Days + +Research ANY topic across Reddit, X, and the web. Surface what people are actually discussing, recommending, and debating right now. + +Use cases: +- **Prompting**: "photorealistic people in Nano Banana Pro", "Midjourney prompts", "ChatGPT image generation" → learn techniques, get copy-paste prompts +- **Recommendations**: "best Claude Code skills", "top AI tools" → get a LIST of specific things people mention +- **News**: "what's happening with OpenAI", "latest AI announcements" → current events and updates +- **General**: any topic you're curious about → understand what the community is saying + +## CRITICAL: Parse User Intent + +Before doing anything, parse the user's input for: + +1. **TOPIC**: What they want to learn about (e.g., "web app mockups", "Claude Code skills", "image generation") +2. **TARGET TOOL** (if specified): Where they'll use the prompts (e.g., "Nano Banana Pro", "ChatGPT", "Midjourney") +3. **QUERY TYPE**: What kind of research they want: + - **PROMPTING** - "X prompts", "prompting for X", "X best practices" → User wants to learn techniques and get copy-paste prompts + - **RECOMMENDATIONS** - "best X", "top X", "what X should I use", "recommended X" → User wants a LIST of specific things + - **NEWS** - "what's happening with X", "X news", "latest on X" → User wants current events/updates + - **GENERAL** - anything else → User wants broad understanding of the topic + +Common patterns: +- `[topic] for [tool]` → "web mockups for Nano Banana Pro" → TOOL IS SPECIFIED +- `[topic] prompts for [tool]` → "UI design prompts for Midjourney" → TOOL IS SPECIFIED +- Just `[topic]` → "iOS design mockups" → TOOL NOT SPECIFIED, that's OK +- "best [topic]" or "top [topic]" → QUERY_TYPE = RECOMMENDATIONS +- "what are the best [topic]" → QUERY_TYPE = RECOMMENDATIONS + +**IMPORTANT: Do NOT ask about target tool before research.** +- If tool is specified in the query, use it +- If tool is NOT specified, run research first, then ask AFTER showing results + +**Store these variables:** +- `TOPIC = [extracted topic]` +- `TARGET_TOOL = [extracted tool, or "unknown" if not specified]` +- `QUERY_TYPE = [RECOMMENDATIONS | NEWS | HOW-TO | GENERAL]` + +--- + +## Setup Check + +The skill works in three modes based on available API keys: + +1. **Full Mode** (both keys): Reddit + X + WebSearch - best results with engagement metrics +2. **Partial Mode** (one key): Reddit-only or X-only + WebSearch +3. **Web-Only Mode** (no keys): WebSearch only - still useful, but no engagement metrics + +**API keys are OPTIONAL.** The skill will work without them using WebSearch fallback. + +### First-Time Setup (Optional but Recommended) + +If the user wants to add API keys for better results: + +```bash +mkdir -p ~/.config/last30days +cat > ~/.config/last30days/.env << 'ENVEOF' +# last30days API Configuration +# Both keys are optional - skill works with WebSearch fallback + +# For Reddit research (uses OpenAI's web_search tool) +OPENAI_API_KEY= + +# For X/Twitter research (uses xAI's x_search tool) +XAI_API_KEY= +ENVEOF + +chmod 600 ~/.config/last30days/.env +echo "Config created at ~/.config/last30days/.env" +echo "Edit to add your API keys for enhanced research." +``` + +**DO NOT stop if no keys are configured.** Proceed with web-only mode. + +--- + +## Research Execution + +**IMPORTANT: The script handles API key detection automatically.** Run it and check the output to determine mode. + +**Step 1: Run the research script** +```bash +python3 ~/.claude/skills/last30days/scripts/last30days.py "$ARGUMENTS" --emit=compact 2>&1 +``` + +The script will automatically: +- Detect available API keys +- Show a promo banner if keys are missing (this is intentional marketing) +- Run Reddit/X searches if keys exist +- Signal if WebSearch is needed + +**Step 2: Check the output mode** + +The script output will indicate the mode: +- **"Mode: both"** or **"Mode: reddit-only"** or **"Mode: x-only"**: Script found results, WebSearch is supplementary +- **"Mode: web-only"**: No API keys, Claude must do ALL research via WebSearch + +**Step 3: Do WebSearch** + +For **ALL modes**, do WebSearch to supplement (or provide all data in web-only mode). + +Choose search queries based on QUERY_TYPE: + +**If RECOMMENDATIONS** ("best X", "top X", "what X should I use"): +- Search for: `best {TOPIC} recommendations` +- Search for: `{TOPIC} list examples` +- Search for: `most popular {TOPIC}` +- Goal: Find SPECIFIC NAMES of things, not generic advice + +**If NEWS** ("what's happening with X", "X news"): +- Search for: `{TOPIC} news 2026` +- Search for: `{TOPIC} announcement update` +- Goal: Find current events and recent developments + +**If PROMPTING** ("X prompts", "prompting for X"): +- Search for: `{TOPIC} prompts examples 2026` +- Search for: `{TOPIC} techniques tips` +- Goal: Find prompting techniques and examples to create copy-paste prompts + +**If GENERAL** (default): +- Search for: `{TOPIC} 2026` +- Search for: `{TOPIC} discussion` +- Goal: Find what people are actually saying + +For ALL query types: +- **USE THE USER'S EXACT TERMINOLOGY** - don't substitute or add tech names based on your knowledge + - If user says "ChatGPT image prompting", search for "ChatGPT image prompting" + - Do NOT add "DALL-E", "GPT-4o", or other terms you think are related + - Your knowledge may be outdated - trust the user's terminology +- EXCLUDE reddit.com, x.com, twitter.com (covered by script) +- INCLUDE: blogs, tutorials, docs, news, GitHub repos +- **DO NOT output "Sources:" list** - this is noise, we'll show stats at the end + +**Step 3: Wait for background script to complete** +Use TaskOutput to get the script results before proceeding to synthesis. + +**Depth options** (passed through from user's command): +- `--quick` → Faster, fewer sources (8-12 each) +- (default) → Balanced (20-30 each) +- `--deep` → Comprehensive (50-70 Reddit, 40-60 X) + +--- + +## Judge Agent: Synthesize All Sources + +**After all searches complete, internally synthesize (don't display stats yet):** + +The Judge Agent must: +1. Weight Reddit/X sources HIGHER (they have engagement signals: upvotes, likes) +2. Weight WebSearch sources LOWER (no engagement data) +3. Identify patterns that appear across ALL three sources (strongest signals) +4. Note any contradictions between sources +5. Extract the top 3-5 actionable insights + +**Do NOT display stats here - they come at the end, right before the invitation.** + +--- + +## FIRST: Internalize the Research + +**CRITICAL: Ground your synthesis in the ACTUAL research content, not your pre-existing knowledge.** + +Read the research output carefully. Pay attention to: +- **Exact product/tool names** mentioned (e.g., if research mentions "ClawdBot" or "@clawdbot", that's a DIFFERENT product than "Claude Code" - don't conflate them) +- **Specific quotes and insights** from the sources - use THESE, not generic knowledge +- **What the sources actually say**, not what you assume the topic is about + +**ANTI-PATTERN TO AVOID**: If user asks about "clawdbot skills" and research returns ClawdBot content (self-hosted AI agent), do NOT synthesize this as "Claude Code skills" just because both involve "skills". Read what the research actually says. + +### If QUERY_TYPE = RECOMMENDATIONS + +**CRITICAL: Extract SPECIFIC NAMES, not generic patterns.** + +When user asks "best X" or "top X", they want a LIST of specific things: +- Scan research for specific product names, tool names, project names, skill names, etc. +- Count how many times each is mentioned +- Note which sources recommend each (Reddit thread, X post, blog) +- List them by popularity/mention count + +**BAD synthesis for "best Claude Code skills":** +> "Skills are powerful. Keep them under 500 lines. Use progressive disclosure." + +**GOOD synthesis for "best Claude Code skills":** +> "Most mentioned skills: /commit (5 mentions), remotion skill (4x), git-worktree (3x), /pr (3x). The Remotion announcement got 16K likes on X." + +### For all QUERY_TYPEs + +Identify from the ACTUAL RESEARCH OUTPUT: +- **PROMPT FORMAT** - Does research recommend JSON, structured params, natural language, keywords? THIS IS CRITICAL. +- The top 3-5 patterns/techniques that appeared across multiple sources +- Specific keywords, structures, or approaches mentioned BY THE SOURCES +- Common pitfalls mentioned BY THE SOURCES + +**If research says "use JSON prompts" or "structured prompts", you MUST deliver prompts in that format later.** + +--- + +## THEN: Show Summary + Invite Vision + +**CRITICAL: Do NOT output any "Sources:" lists. The final display should be clean.** + +**Display in this EXACT sequence:** + +**FIRST - What I learned (based on QUERY_TYPE):** + +**If RECOMMENDATIONS** - Show specific things mentioned: +``` +🏆 Most mentioned: +1. [Specific name] - mentioned {n}x (r/sub, @handle, blog.com) +2. [Specific name] - mentioned {n}x (sources) +3. [Specific name] - mentioned {n}x (sources) +4. [Specific name] - mentioned {n}x (sources) +5. [Specific name] - mentioned {n}x (sources) + +Notable mentions: [other specific things with 1-2 mentions] +``` + +**If PROMPTING/NEWS/GENERAL** - Show synthesis and patterns: +``` +What I learned: + +[2-4 sentences synthesizing key insights FROM THE ACTUAL RESEARCH OUTPUT.] + +KEY PATTERNS I'll use: +1. [Pattern from research] +2. [Pattern from research] +3. [Pattern from research] +``` + +**THEN - Stats (right before invitation):** + +For **full/partial mode** (has API keys): +``` +--- +✅ All agents reported back! +├─ 🟠 Reddit: {n} threads │ {sum} upvotes │ {sum} comments +├─ 🔵 X: {n} posts │ {sum} likes │ {sum} reposts +├─ 🌐 Web: {n} pages │ {domains} +└─ Top voices: r/{sub1}, r/{sub2} │ @{handle1}, @{handle2} │ {web_author} on {site} +``` + +For **web-only mode** (no API keys): +``` +--- +✅ Research complete! +├─ 🌐 Web: {n} pages │ {domains} +└─ Top sources: {author1} on {site1}, {author2} on {site2} + +💡 Want engagement metrics? Add API keys to ~/.config/last30days/.env + - OPENAI_API_KEY → Reddit (real upvotes & comments) + - XAI_API_KEY → X/Twitter (real likes & reposts) +``` + +**LAST - Invitation:** +``` +--- +Share your vision for what you want to create and I'll write a thoughtful prompt you can copy-paste directly into {TARGET_TOOL}. +``` + +**Use real numbers from the research output.** The patterns should be actual insights from the research, not generic advice. + +**SELF-CHECK before displaying**: Re-read your "What I learned" section. Does it match what the research ACTUALLY says? If the research was about ClawdBot (a self-hosted AI agent), your summary should be about ClawdBot, not Claude Code. If you catch yourself projecting your own knowledge instead of the research, rewrite it. + +**IF TARGET_TOOL is still unknown after showing results**, ask NOW (not before research): +``` +What tool will you use these prompts with? + +Options: +1. [Most relevant tool based on research - e.g., if research mentioned Figma/Sketch, offer those] +2. Nano Banana Pro (image generation) +3. ChatGPT / Claude (text/code) +4. Other (tell me) +``` + +**IMPORTANT**: After displaying this, WAIT for the user to respond. Don't dump generic prompts. + +--- + +## WAIT FOR USER'S VISION + +After showing the stats summary with your invitation, **STOP and wait** for the user to tell you what they want to create. + +When they respond with their vision (e.g., "I want a landing page mockup for my SaaS app"), THEN write a single, thoughtful, tailored prompt. + +--- + +## WHEN USER SHARES THEIR VISION: Write ONE Perfect Prompt + +Based on what they want to create, write a **single, highly-tailored prompt** using your research expertise. + +### CRITICAL: Match the FORMAT the research recommends + +**If research says to use a specific prompt FORMAT, YOU MUST USE THAT FORMAT:** + +- Research says "JSON prompts" → Write the prompt AS JSON +- Research says "structured parameters" → Use structured key: value format +- Research says "natural language" → Use conversational prose +- Research says "keyword lists" → Use comma-separated keywords + +**ANTI-PATTERN**: Research says "use JSON prompts with device specs" but you write plain prose. This defeats the entire purpose of the research. + +### Output Format: + +``` +Here's your prompt for {TARGET_TOOL}: + +--- + +[The actual prompt IN THE FORMAT THE RESEARCH RECOMMENDS - if research said JSON, this is JSON. If research said natural language, this is prose. Match what works.] + +--- + +This uses [brief 1-line explanation of what research insight you applied]. +``` + +### Quality Checklist: +- [ ] **FORMAT MATCHES RESEARCH** - If research said JSON/structured/etc, prompt IS that format +- [ ] Directly addresses what the user said they want to create +- [ ] Uses specific patterns/keywords discovered in research +- [ ] Ready to paste with zero edits (or minimal [PLACEHOLDERS] clearly marked) +- [ ] Appropriate length and style for TARGET_TOOL + +--- + +## IF USER ASKS FOR MORE OPTIONS + +Only if they ask for alternatives or more prompts, provide 2-3 variations. Don't dump a prompt pack unless requested. + +--- + +## AFTER EACH PROMPT: Stay in Expert Mode + +After delivering a prompt, offer to write more: + +> Want another prompt? Just tell me what you're creating next. + +--- + +## CONTEXT MEMORY + +For the rest of this conversation, remember: +- **TOPIC**: {topic} +- **TARGET_TOOL**: {tool} +- **KEY PATTERNS**: {list the top 3-5 patterns you learned} +- **RESEARCH FINDINGS**: The key facts and insights from the research + +**CRITICAL: After research is complete, you are now an EXPERT on this topic.** + +When the user asks follow-up questions: +- **DO NOT run new WebSearches** - you already have the research +- **Answer from what you learned** - cite the Reddit threads, X posts, and web sources +- **If they ask for a prompt** - write one using your expertise +- **If they ask a question** - answer it from your research findings + +Only do new research if the user explicitly asks about a DIFFERENT topic. + +--- + +## Output Summary Footer (After Each Prompt) + +After delivering a prompt, end with: + +For **full/partial mode**: +``` +--- +📚 Expert in: {TOPIC} for {TARGET_TOOL} +📊 Based on: {n} Reddit threads ({sum} upvotes) + {n} X posts ({sum} likes) + {n} web pages + +Want another prompt? Just tell me what you're creating next. +``` + +For **web-only mode**: +``` +--- +📚 Expert in: {TOPIC} for {TARGET_TOOL} +📊 Based on: {n} web pages from {domains} + +Want another prompt? Just tell me what you're creating next. + +💡 Unlock Reddit & X data: Add API keys to ~/.config/last30days/.env +``` diff --git a/docs/test-results/v1-vs-v2-quick/v1/1-kanye-west.txt b/docs/test-results/v1-vs-v2-quick/v1/1-kanye-west.txt new file mode 100644 index 0000000..60787ec --- /dev/null +++ b/docs/test-results/v1-vs-v2-quick/v1/1-kanye-west.txt @@ -0,0 +1,25 @@ +## What I learned: + +The last 30 days have been one of Kanye West's most significant periods in years. The biggest story is his **full-page Wall Street Journal apology** (Jan 26, 2026) titled "To Those I've Hurt" — a comprehensive open letter addressing his antisemitic remarks and erratic behavior. He attributed his actions to a "four-month-long manic episode" and revealed that a car accident 25 years ago caused a frontal-lobe brain injury that wasn't properly diagnosed until 2023, leading to his bipolar type-1 diagnosis. Multiple sources describe wife **Bianca Censori** as instrumental in getting him to seek treatment during a manic depressive episode. + +On the music side, he's announced **two upcoming albums**: *BULLY* (now scheduled for March 20, 2026) and *IN A PERFECT WORLD*. Daughter **North West** collaborated with him on a track called "Piercings on My Hand." He performed a massive show in Mexico City (Jan 30) at the Monumental Plaza de Toros, and he'll headline the new **Hellwatt Festival** in Italy (July 4-18, 2026). + +The **2026 Grammys** remain contentious — he's reportedly not invited after last year's red carpet drama. Health concerns also surfaced after a January LA outing where he appeared "noticeably bloated and moving with sluggish energy." + +**KEY THEMES:** +1. **Accountability arc** — The WSJ apology is being widely discussed as either genuine growth or calculated PR +2. **Mental health transparency** — Brain injury + bipolar diagnosis framing his past behavior +3. **Bianca Censori's role** — Increasingly seen as a stabilizing force (despite continued provocative public appearances) +4. **Music comeback** — Two albums announced, active touring in Latin America and Europe +5. **Grammy exile** — Still persona non grata at major award shows + +--- +✅ All agents reported back! +├─ 🟠 Reddit: 0 relevant threads (filtered out noise) +├─ 🔵 X: 30 posts │ ~10 likes │ casual fan chatter + Bianca Censori discourse +├─ 🌐 Web: 20+ pages │ washingtonpost.com, billboard.com, allhiphop.com, euronews.com, rap-up.com +└─ Top voices: Washington Post, Billboard, AllHipHop, Euronews, The Mirror + +--- + +Want to dive deeper into any of these threads — the apology, the new albums, the Grammys situation, or Bianca Censori? Just tell me what angle you're interested in. diff --git a/docs/test-results/v1-vs-v2-quick/v1/3-open-claw.txt b/docs/test-results/v1-vs-v2-quick/v1/3-open-claw.txt new file mode 100644 index 0000000..2e59951 --- /dev/null +++ b/docs/test-results/v1-vs-v2-quick/v1/3-open-claw.txt @@ -0,0 +1,24 @@ +## What I learned: + +OpenClaw (formerly ClawdBot, formerly Moltbot) is an open-source autonomous AI agent created by Peter Steinberger that went massively viral in late January 2026 — hitting 145,000+ GitHub stars and 20,000+ forks. It runs locally on your machine, connects to messaging platforms (Telegram, WhatsApp, Discord, Signal, etc.), and uses LLMs like Claude, GPT, or Gemini to actually perform tasks on your computer — file management, scheduling, web browsing, purchases, and more. Think "JARVIS for everyone." + +The community is simultaneously excited and alarmed. The "ClawHub" skill marketplace has 700+ skills but a VirusTotal/Koi Security audit found **341 malicious skills** doing data exfiltration and crypto theft. A high-severity RCE vulnerability was also disclosed. The #1 most downloaded skill on ClawHub was confirmed malware. + +**KEY PATTERNS from the research:** + +1. **Cost is the top complaint** — API usage burns money fast; many users recommend Gemini CLI (free 1,000 req/day) over Claude/GPT for OpenClaw to control spend +2. **Security is a dumpster fire** — Supply chain attacks via ClawHub skills, prompt injection risks when browsing the web, RCE vulnerabilities, and orgs are banning it at the EDR level +3. **Setup is nontrivial** — Even after install, most users get stuck on skills/tools/automation config; the `openclaw onboard` wizard is the recommended path +4. **The skill ecosystem is powerful but dangerous** — 700+ skills available, but no code signing, no real moderation, and active malware campaigns +5. **Community is split** — Enthusiasts love the autonomy ("I give it sudo and let it configure everything"), while security folks are terrified ("prompt injection is terrifying when you give the bot access to your actual bank account") + +--- +✅ All agents reported back! +├── 🟠 Reddit: 25 threads │ ~750+ upvotes │ ~600+ comments +├── 🔵 X: 27 posts │ 1,750+ likes │ 325+ reposts +├── 🌐 Web: 20+ pages │ Wikipedia, Bloomberg, The Register, Hacker News, Nature, VirusTotal, DigitalOcean, Codecademy, Tom's Hardware +└── Top voices: r/openclaw, r/cybersecurity, r/clawdbot, r/ArtificialInteligence │ @grok, @Starlink │ Nature, Bloomberg, ACM + +--- + +Share your vision for what you want to create and I'll write a thoughtful prompt you can copy-paste directly. diff --git a/docs/test-results/v1-vs-v2-quick/v1/4-nano-banana-pro-prompting.txt b/docs/test-results/v1-vs-v2-quick/v1/4-nano-banana-pro-prompting.txt new file mode 100644 index 0000000..95d59ae --- /dev/null +++ b/docs/test-results/v1-vs-v2-quick/v1/4-nano-banana-pro-prompting.txt @@ -0,0 +1,27 @@ +Now I have a comprehensive picture. Let me synthesize everything. + +**What I learned:** + +Nano Banana Pro is fundamentally different from older image models — it's a "thinking" model that understands intent, physics, and composition. The community has converged on two distinct prompting styles, and **both work well** depending on the use case: + +1. **JSON structured prompts** — Dominant on Reddit (r/nanobanana2pro) and X. Power users share elaborate JSON objects with nested fields for subject, appearance, lighting, composition, and style. These produce the most consistent, controlled results for portraits, editorials, and product shots. + +2. **Natural language "Creative Director" prompts** — Recommended by Google's official guide. Full sentences describing the scene as if briefing a photographer. Better for cinematic shots, complex edits, and iterative refinement. + +**KEY PATTERNS I'll use:** +1. **JSON format for maximum control** — The top Reddit posts (149-259 upvotes) all use structured JSON with nested objects: `subject > appearance > hair/skin/expression`, `lighting > key/fill/rim`, `camera > lens/angle/depth_of_field` +2. **Be a Creative Director, not a tag spammer** — Replace "4k, realistic, beautiful" with specific technical directions: "85mm lens at f/1.8", "three-point lighting with key at 45°", "shallow depth of field with bokeh" +3. **Provide context for WHY** — Adding purpose ("for a luxury fashion editorial", "for a high-end gourmet cookbook") helps the model infer appropriate styling, plating, lighting +4. **Edit, don't regenerate** — When 80% is right, ask for the specific change conversationally +5. **Text rendering works** — Keep text under 3 words for best results (75% success rate), specify font style and placement + +--- +All agents reported back! +- 🟠 Reddit: 5 threads | 638 upvotes | 66 comments +- 🔵 X: 29 posts | 312 likes | 117 reposts +- 🌐 Web: 10+ pages | blog.google, dev.to, eweek.com, imagine.art, github.com, leonardo.ai, medium.com +- Top voices: r/nanobanana2pro | @Xmira_belle, @doctorwasif, @chatgptpaglu | Google AI blog, DEV Community + +--- + +Share your vision for what you want to create and I'll write a thoughtful prompt you can copy-paste directly into Nano Banana Pro. diff --git a/docs/test-results/v1-vs-v2-quick/v1/5-how-to-best-setup-clawdbot.txt b/docs/test-results/v1-vs-v2-quick/v1/5-how-to-best-setup-clawdbot.txt new file mode 100644 index 0000000..8b16817 --- /dev/null +++ b/docs/test-results/v1-vs-v2-quick/v1/5-how-to-best-setup-clawdbot.txt @@ -0,0 +1,48 @@ +**What I learned:** + +Clawdbot (now officially renamed **OpenClaw**) is a self-hosted, open-source personal AI assistant that connects to messaging apps (Telegram, WhatsApp, Discord, Slack, Signal, iMessage) and uses LLMs to act as an autonomous agent on your behalf. It's roughly one week old as a public project and the community is moving fast — the name has already changed twice (Clawdbot → Moltbot → OpenClaw). + +**KEY PATTERNS from the research:** + +1. **Security is the #1 concern** — Multiple Reddit threads and guides warn that many setups are wide open to the internet with zero auth. Run it on dedicated/isolated hardware (VM, Mac Mini, NAS), never your main machine. Block dangerous commands via allowlist. Never add it to group chats. + +2. **Use the onboarding wizard, not Docker** — Docker was reported as unreliable (shims, bridges). The single-line install script (`curl -fsSL https://clawd.bot/install.sh | bash`) followed by `openclaw onboard` is the smoothest path. Docker is a "no-go" per multiple users. + +3. **Model choice matters a lot** — Claude Opus 4.5 is recommended for best results (trained to resist prompt injection, ~99% resistance). For budget setups, Claude Haiku works at $0. Local models (Ollama/LM Studio) work but cause hallucination and token issues — GLM 4.7 Flash (30B) was recommended as the best local option. + +4. **Telegram is the easiest channel to start with** — Most guides and users start with a Telegram bot token. WhatsApp QR code pairing fails frequently. Get Telegram working first, then expand. + +5. **Sandbox and isolate** — Set `agents.defaults.sandbox.mode` to `"non-main"` for Docker sandboxing. Use a Linux VM or UTM on Mac to segregate traffic. Tailscale for secure remote access. + +--- +✅ All agents reported back! +├─ 🟠 Reddit: 19 threads │ 415+ upvotes │ 290+ comments +├─ 🔵 X: 29 posts │ 815+ likes │ 65+ reposts +├─ 🌐 Web: 10+ pages │ Medium, DataCamp, DEV, GitHub, QNAP, docs.openclaw.ai +└─ Top voices: r/clawdbot, r/LocalLLM, r/AskVibecoders │ @aashatwt (452 likes), @recap_david (329 likes) │ Lukas Niessen on Medium + +--- + +**Here's the consensus best setup path based on what the community actually recommends:** + +### Quick-Start (30 min) +1. **Get dedicated hardware** — Mac Mini, old laptop, NAS (QNAP has an official guide), or a VPS (DigitalOcean gist available). Don't use your daily driver. +2. **Install via script**: `curl -fsSL https://clawd.bot/install.sh | bash` +3. **Run the wizard**: `openclaw onboard` — it walks through gateway, workspace, channels, and skills +4. **Connect Telegram first** — create a bot via @BotFather, paste the token +5. **Set your API key** — Anthropic API key for Claude, or connect Ollama for local models +6. **Customize SOUL.md** — this controls your bot's personality and behavior + +### Security Hardening (do this immediately) +- Bind gateway to localhost only, use Tailscale for remote access +- Set sandbox mode to `"non-main"` +- Create an explicit command allowlist (block `rm -rf`, `sudo`, `chmod`) +- Rotate API keys every 90 days +- Never add the bot to group chats + +### Budget Option ($0) +Per @shynxbt: Use a free AWS VPS + Claude Haiku model + Telegram bot = fully functional for $0. + +--- + +Want me to help you with a specific part of the setup, or do you have a particular use case in mind (home automation, CRM, coding assistant, etc.)? diff --git a/docs/test-results/v1-vs-v2-quick/v2-SKILL.md b/docs/test-results/v1-vs-v2-quick/v2-SKILL.md new file mode 100644 index 0000000..ff2a4de --- /dev/null +++ b/docs/test-results/v1-vs-v2-quick/v2-SKILL.md @@ -0,0 +1,332 @@ +--- +name: last30days +description: Research a topic from the last 30 days on Reddit + X + Web, become an expert, and write copy-paste-ready prompts for the user's target tool. +argument-hint: '"[topic] for [tool]" or "[topic]"' +allowed-tools: Bash, Read, Write, AskUserQuestion, WebSearch +--- + +# last30days: Research Any Topic from the Last 30 Days + +Research ANY topic across Reddit, X, and the web. Surface what people are actually discussing, recommending, and debating right now. + +## CRITICAL: Parse User Intent + +Before doing anything, parse the user's input for: + +1. **TOPIC**: What they want to learn about (e.g., "web app mockups", "Claude Code skills", "image generation") +2. **TARGET TOOL** (if specified): Where they'll use the prompts (e.g., "Nano Banana Pro", "ChatGPT", "Midjourney") +3. **QUERY TYPE**: What kind of research they want: + - **PROMPTING** - "X prompts", "prompting for X", "X best practices" → User wants to learn techniques and get copy-paste prompts + - **RECOMMENDATIONS** - "best X", "top X", "what X should I use", "recommended X" → User wants a LIST of specific things + - **NEWS** - "what's happening with X", "X news", "latest on X" → User wants current events/updates + - **GENERAL** - anything else → User wants broad understanding of the topic + +Common patterns: +- `[topic] for [tool]` → "web mockups for Nano Banana Pro" → TOOL IS SPECIFIED +- `[topic] prompts for [tool]` → "UI design prompts for Midjourney" → TOOL IS SPECIFIED +- Just `[topic]` → "iOS design mockups" → TOOL NOT SPECIFIED, that's OK +- "best [topic]" or "top [topic]" → QUERY_TYPE = RECOMMENDATIONS +- "what are the best [topic]" → QUERY_TYPE = RECOMMENDATIONS + +**IMPORTANT: Do NOT ask about target tool before research.** +- If tool is specified in the query, use it +- If tool is NOT specified, run research first, then ask AFTER showing results + +**Store these variables:** +- `TOPIC = [extracted topic]` +- `TARGET_TOOL = [extracted tool, or "unknown" if not specified]` +- `QUERY_TYPE = [RECOMMENDATIONS | NEWS | HOW-TO | GENERAL]` + +**DISPLAY your parsing to the user.** Before running any tools, output a single line: + +🔍 **{TOPIC}** · {QUERY_TYPE} +Searching Reddit, X, and the web for {natural language description of what you'll look for}... + +Example outputs: +- 🔍 **kanye west** · News — Searching Reddit, X, and the web for the latest kanye west news and discussions... +- 🔍 **best MCP servers** · Recommendations — Searching Reddit, X, and the web for the most recommended MCP servers... +- 🔍 **nano banana pro prompting** · Prompting — Searching Reddit, X, and the web for nano banana pro prompting techniques and tips... +- 🔍 **open claw** · General — Searching Reddit, X, and the web for what people are saying about open claw... + +If TARGET_TOOL is known, mention it: "...for nano banana pro prompting techniques to use in ChatGPT..." + +This text MUST appear before you call any tools. It confirms to the user that you understood their request. + +--- + +## Research Execution + +**Step 1: Run the research script** +```bash +python3 ~/.claude/skills/last30days/scripts/last30days.py "$ARGUMENTS" --emit=compact 2>&1 +``` + +The script will automatically: +- Detect available API keys +- Run Reddit/X searches if keys exist +- Signal if WebSearch is needed + +--- + +## STEP 2: DO WEBSEARCH WHILE SCRIPT RUNS + +The script auto-detects sources (Bird CLI, API keys, etc). While waiting for it, do WebSearch. + +For **ALL modes**, do WebSearch to supplement (or provide all data in web-only mode). + +Choose search queries based on QUERY_TYPE: + +**If RECOMMENDATIONS** ("best X", "top X", "what X should I use"): +- Search for: `best {TOPIC} recommendations` +- Search for: `{TOPIC} list examples` +- Search for: `most popular {TOPIC}` +- Goal: Find SPECIFIC NAMES of things, not generic advice + +**If NEWS** ("what's happening with X", "X news"): +- Search for: `{TOPIC} news 2026` +- Search for: `{TOPIC} announcement update` +- Goal: Find current events and recent developments + +**If PROMPTING** ("X prompts", "prompting for X"): +- Search for: `{TOPIC} prompts examples 2026` +- Search for: `{TOPIC} techniques tips` +- Goal: Find prompting techniques and examples to create copy-paste prompts + +**If GENERAL** (default): +- Search for: `{TOPIC} 2026` +- Search for: `{TOPIC} discussion` +- Goal: Find what people are actually saying + +For ALL query types: +- **USE THE USER'S EXACT TERMINOLOGY** - don't substitute or add tech names based on your knowledge +- EXCLUDE reddit.com, x.com, twitter.com (covered by script) +- INCLUDE: blogs, tutorials, docs, news, GitHub repos +- **DO NOT output "Sources:" list** - this is noise, we'll show stats at the end + +**Depth options** (passed through from user's command): +- `--quick` → Faster, fewer sources (8-12 each) +- (default) → Balanced (20-30 each) +- `--deep` → Comprehensive (50-70 Reddit, 40-60 X) + +--- + +## Judge Agent: Synthesize All Sources + +**After all searches complete, internally synthesize (don't display stats yet):** + +The Judge Agent must: +1. Weight Reddit/X sources HIGHER (they have engagement signals: upvotes, likes) +2. Weight WebSearch sources LOWER (no engagement data) +3. Identify patterns that appear across ALL three sources (strongest signals) +4. Note any contradictions between sources +5. Extract the top 3-5 actionable insights + +**Do NOT display stats here - they come at the end, right before the invitation.** + +--- + +## FIRST: Internalize the Research + +**CRITICAL: Ground your synthesis in the ACTUAL research content, not your pre-existing knowledge.** + +Read the research output carefully. Pay attention to: +- **Exact product/tool names** mentioned (e.g., if research mentions "ClawdBot" or "@clawdbot", that's a DIFFERENT product than "Claude Code" - don't conflate them) +- **Specific quotes and insights** from the sources - use THESE, not generic knowledge +- **What the sources actually say**, not what you assume the topic is about + +**ANTI-PATTERN TO AVOID**: If user asks about "clawdbot skills" and research returns ClawdBot content (self-hosted AI agent), do NOT synthesize this as "Claude Code skills" just because both involve "skills". Read what the research actually says. + +### If QUERY_TYPE = RECOMMENDATIONS + +**CRITICAL: Extract SPECIFIC NAMES, not generic patterns.** + +When user asks "best X" or "top X", they want a LIST of specific things: +- Scan research for specific product names, tool names, project names, skill names, etc. +- Count how many times each is mentioned +- Note which sources recommend each (Reddit thread, X post, blog) +- List them by popularity/mention count + +**BAD synthesis for "best Claude Code skills":** +> "Skills are powerful. Keep them under 500 lines. Use progressive disclosure." + +**GOOD synthesis for "best Claude Code skills":** +> "Most mentioned skills: /commit (5 mentions), remotion skill (4x), git-worktree (3x), /pr (3x). The Remotion announcement got 16K likes on X." + +### For all QUERY_TYPEs + +Identify from the ACTUAL RESEARCH OUTPUT: +- **PROMPT FORMAT** - Does research recommend JSON, structured params, natural language, keywords? +- The top 3-5 patterns/techniques that appeared across multiple sources +- Specific keywords, structures, or approaches mentioned BY THE SOURCES +- Common pitfalls mentioned BY THE SOURCES + +--- + +## THEN: Show Summary + Invite Vision + +**Display in this EXACT sequence:** + +**FIRST - What I learned (based on QUERY_TYPE):** + +**If RECOMMENDATIONS** - Show specific things mentioned with sources: +``` +🏆 Most mentioned: + +[Tool Name] - {n}x mentions +Use Case: [what it does] +Sources: @handle1, @handle2, r/sub, blog.com + +[Tool Name] - {n}x mentions +Use Case: [what it does] +Sources: @handle3, r/sub2, Complex + +Notable mentions: [other specific things with 1-2 mentions] +``` + +**CRITICAL for RECOMMENDATIONS:** +- Each item MUST have a "Sources:" line with actual @handles from X posts (e.g., @LONGLIVE47, @ByDobson) +- Include subreddit names (r/hiphopheads) and web sources (Complex, Variety) +- Parse @handles from research output and include the highest-engagement ones +- Format naturally - tables work well for wide terminals, stacked cards for narrow + +**If PROMPTING/NEWS/GENERAL** - Show synthesis and patterns: + +CITATION RULE: Cite sources sparingly to prove research is real. +- In the "What I learned" intro: cite 1-2 top sources total, not every sentence +- In KEY PATTERNS: cite 1 source per pattern, short format: "per @handle" or "per r/sub" +- Do NOT include engagement metrics in citations (likes, upvotes) - save those for stats box +- Do NOT chain multiple citations: "per @x, @y, @z" is too much. Pick the strongest one. + +**BAD:** "His album is set for March 20 (per @cocoabutterbf; Rolling Stone; HotNewHipHop; Complex)." +**GOOD:** "His album BULLY is set for March 20 via Gamma, per Rolling Stone." + +``` +What I learned: + +**{Topic 1}** — [1-2 sentences about this storyline, per source] + +**{Topic 2}** — [1-2 sentences, per source] + +**{Topic 3}** — [1-2 sentences, per source] + +KEY PATTERNS from the research: +1. [Pattern] — per @handle +2. [Pattern] — per r/sub +3. [Pattern] — per source +``` + +**THEN - Stats (right before invitation):** + +**CRITICAL: Calculate actual totals from the research output.** +- Count posts/threads from each section +- Sum engagement: parse `[Xlikes, Yrt]` from each X post, `[Xpts, Ycmt]` from Reddit +- Identify top voices: highest-engagement @handles from X, most active subreddits + +**Copy this EXACTLY, replacing only the {placeholders}:** + +``` +--- +✅ All agents reported back! +├─ 🟠 Reddit: {N} threads │ {N} upvotes │ {N} comments +├─ 🔵 X: {N} posts │ {N} likes │ {N} reposts (via Bird/xAI) +├─ 🌐 Web: {N} pages │ {domain1}, {domain2}, {domain3} +└─ 🗣️ Top voices: @{handle1} ({N} likes), @{handle2} │ r/{sub1}, r/{sub2} +--- +``` + +If Reddit returned 0 threads, write: "├─ 🟠 Reddit: 0 threads (no results this cycle)" +NEVER use plain text dashes (-) or pipe (|). ALWAYS use ├─ └─ │ and the emoji. + +**SELF-CHECK before displaying**: Re-read your "What I learned" section. Does it match what the research ACTUALLY says? If you catch yourself projecting your own knowledge instead of the research, rewrite it. + +**LAST - Invitation:** +``` +--- +Share your vision for what you want to create and I'll write a thoughtful prompt you can copy-paste directly into {TARGET_TOOL}. +``` + +--- + +## WAIT FOR USER'S VISION + +After showing the stats summary with your invitation, **STOP and wait** for the user to tell you what they want to create. + +--- + +## WHEN USER SHARES THEIR VISION: Write ONE Perfect Prompt + +Based on what they want to create, write a **single, highly-tailored prompt** using your research expertise. + +### CRITICAL: Match the FORMAT the research recommends + +**If research says to use a specific prompt FORMAT, YOU MUST USE THAT FORMAT.** + +**ANTI-PATTERN**: Research says "use JSON prompts with device specs" but you write plain prose. This defeats the entire purpose of the research. + +### Quality Checklist (run before delivering): +- [ ] **FORMAT MATCHES RESEARCH** - If research said JSON/structured/etc, prompt IS that format +- [ ] Directly addresses what the user said they want to create +- [ ] Uses specific patterns/keywords discovered in research +- [ ] Ready to paste with zero edits (or minimal [PLACEHOLDERS] clearly marked) +- [ ] Appropriate length and style for TARGET_TOOL + +### Output Format: + +``` +Here's your prompt for {TARGET_TOOL}: + +--- + +[The actual prompt IN THE FORMAT THE RESEARCH RECOMMENDS] + +--- + +This uses [brief 1-line explanation of what research insight you applied]. +``` + +--- + +## IF USER ASKS FOR MORE OPTIONS + +Only if they ask for alternatives or more prompts, provide 2-3 variations. Don't dump a prompt pack unless requested. + +--- + +## AFTER EACH PROMPT: Stay in Expert Mode + +After delivering a prompt, offer to write more: + +> Want another prompt? Just tell me what you're creating next. + +--- + +## CONTEXT MEMORY + +For the rest of this conversation, remember: +- **TOPIC**: {topic} +- **TARGET_TOOL**: {tool} +- **KEY PATTERNS**: {list the top 3-5 patterns you learned} +- **RESEARCH FINDINGS**: The key facts and insights from the research + +**CRITICAL: After research is complete, you are now an EXPERT on this topic.** + +When the user asks follow-up questions: +- **DO NOT run new WebSearches** - you already have the research +- **Answer from what you learned** - cite the Reddit threads, X posts, and web sources +- **If they ask for a prompt** - write one using your expertise + +Only do new research if the user explicitly asks about a DIFFERENT topic. + +--- + +## Output Summary Footer (After Each Prompt) + +After delivering a prompt, end with: + +``` +--- +📚 Expert in: {TOPIC} for {TARGET_TOOL} +📊 Based on: {n} Reddit threads ({sum} upvotes) + {n} X posts ({sum} likes) + {n} web pages + +Want another prompt? Just tell me what you're creating next. +``` diff --git a/docs/test-results/v1-vs-v2-quick/v2/1-kanye-west.txt b/docs/test-results/v1-vs-v2-quick/v2/1-kanye-west.txt new file mode 100644 index 0000000..e4a9885 --- /dev/null +++ b/docs/test-results/v1-vs-v2-quick/v2/1-kanye-west.txt @@ -0,0 +1,25 @@ +Here's what I found: + +## What I learned: + +**BULLY Album — March 20, 2026 via Gamma** — After years of delays (first announced September 2024), Kanye's 12th studio album finally has a firm release date. The 13-track project features Peso Pluma, Playboi Carti, and Ty Dolla Sign. Sonically it recalls *808s & Heartbreak* and *MBDTF* — Ye mostly sings rather than raps. Notably, earlier leak versions used AI-deepfaked vocals, which have reportedly been re-recorded with his real voice for the official release, per Rolling Stone. + +**Public Apology for Antisemitism** — On January 26, Ye took out a full-page Wall Street Journal ad titled "To Those I've Hurt," publicly apologizing for his antisemitic remarks. He disclosed a previously undiagnosed brain injury from his 2002 car accident and attributed his behavior to a four-month manic episode fueled by bipolar disorder, psychosis, and paranoia. "I lost touch with reality," he wrote. The apology also extended to the Black community, per The Washington Post. + +**Hellwatt Festival in Italy** — Ye is headlining a brand-new festival at the 103,000-capacity RCF Arena in Italy over three weekends from July 4-18, 2026 — his first-ever live concert in Italy, per Billboard. + +**Health Concerns** — A rare January 2026 outing in LA with Bianca Censori reignited concern about Ye's physical appearance. Insiders point to medication side effects, frequent travel, and inconsistent routines, per AllHipHop. + +**Grammys Ban** — Ye is reportedly not welcome at the 2026 Grammy Awards after clashing with organizers last year over his invitation terms, per The News International. + +**Kim & Lewis Hamilton Buzz** — X chatter is heavily focused on Kim Kardashian's relationship with Lewis Hamilton, with users contrasting her new relationship against her marriage to Ye. + +--- +✅ All agents reported back! +├─ 🟠 Reddit: 0 threads (no results this cycle) +├─ 🔵 X: 29 posts │ 33 likes │ 14 reposts (via xAI) +├─ 🌐 Web: 30+ pages │ rollingstone.com, washingtonpost.com, complex.com, billboard.com, npr.org +└─ 🗣️ Top voices: @honest30bgfan_ (33 likes), @HipHopCrave_ │ Rolling Stone, Washington Post, Complex +--- + +Share your vision for what you want to create and I'll write a thoughtful prompt you can copy-paste directly into your tool of choice. diff --git a/docs/v2.5-launch-tweets.md b/docs/v2.5-launch-tweets.md new file mode 100644 index 0000000..a5480f1 --- /dev/null +++ b/docs/v2.5-launch-tweets.md @@ -0,0 +1,360 @@ +# last30days v2.5 Launch Thread + +## FINAL THREAD (6 tweets) + +### 1/6 - Announcement + +I can't believe it's been 30 days since I launched @slashlast30days. 3.2k stars later, time for v2.5. + +Three big additions: +1. @Polymarket prediction markets as a 6th source - helps you predict the future +2. Cross-source linking + massively better results - detects when the same story trends across multiple platforms. Ran a 15-way blinded comparison, v2.5 scored 4.38 vs 3.73 for the original. Won all 5 topics. +3. Hacker News as a 5th source - a window into the tech and developer insider world + +github.com/mvanhorn/last30days-skill + +### 2/6 - Demo: Anthropic vs Pentagon + +"/last30days Anthropic Pete Hegseth" + +14 Reddit threads. 29 X posts (11,559 likes). 20 YouTube videos (739K views). 5 HN stories. 9 Polymarket markets. + +This story broke TODAY. Hegseth designated Anthropic a "supply chain risk." Trump ordered every agency to stop using their tech. + +Polymarket: Anthropic still 99% for best AI model. $500B+ valuation: 68%. IPO >$600B: 97%. Hegseth out by March: only 6%. + +Markets say Anthropic wins regardless. That's the kind of signal you can't get from opinion threads. + +### 3/6 - Demo: Seedance Prompting + +"/last30days Seedance prompting" + +13 Reddit threads. 33 X posts. 20 YouTube videos (1.2M views, 4 transcripts). 15 web pages. + +Top finding: Seedance 2.0 prompts follow a director's shot-list format, not freeform text. 30-100 words. Subject + Action + Camera + Scene + Style. Beyond 100 words, results degrade. + +Then I said: "a cinematic drone shot over a city at golden hour" + +It wrote me a copy-paste prompt using the exact patterns from the research. Research first, then create from what you learned. + +### 4/6 - Demo: Arizona Basketball + +"/last30days arizona basketball" + +6 Polymarket markets. 37 X posts (4,200 likes). 15 YouTube videos (517K views). 2 Reddit threads. + +Arizona is 25-2, set a program record with a 22-0 start, and holds a 2-game Big 12 lead with 3 games left. The Field of 68 called them "the TOUGHEST team in America" after escaping Baylor shorthanded. Kansas rematch Saturday - the highlight video from their first meeting has 248K views on ESPN's YouTube. + +Polymarket: Championship 13%. #1 seed: 88%. Duke and Michigan each at 18% to win it all. + +That's not a sports blog. That's Reddit reactions + X engagement + YouTube analysis + prediction market odds from one command. + +### 5/6 - Demo: Iran War + +"/last30days iran war" + +2 Reddit threads. 34 X posts (10,048 likes). 20 YouTube videos (1.6M views, 5 transcripts). 4 HN stories (850 points). 14 Polymarket markets ($473M volume). + +Geneva talks just ended without a deal. 150+ US aircraft deployed. Two carrier strike groups in position. F-22s sent to Israel. Members of Congress who saw the secret war plan came out "terrified." @cenkuygur: "they are about to drag us into a war that 70-85% of Americans oppose" (7,700 likes). + +Polymarket ($473M in volume - one of their biggest markets ever): strikes by 2026: 80%. By March 31: 68%. War Powers invoked: 51%. Formal war declaration: only 12%. + +Markets say: strikes are very likely, declared war is not. That's the sharpest signal in the entire research. + +### 6/6 - Thank You + +Thank you to ARJ999 and wkbaran on GitHub who filed three separate issues asking for Hacker News support. v2.5 delivers. + +It's been a crazy 30 days. 3.2k stars. Six sources. Massively better results. Super excited to get this out. + +Try it: /last30days [any topic] + +github.com/mvanhorn/last30days-skill + +--- +--- + +## REFERENCE MATERIAL BELOW + +## Context +- 3.2k stars on GitHub +- V2.5 headline features: Polymarket (6th source), Hacker News (5th source), cross-source linking +- Ran 15-way blinded comparison: 4.38/5.0 vs 3.73/5.0 +- Won all 5 topics, zero regressions +- Cross-source linking: 3 -> 13 linked items +- Demo topics: Anthropic odds (11 markets), Arizona basketball (6 markets), Iran war ($425M volume) + +--- + +## Post 1: Lead (Announcement) + +V2.5 of @slashlast30days is out. Now with @Polymarket prediction markets, cross-source linking, and massively better results. + +1. Polymarket as a 6th source - real money on outcomes, no API key needed +2. Hacker News as a 5th source +3. Cross-source linking - detects when the same story trends across multiple platforms + +Ran a 15-way blinded comparison across 5 topics. v2.5 scored 4.38 vs 3.73 for the original. Won all 5. Zero regressions. + +github.com/mvanhorn/last30days-skill + +--- + +## Post 2: POLYMARKET AS A 6TH SOURCE. + +Reddit tells you what people think. X tells you what people share. YouTube tells you what people watch. HN tells you what developers discuss. + +Polymarket helps you predict the future. + +"/last30days anthropic odds" + +11 markets found. Best AI model February: Anthropic 98%. IPO before OpenAI: 64%. $500B+ valuation: 87%. Pentagon ban odds: only 22%. + +Free API. No key. Real money on outcomes. + +--- + +## Post 3: CROSS-SOURCE LINKING. + +When a Seedance 2.0 tutorial has 44K YouTube views AND trends on HN AND gets discussed on Reddit, v2.5 flags it: [also on: HN, YouTube] + +Old version linked 3 items across 5 test topics. New version links 13. The difference is hybrid similarity - combining character-trigram and token-level matching at a tuned threshold. + +Cross-platform convergence is the strongest signal that something actually matters. Not engagement on one platform. Convergence across all of them. + +--- + +## Post 4: 15-WAY BLINDED EVALUATION. + +I don't trust vibes for measuring quality. So I ran a scientific comparison. + +5 topics x 3 versions. Stripped version labels. Randomized as A/B/C. Scored on groundedness, specificity, coverage, actionability, and format. + +v2.5: 4.38/5.0 +v2.2 (HN only): 4.10/5.0 +v2.0 (original): 3.73/5.0 + +Won all 5 topics. Zero regressions. Biggest gains: specificity (+0.8) and format (+1.0) from cross-source linking giving the synthesis better material to work with. + +--- + +## Post 5: Demo - Anthropic Odds + +Asked it about Anthropic odds. + +11 Polymarket markets. 25 X posts. 13 YouTube videos (719K views). 6 HN stories (471 points). + +Best AI model February: 98%. IPO before OpenAI: 64%. $500B+ valuation: 87%. FrontierMath 50% score: 48% (up 28% today). Pentagon ban: only 22%. + +Markets say Anthropic is winning the model race AND the valuation race. The Pentagon thing is noise. + +--- + +## Post 6: Demo - Arizona Basketball + +"/last30days arizona basketball" + +6 Polymarket markets. 37 X posts (4,200 likes). 15 YouTube videos (517K views). 2 Reddit threads. + +Championship odds: 13%. #1 seed: 88%. Big 12 title: Arizona leads by 2. + +That's not a sports blog. That's Reddit reactions + X engagement + YouTube analysis + prediction market odds from one command. + +The Polymarket integration uses two-pass query expansion. First pass finds "Arizona Big 12." Second pass discovers the championship and #1 seed markets via tag-based domain bridging. + +--- + +## Post 7: Demo - Iran War + +The best Polymarket demo is news. + +"/last30days iran war" + +14 Polymarket markets. $425M+ in volume. 7 Reddit threads. 30 X posts. 20 YouTube videos (2M views). 18 HN stories (1,187 points). + +US strikes Iran by 2026: 70%. War Powers by March: 60%. Israel strikes by June: 64%. Formal war declaration: only 8%. + +Markets say: limited strikes with War Powers, NOT a declared war. Breaking Points (435K views) covered leaked Pentagon opposition. r/Conservative "imploding" per r/SubredditDrama. + +One command. Six sources. Real money. + +--- + +## Post 8: Credits + CTA + +Also in v2.5: YouTube synonym expansion ("hip hop" now matches "rap" - relevance jumped 0.33 to 0.71), X handle resolution, and HN OR queries for framework topics. + +The difference between "good research" and "research you'd actually trust" is in details like this. + +Try it: /last30days [any topic] + +github.com/mvanhorn/last30days-skill + +--- + +## Post 9: Demo - Claude Code (ALL 6 sources) + +"/last30days Claude Code" + +3 Reddit threads (199 upvotes). 35 X posts (5,239 likes). 15 YouTube videos (1.4M views, 5 transcripts). 30 HN stories (~8,500 points). 8 Polymarket markets. 20 web pages. + +All six sources hit. Top finding: the planning-first workflow has won. The #1 HN post this month (969 pts, 590 comments) is about separating planning from execution. Boris Cherny (Head of Claude Code) on Lenny's Podcast: "100% of my code is written by Claude Code - I have not edited a single line by hand since November." + +Polymarket: Anthropic 99% for best AI model in February. 58% for March. Claude on FrontierMath at 55%. The US government rejected Claude - Polymarket has the Hegseth ban at 32%. + +Then I asked it to dig deeper into the planning-first workflow. No new searches - it answered from what it already learned. + +--- + +## Post 10: Demo - March Madness Odds (Polymarket + Sports) + +"/last30days March Madness Odds" + +2 Reddit threads. 31 X posts. 6 YouTube videos (46K views, 4 transcripts). 2 Polymarket markets. + +Tournament winner: Duke 18%, Michigan 18%, Arizona 13%. #1 seeds: Michigan 98%, Duke 91%, Arizona 88%. + +Duke is the hottest mover - went from +700 to +450 in one week. The skill surfaced that from sportsbook data, X commentary, and Polymarket odds simultaneously. + +Then I asked it to break down Michigan vs Duke vs Arizona. Full analysis from the research it already had. + +--- + +## Post 11: Demo - Seedance Prompting (Expert + Prompt Mode) + +"/last30days Seedance prompting" + +13 Reddit threads. 33 X posts. 20 YouTube videos (1.2M views, 4 transcripts). 1 HN story. 15 web pages. + +Top finding: Seedance 2.0 prompts follow a director's shot-list format, not freeform text. 30-100 words. Subject + Action + Camera + Scene + Style + Constraints. Beyond 100 words, results degrade. + +Then I said: "a cinematic drone shot over a city at golden hour" + +It wrote me a copy-paste prompt using the exact patterns from the research. That's the skill's real power - research first, then create from what you learned. + +--- + +## Post 12: Thank You + CTA (Final) + +Thank you to ARJ999 and wkbaran on GitHub who kept asking for Hacker News support. Three separate issues. v2.5 delivers. + +30 days. 3.2k stars. 6 sources. Massively better results. + +Try it: /last30days [any topic] + +github.com/mvanhorn/last30days-skill + +--- + +## Post 13: Demo - Anthropic vs Pentagon (Breaking News + Polymarket) + +"/last30days Anthropic Pete Hegseth" + +14 Reddit threads. 29 X posts (11,559 likes). 20 YouTube videos (739K views, 5 transcripts). 5 HN stories. 9 Polymarket markets. 10 web pages. + +This story broke TODAY. Defense Secretary Hegseth designated Anthropic a "supply chain risk" - believed to be the first time an American company has ever received this designation. Trump ordered every federal agency to stop using Anthropic tech. + +Polymarket: Anthropic still 99% for best AI model. $500B+ valuation: 68%. IPO >$600B: 97%. Hegseth out by March 31: only 6%. + +Markets say: Anthropic wins the model race regardless. Bettors don't think Hegseth survives this. That's the kind of signal you can't get from opinion threads. + +--- + +## Post 14: Demo - OpenAI Insider Trading (News + Polymarket) + +"/last30days OpenAI Insider Trading" + +2 Reddit threads. 29 X posts. 4 YouTube videos (360K views, 4 transcripts). 2 HN stories. 15 Polymarket markets. 15 web pages. + +An OpenAI employee was just fired for using confidential info to bet on Polymarket. 13 brand-new wallets appeared 40 hours before the browser launch. $309K bet on the right outcome. Unusual Whales flagged 77 suspected insider positions across 60 wallets. + +Meanwhile Polymarket has OpenAI's IPO at $1.25-1.5T: 54%. Anthropic IPOs first: 62%. Best AI model: Anthropic 99%. + +The prediction markets are both the story AND the source. One command pulled all of it together. + +--- + +## Standalone Tweet: Polymarket Stats Line + +"/last30days Anthropic Pete Hegseth" + +The Pentagon just designated Anthropic a supply chain risk. First time ever for an American company. Trump ordered every agency to stop using their tech. + +Here's what Polymarket says: + +📊 9 markets │ Best AI model: 99% │ $500B+ valuation: 68% │ IPO >$600B: 97% │ Hegseth out by March: 6% + +Bettors with real money on the line think Anthropic wins the model race, goes public at a massive valuation, and Hegseth doesn't survive this. + +That's the gap between headlines and reality. One command, six sources. + +github.com/mvanhorn/last30days-skill + +--- + +## Recommended Thread Order (pick 8-10) + +The full thread above is 12 posts. Here's what I'd cut to keep it tight: + +**Must include (core story):** +1. Post 1 - Lead announcement +2. Post 2 - Polymarket ("Reddit tells you what people think...") +3. Post 3 - Cross-source linking +4. Post 4 - Blinded evaluation + +**Best demos (pick 3-4):** +- Post 13 (Anthropic vs Pentagon) - STRONGEST. Breaking news today. Polymarket cuts through the noise. "Markets say Anthropic wins regardless." +- Post 9 (Claude Code) - All 6 sources. Massive numbers. Shows follow-up flow. +- Post 10 (March Madness) - Sports/Polymarket crossover. Timely with tournament approaching. +- Post 11 (Seedance) - Shows prompting flow. 1.2M YouTube views. +- Post 5 (Anthropic Odds) - Overlaps with Post 13 now. Skip. + +**Skip or save for standalone tweets:** +- Post 5 (Anthropic Odds) - Redundant with Post 13 +- Post 6 (Arizona Basketball) - Covered by March Madness now +- Post 7 (Iran War) - Great standalone tweet, not for launch thread +- Post 8 (Credits/minor features) - Fold into CTA + +**My recommended 8-post thread:** +1. Lead (Post 1) +2. Polymarket (Post 2) +3. Cross-source linking (Post 3) +4. Blinded evaluation (Post 4) +5. Demo: Anthropic vs Pentagon (Post 13) - breaking news, best Polymarket showcase +6. Demo: Claude Code (Post 9) +7. Demo: March Madness (Post 10) +8. Thank you + CTA (Post 12) + +--- + +## Video Script (~60 seconds) + +**[Talking to camera]** + +Oh my god, I can't believe it's been 30 days since I launched last30days. 3,200 stars on GitHub. This has been the craziest month. + +Today I'm shipping v2.5 and I'm really excited about this one. Three big things. + +**[Screen recording: typing /last30days Anthropic Pete Hegseth]** + +First - Polymarket prediction markets as a 6th source. So this Anthropic-Pentagon story broke today. Hegseth designated Anthropic a supply chain risk, Trump ordered agencies to stop using their tech. Scary headline, right? + +But Polymarket says: Anthropic still 99% for best AI model. IPO above 600 billion: 97%. Hegseth out by March: 6%. Real money on outcomes helps you predict the future. That's a different story than the headlines. + +**[Screen recording: typing /last30days arizona basketball]** + +Second - it now searches Hacker News and does cross-source linking. When the same story shows up on Reddit AND YouTube AND HN, it flags it. I ran a 15-way blinded comparison and v2.5 scored 4.38 versus 3.73 for the original. Won all 5 test topics. + +**[Back to camera]** + +Thank you to everyone who starred it, filed issues, and kept pushing me to make this better. Shoutout to the people on GitHub who literally filed three separate issues asking for Hacker News. v2.5 delivers. + +Link in bio. Try it on anything. + +--- + +## Scoring Note + +The 4.38 vs 3.73 score is from a custom 5-dimension rubric (30% groundedness, 25% specificity, 20% coverage, 15% actionability, 10% format compliance) evaluated by Claude on blinded outputs. The relative ranking is meaningful; the absolute numbers are not. It's an LLM grading LLM output - useful for A/B comparison, not for claiming "4.38 out of 5 quality." + +If using the score in a tweet, frame it as "scored X vs Y on a blinded comparison" not "rated 4.38/5.0 quality" - the former is honest, the latter implies an objective standard that doesn't exist. diff --git a/scripts/last30days.py b/scripts/last30days.py index e8d2105..f57c813 100644 --- a/scripts/last30days.py +++ b/scripts/last30days.py @@ -44,7 +44,10 @@ TIMEOUT_PROFILES = { } # Valid source names for the --search flag -VALID_SEARCH_SOURCES = {"reddit", "x", "hn", "youtube", "tiktok", "instagram", "polymarket", "web"} +VALID_SEARCH_SOURCES = { + "reddit", "x", "hn", "youtube", "tiktok", "instagram", + "polymarket", "web", "xiaohongshu", "xhs", +} def parse_search_flag(search_str: str) -> set: @@ -64,6 +67,8 @@ def parse_search_flag(search_str: str) -> set: s = s.strip().lower() if not s: continue + if s == "xhs": + s = "xiaohongshu" if s not in VALID_SEARCH_SOURCES: print( f"Error: Unknown search source '{s}'. " @@ -133,6 +138,7 @@ from lib import ( dates, dedupe, hackernews, + xiaohongshu_api, polymarket, entity_extract, env, @@ -208,36 +214,60 @@ def _search_reddit( sys.stderr.flush() # Fall through to OpenAI if we have that key if not config.get("OPENAI_API_KEY"): - return [], {"error": str(e)}, reddit_error, used_scrapecreators + # No OpenAI either: try public Reddit fallback. + try: + reddit_items = openai_reddit.search_reddit_public( + topic, from_date, to_date, depth=depth, + ) + raw_response = {"source": "reddit_public", "items": reddit_items} + return reddit_items, raw_response, None, False + except Exception as e2: + return [], {"error": str(e)}, reddit_error, used_scrapecreators used_scrapecreators = False sys.stderr.write("[Reddit] Falling back to OpenAI\n") sys.stderr.flush() # === OpenAI path (fallback) === if not mock: - try: - raw_response = openai_reddit.search_reddit( - config["OPENAI_API_KEY"], - selected_models["openai"], - topic, - from_date, - to_date, - depth=depth, - auth_source=config.get("OPENAI_AUTH_SOURCE", "api_key"), - account_id=config.get("OPENAI_CHATGPT_ACCOUNT_ID"), - ) - except http.HTTPError as e: - raw_response = {"error": str(e)} - reddit_error = f"API error: {e}" - except Exception as e: - raw_response = {"error": str(e)} - reddit_error = f"{type(e).__name__}: {e}" + if config.get("OPENAI_API_KEY"): + try: + raw_response = openai_reddit.search_reddit( + config["OPENAI_API_KEY"], + selected_models["openai"], + topic, + from_date, + to_date, + depth=depth, + auth_source=config.get("OPENAI_AUTH_SOURCE", "api_key"), + account_id=config.get("OPENAI_CHATGPT_ACCOUNT_ID"), + ) + except http.HTTPError as e: + raw_response = {"error": str(e)} + reddit_error = f"API error: {e}" + except Exception as e: + raw_response = {"error": str(e)} + reddit_error = f"{type(e).__name__}: {e}" + else: + # No OpenAI auth: direct Reddit public JSON fallback. + try: + reddit_items = openai_reddit.search_reddit_public( + topic, from_date, to_date, depth=depth, + ) + raw_response = {"source": "reddit_public", "items": reddit_items} + except http.HTTPError as e: + reddit_items = [] + raw_response = {"error": str(e), "source": "reddit_public"} + reddit_error = f"Reddit public API error: {e}" + except Exception as e: + reddit_items = [] + raw_response = {"error": str(e), "source": "reddit_public"} + reddit_error = f"Reddit public search error: {type(e).__name__}: {e}" # Parse response reddit_items = openai_reddit.parse_reddit_response(raw_response or {}) # Quick retry with simpler query if few results - if len(reddit_items) < 5 and not mock and not reddit_error: + if len(reddit_items) < 5 and not mock and not reddit_error and config.get("OPENAI_API_KEY"): core = openai_reddit._extract_core_subject(topic) if core.lower() != topic.lower(): try: @@ -259,7 +289,7 @@ def _search_reddit( pass # Subreddit-targeted fallback if still < 3 results - if len(reddit_items) < 3 and not mock and not reddit_error: + if len(reddit_items) < 3 and not mock and not reddit_error and config.get("OPENAI_API_KEY"): sub_query = openai_reddit._build_subreddit_query(topic) try: sub_raw = openai_reddit.search_reddit( @@ -543,6 +573,48 @@ def _search_web( return raw_results, web_error +def _search_xiaohongshu( + topic: str, + config: dict, + from_date: str, + to_date: str, + depth: str, +) -> tuple: + """Search Xiaohongshu via xiaohongshu-mcp HTTP API (runs in thread). + + Returns: + Tuple of (xiaohongshu_items, xiaohongshu_error) + Items are in web-item dict shape and can be normalized with websearch module. + """ + base_url = env.get_xiaohongshu_api_base(config) + try: + items = xiaohongshu_api.search_feeds( + topic=topic, + from_date=from_date, + to_date=to_date, + base_url=base_url, + depth=depth, + ) + except Exception as e: + return [], f"{type(e).__name__}: {e}" + + # Ensure all required keys exist for normalize_websearch_items() + for i, item in enumerate(items): + item.setdefault("id", f"XHS{i+1}") + item.setdefault("title", "") + item.setdefault("url", "") + item.setdefault("source_domain", "xiaohongshu.com") + item.setdefault("snippet", "") + if item.get("date") and not item.get("date_confidence"): + item["date_confidence"] = "med" + elif not item.get("date"): + item["date_confidence"] = "low" + item.setdefault("relevance", 0.5) + item.setdefault("why_relevant", "") + + return items, None + + def _run_supplemental( topic: str, reddit_items: list, @@ -727,6 +799,7 @@ def run_research( run_youtube: bool = False, run_tiktok: bool = False, run_instagram: bool = False, + run_xiaohongshu: bool = False, timeouts: dict = None, resolved_handle: str = None, do_hackernews: bool = True, @@ -769,6 +842,7 @@ def run_research( hackernews_error = None polymarket_error = None web_error = None + xiaohongshu_error = None # Determine web search mode do_web = sources in ("all", "web", "reddit-web", "x-web") @@ -796,6 +870,19 @@ def run_research( if progress: progress.start_web_only() progress.end_web_only() + # Optional Xiaohongshu search in web-only mode. + if run_xiaohongshu: + try: + xhs_items, xiaohongshu_error = _search_xiaohongshu( + topic, config, from_date, to_date, depth, + ) + web_items.extend(xhs_items) + if xiaohongshu_error and progress: + progress.show_error(f"Xiaohongshu error: {xiaohongshu_error}") + except Exception as e: + xiaohongshu_error = f"{type(e).__name__}: {e}" + if progress: + progress.show_error(f"Xiaohongshu error: {e}") # Still run YouTube/TikTok/Instagram in web-only mode if available if run_youtube: if progress: @@ -851,10 +938,20 @@ def run_research( youtube_future = None tiktok_future = None instagram_future = None + xiaohongshu_future = None hackernews_future = None polymarket_future = None web_future = None - max_workers = 2 + (1 if run_youtube else 0) + (1 if run_tiktok else 0) + (1 if run_instagram else 0) + (1 if do_hackernews else 0) + (1 if do_polymarket else 0) + (1 if web_backend else 0) + max_workers = ( + 2 + + (1 if run_youtube else 0) + + (1 if run_tiktok else 0) + + (1 if run_instagram else 0) + + (1 if run_xiaohongshu else 0) + + (1 if do_hackernews else 0) + + (1 if do_polymarket else 0) + + (1 if web_backend else 0) + ) with ThreadPoolExecutor(max_workers=max_workers) as executor: # Submit searches @@ -897,6 +994,11 @@ def run_research( env.get_instagram_token(config), ) + if run_xiaohongshu: + xiaohongshu_future = executor.submit( + _search_xiaohongshu, topic, config, from_date, to_date, depth, + ) + if do_hackernews: if progress: progress.start_hackernews() @@ -1004,6 +1106,21 @@ def run_research( if progress: progress.end_instagram(len(instagram_items)) + if xiaohongshu_future: + try: + xhs_items, xiaohongshu_error = xiaohongshu_future.result(timeout=future_timeout) + web_items.extend(xhs_items) + if xiaohongshu_error and progress: + progress.show_error(f"Xiaohongshu error: {xiaohongshu_error}") + except TimeoutError: + xiaohongshu_error = f"Xiaohongshu search timed out after {future_timeout}s" + if progress: + progress.show_error(xiaohongshu_error) + except Exception as e: + xiaohongshu_error = f"{type(e).__name__}: {e}" + if progress: + progress.show_error(f"Xiaohongshu error: {e}") + if hackernews_future: hn_timeout = timeouts.get("hackernews_future", future_timeout) try: @@ -1303,11 +1420,15 @@ def main(): # Auto-detect ScrapeCreators for Instagram has_instagram = env.is_instagram_available(config) + # Auto-detect Xiaohongshu HTTP API (requires service + login) + has_xiaohongshu = env.is_xiaohongshu_available(config) + # --diagnose: show source availability and exit if args.diagnose: web_source = env.get_web_search_source(config) diag = { "openai": bool(config.get("OPENAI_API_KEY")), + "reddit_public": True, "xai": bool(config.get("XAI_API_KEY")), "x_source": x_source_status["source"], "bird_installed": x_source_status["bird_installed"], @@ -1316,6 +1437,8 @@ def main(): "youtube": has_ytdlp, "tiktok": has_tiktok, "instagram": has_instagram, + "xiaohongshu": has_xiaohongshu, + "xiaohongshu_api_base": env.get_xiaohongshu_api_base(config), "hackernews": True, "polymarket": True, "web_search_backend": web_source, @@ -1339,6 +1462,7 @@ def main(): web_source = env.get_web_search_source(config) diag = { "openai": bool(config.get("OPENAI_API_KEY")), + "reddit_public": True, "xai": bool(config.get("XAI_API_KEY")), "x_source": x_source_status["source"], "bird_installed": x_source_status["bird_installed"], @@ -1346,6 +1470,8 @@ def main(): "bird_username": x_source_status.get("bird_username"), "youtube": has_ytdlp, "tiktok": has_tiktok, + "instagram": has_instagram, + "xiaohongshu": has_xiaohongshu, "hackernews": True, "polymarket": True, "web_search_backend": "deferred to assistant" if args.no_native_web else web_source, @@ -1432,6 +1558,7 @@ def main(): search_run_youtube = has_ytdlp search_run_tiktok = has_tiktok search_run_instagram = has_instagram + search_run_xiaohongshu = has_xiaohongshu if args.search: search_sources = parse_search_flag(args.search) has_reddit = "reddit" in search_sources @@ -1441,6 +1568,8 @@ def main(): search_run_youtube = "youtube" in search_sources and has_ytdlp search_run_tiktok = "tiktok" in search_sources and has_tiktok search_run_instagram = "instagram" in search_sources and has_instagram + # If explicitly requested, attempt Xiaohongshu even when preflight says unavailable. + search_run_xiaohongshu = "xiaohongshu" in search_sources include_search_web = "web" in search_sources # Map to existing sources string if has_reddit and has_x: @@ -1468,6 +1597,7 @@ def main(): run_youtube=search_run_youtube, run_tiktok=search_run_tiktok, run_instagram=search_run_instagram, + run_xiaohongshu=search_run_xiaohongshu, timeouts=timeouts, resolved_handle=args.x_handle, do_hackernews=search_do_hackernews, @@ -1590,8 +1720,6 @@ def main(): # Build source info for status footer source_info = {} - if not bool(config.get("OPENAI_API_KEY")): - source_info["reddit_skip_reason"] = "No OPENAI_API_KEY (add to ~/.config/last30days/.env)" if not x_source: if x_source_status["bird_installed"]: source_info["x_skip_reason"] = "Bird installed but not authenticated — log into x.com in browser" @@ -1605,6 +1733,11 @@ def main(): source_info["tiktok_skip_reason"] = "No SCRAPECREATORS_API_KEY - sign up at scrapecreators.com (100 free credits)" if not has_instagram: source_info["instagram_skip_reason"] = "No SCRAPECREATORS_API_KEY - sign up at scrapecreators.com (100 free credits)" + if not has_xiaohongshu: + source_info["xiaohongshu_skip_reason"] = ( + f"Xiaohongshu API unavailable or not logged in - start xiaohongshu-mcp and login " + f"(base: {env.get_xiaohongshu_api_base(config)})" + ) if not web_source: source_info["web_skip_reason"] = "assistant will use WebSearch (add BRAVE_API_KEY for native search)" diff --git a/scripts/lib/env.py b/scripts/lib/env.py index 207e40a..e7f3a4d 100644 --- a/scripts/lib/env.py +++ b/scripts/lib/env.py @@ -199,6 +199,7 @@ def get_config() -> Dict[str, Any]: ('OPENROUTER_API_KEY', None), ('PARALLEL_API_KEY', None), ('BRAVE_API_KEY', None), + ('XIAOHONGSHU_API_BASE', None), ('OPENAI_MODEL_POLICY', 'auto'), ('OPENAI_MODEL_PIN', None), ('XAI_MODEL_POLICY', 'latest'), @@ -245,11 +246,12 @@ def get_reddit_source(config: Dict[str, Any]) -> Optional[str]: def get_available_sources(config: Dict[str, Any]) -> str: - """Determine which sources are available based on API keys. + """Determine which sources are available. Returns: 'all', 'both', 'reddit', 'reddit-web', 'x', 'x-web', 'web', or 'none' """ - has_reddit = is_reddit_available(config) + # Reddit is available via public JSON fallback even without OpenAI auth. + has_reddit = True has_xai = bool(config.get('XAI_API_KEY')) has_web = has_web_search_keys(config) @@ -257,12 +259,7 @@ def get_available_sources(config: Dict[str, Any]) -> str: return 'all' if has_web else 'both' elif has_reddit: return 'reddit-web' if has_web else 'reddit' - elif has_xai: - return 'x-web' if has_web else 'x' - elif has_web: - return 'web' - else: - return 'web' # Fallback: assistant WebSearch (no API keys needed) + return 'web' if has_web else 'none' def has_web_search_keys(config: Dict[str, Any]) -> bool: @@ -291,7 +288,7 @@ def get_missing_keys(config: Dict[str, Any]) -> str: Returns: 'all', 'both', 'reddit', 'x', 'web', or 'none' """ - has_reddit = is_reddit_available(config) + has_reddit = True has_xai = bool(config.get('XAI_API_KEY')) has_web = has_web_search_keys(config) @@ -305,12 +302,11 @@ def get_missing_keys(config: Dict[str, Any]) -> str: return 'none' elif has_reddit and has_x: return 'web' # Missing web search keys + elif has_reddit and has_web: + return 'x' # Missing X source elif has_reddit: return 'x' # Missing X source (and possibly web) - elif has_x: - return 'reddit' # Missing Reddit source (and possibly web) - else: - return 'all' # Missing everything + return 'all' def validate_sources(requested: str, available: str, include_web: bool = False) -> tuple[str, Optional[str]]: @@ -324,56 +320,51 @@ def validate_sources(requested: str, available: str, include_web: bool = False) Returns: Tuple of (effective_sources, error_message) """ - # No API keys at all - if available == 'none': - if requested == 'auto': - return 'web', "No API keys configured. The assistant can still search the web if it has a search tool." - elif requested == 'web': - return 'web', None - else: - return 'web', f"No API keys configured. Add keys to ~/.config/last30days/.env for Reddit/X." - - # Web-only mode (only web search API keys) - if available == 'web': - if requested == 'auto': - return 'web', None - elif requested == 'web': - return 'web', None - else: - return 'web', "Only web search keys configured. Add OPENAI_API_KEY (or run codex login) for Reddit, XAI_API_KEY for X." + has_reddit = available in ('reddit', 'both', 'reddit-web', 'all') + has_x = available in ('x', 'both', 'x-web', 'all') + has_web = available in ('web', 'reddit-web', 'x-web', 'all') if requested == 'auto': - # Add web to sources if include_web is set + if has_reddit and has_x: + base = 'both' + elif has_reddit: + base = 'reddit' + elif has_x: + base = 'x' + elif has_web: + base = 'web' + else: + return 'none', "No sources are available." + if include_web: - if available == 'both': - return 'all', None # reddit + x + web - elif available == 'reddit': + if base == 'both': + return 'all', None + if base == 'reddit': return 'reddit-web', None - elif available == 'x': + if base == 'x': return 'x-web', None - return available, None + return base, None if requested == 'web': return 'web', None if requested == 'both': - if available not in ('both',): - missing = 'xAI' if available == 'reddit' else 'OpenAI' - return 'none', f"Requested both sources but {missing} key is missing. Use --sources=auto to use available keys." + if not (has_reddit and has_x): + return 'none', "Requested both sources but X source is missing." if include_web: return 'all', None return 'both', None if requested == 'reddit': - if available == 'x': + if not has_reddit: return 'none', "Requested Reddit but only xAI key is available." if include_web: return 'reddit-web', None return 'reddit', None if requested == 'x': - if available == 'reddit': - return 'none', "Requested X but only OpenAI key is available." + if not has_x: + return 'none', "Requested X but no X source is available (need Bird auth or XAI_API_KEY)." if include_web: return 'x-web', None return 'x', None @@ -459,6 +450,40 @@ def get_instagram_token(config: Dict[str, Any]) -> str: return config.get('SCRAPECREATORS_API_KEY') or '' +def get_xiaohongshu_api_base(config: Dict[str, Any]) -> str: + """Get Xiaohongshu HTTP API base URL. + + Defaults to host.docker.internal so OpenClaw Docker can reach host service. + """ + return (config.get('XIAOHONGSHU_API_BASE') or "http://host.docker.internal:18060").rstrip("/") + + +def is_xiaohongshu_available(config: Dict[str, Any]) -> bool: + """Check whether Xiaohongshu HTTP API is reachable and logged in.""" + # Import here to avoid heavy imports at module load. + from . import http + + base = get_xiaohongshu_api_base(config) + try: + # Keep health probe snappy, but allow one retry for transient hiccups. + health = http.get(f"{base}/health", timeout=3, retries=2) + if not isinstance(health, dict): + return False + if not health.get("success"): + return False + + # Login probe can be slower on some deployments (browser/session checks), + # so use a slightly longer timeout to avoid false negatives. + login = http.get(f"{base}/api/v1/login/status", timeout=8, retries=2) + is_logged_in = ( + login.get("data", {}).get("is_logged_in") + if isinstance(login, dict) else False + ) + return bool(is_logged_in) + except Exception: + return False + + # Backward compat alias is_apify_available = is_tiktok_available diff --git a/scripts/lib/openai_reddit.py b/scripts/lib/openai_reddit.py index 6a8f361..f98e8e4 100644 --- a/scripts/lib/openai_reddit.py +++ b/scripts/lib/openai_reddit.py @@ -355,6 +355,105 @@ def search_reddit( raise http.HTTPError("No models available") +def _public_relevance(score: int, num_comments: int) -> float: + """Estimate relevance for public Reddit search results.""" + # Lightweight heuristic: blend normalized score + comments. + score_component = min(1.0, max(0.0, score / 500.0)) + comments_component = min(1.0, max(0.0, num_comments / 200.0)) + return round((score_component * 0.6) + (comments_component * 0.4), 3) + + +def search_reddit_public( + topic: str, + from_date: str, + to_date: str, + depth: str = "default", +) -> List[Dict[str, Any]]: + """Search Reddit directly via public JSON endpoint (no OpenAI key required). + + This is a fallback mode for environments where OpenAI auth is unavailable. + It uses reddit.com/search/.json with recency filter (t=month). + """ + _, max_items = DEPTH_CONFIG.get(depth, DEPTH_CONFIG["default"]) + limit = min(100, max(20, max_items)) + + core = _extract_core_subject(topic) + queries = [topic] + if core and core.lower() != topic.lower(): + queries.append(core) + queries.append(f'"{core}"') + + seen_urls = set() + all_items: List[Dict[str, Any]] = [] + + headers = { + "User-Agent": http.USER_AGENT, + "Accept": "application/json", + } + + for query in queries: + try: + url = ( + "https://www.reddit.com/search/.json" + f"?q={_url_encode(query)}&sort=new&t=month&limit={limit}&raw_json=1" + ) + data = http.get(url, headers=headers, timeout=20, retries=2) + children = data.get("data", {}).get("children", []) + for child in children: + if child.get("kind") != "t3": + continue + post = child.get("data", {}) + permalink = str(post.get("permalink", "")).strip() + if not permalink or "/comments/" not in permalink: + continue + + full_url = f"https://www.reddit.com{permalink}" + if full_url in seen_urls: + continue + seen_urls.add(full_url) + + score = int(post.get("score", 0) or 0) + num_comments = int(post.get("num_comments", 0) or 0) + + # Parse date from created_utc + created_utc = post.get("created_utc") + date_value = None + if created_utc: + from . import dates as dates_mod + date_value = dates_mod.timestamp_to_date(created_utc) + + all_items.append({ + "id": f"R{len(all_items)+1}", + "title": str(post.get("title", "")).strip(), + "url": full_url, + "subreddit": str(post.get("subreddit", "")).strip(), + "date": date_value, + "why_relevant": "Found via Reddit public search", + "relevance": _public_relevance(score, num_comments), + "engagement": { + "score": score, + "num_comments": num_comments, + "upvote_ratio": post.get("upvote_ratio"), + }, + }) + + except http.HTTPError as e: + _log_info(f"Public Reddit search failed for query '{query}': {e}") + # Continue with next query; partial results are still useful. + continue + except Exception as e: + _log_info(f"Public Reddit search error for query '{query}': {e}") + continue + + # Sort by date (desc, unknown dates last), then relevance desc + def _sort_key(item: Dict[str, Any]): + date_str = item.get("date") or "" + return (date_str, float(item.get("relevance", 0.0))) + + all_items.sort(key=_sort_key, reverse=True) + return all_items[: max_items * 2] + + def search_subreddits( subreddits: List[str], topic: str, diff --git a/scripts/lib/render.py b/scripts/lib/render.py index 1af86cf..7946b8c 100644 --- a/scripts/lib/render.py +++ b/scripts/lib/render.py @@ -510,6 +510,20 @@ def render_source_status(report: schema.Report, source_info: dict = None) -> str lines.append(f" ✅ Instagram: {len(report.instagram)} reels ({with_captions} with captions)") # Hide when zero results + # Xiaohongshu (from Web source bucket) + xhs_count = 0 + if report.web: + xhs_count = sum( + 1 for w in report.web + if getattr(w, "source_domain", "").lower().endswith("xiaohongshu.com") + ) + if xhs_count > 0: + lines.append(f" ✅ Xiaohongshu: {xhs_count} notes") + else: + reason = source_info.get("xiaohongshu_skip_reason") + if reason: + lines.append(f" ⚡ Xiaohongshu: {reason}") + # Hacker News if report.hackernews_error: lines.append(f" ❌ HN: error - {report.hackernews_error}") diff --git a/scripts/lib/ui.py b/scripts/lib/ui.py index 1aeff0b..2351344 100644 --- a/scripts/lib/ui.py +++ b/scripts/lib/ui.py @@ -426,12 +426,15 @@ def show_diagnostic_banner(diag: dict): bird_username, youtube, web_search_backend """ has_openai = diag.get("openai", False) + has_reddit_public = diag.get("reddit_public", False) + has_reddit = has_openai or has_reddit_public has_x = diag.get("x_source") is not None has_youtube = diag.get("youtube", False) + has_xiaohongshu = diag.get("xiaohongshu", False) has_web = diag.get("web_search_backend") is not None # If everything is available, no banner needed - if has_openai and has_x and has_youtube and has_web: + if has_reddit and has_x and has_youtube and has_web: return lines = [] @@ -443,7 +446,9 @@ def show_diagnostic_banner(diag: dict): # Reddit if has_openai: - lines.append(f"{Colors.DIM}│{Colors.RESET} {Colors.GREEN}✅ Reddit{Colors.RESET} — OPENAI_API_KEY found {Colors.DIM}│{Colors.RESET}") + lines.append(f"{Colors.DIM}│{Colors.RESET} {Colors.GREEN}✅ Reddit{Colors.RESET} — OpenAI/Codex auth found {Colors.DIM}│{Colors.RESET}") + elif has_reddit_public: + lines.append(f"{Colors.DIM}│{Colors.RESET} {Colors.GREEN}✅ Reddit{Colors.RESET} — Public Reddit search (no key) {Colors.DIM}│{Colors.RESET}") else: lines.append(f"{Colors.DIM}│{Colors.RESET} {Colors.RED}❌ Reddit{Colors.RESET} — No OPENAI_API_KEY {Colors.DIM}│{Colors.RESET}") lines.append(f"{Colors.DIM}│{Colors.RESET} └─ Add to ~/.config/last30days/.env {Colors.DIM}│{Colors.RESET}") @@ -469,6 +474,12 @@ def show_diagnostic_banner(diag: dict): lines.append(f"{Colors.DIM}│{Colors.RESET} {Colors.RED}❌ YouTube{Colors.RESET} — yt-dlp not installed {Colors.DIM}│{Colors.RESET}") lines.append(f"{Colors.DIM}│{Colors.RESET} └─ Fix: brew install yt-dlp (free) {Colors.DIM}│{Colors.RESET}") + # Xiaohongshu + if has_xiaohongshu: + lines.append(f"{Colors.DIM}│{Colors.RESET} {Colors.GREEN}✅ Xiaohongshu{Colors.RESET} — API connected + logged in {Colors.DIM}│{Colors.RESET}") + else: + lines.append(f"{Colors.DIM}│{Colors.RESET} {Colors.YELLOW}⚡ Xiaohongshu{Colors.RESET} — API not connected/logged in {Colors.DIM}│{Colors.RESET}") + # Web if has_web: backend = diag.get("web_search_backend", "") @@ -486,7 +497,9 @@ def show_diagnostic_banner(diag: dict): lines.append("│ │") if has_openai: - lines.append("│ ✅ Reddit — OPENAI_API_KEY found │") + lines.append("│ ✅ Reddit — OpenAI/Codex auth found │") + elif has_reddit_public: + lines.append("│ ✅ Reddit — Public Reddit search (no key) │") else: lines.append("│ ❌ Reddit — No OPENAI_API_KEY │") lines.append("│ └─ Add to ~/.config/last30days/.env │") @@ -506,6 +519,11 @@ def show_diagnostic_banner(diag: dict): lines.append("│ ❌ YouTube — yt-dlp not installed │") lines.append("│ └─ Fix: brew install yt-dlp (free) │") + if has_xiaohongshu: + lines.append("│ ✅ Xiaohongshu — API connected + logged in │") + else: + lines.append("│ ⚡ Xiaohongshu — API not connected/logged in │") + if has_web: lines.append("│ ✅ Web — API search available │") else: diff --git a/scripts/lib/xiaohongshu_api.py b/scripts/lib/xiaohongshu_api.py new file mode 100644 index 0000000..12cee07 --- /dev/null +++ b/scripts/lib/xiaohongshu_api.py @@ -0,0 +1,162 @@ +"""Xiaohongshu HTTP API search client for last30days. + +Uses xpzouying/xiaohongshu-mcp REST endpoints: +- GET/POST /api/v1/feeds/search +- GET /api/v1/login/status +""" + +from datetime import datetime, timezone +from typing import Any, Dict, List, Optional + +from . import http + + +def _to_int(value: Any) -> int: + """Convert Xiaohongshu count strings to int. + + Supports plain ints and Chinese suffixes like 1.2万 / 3亿. + """ + if value is None: + return 0 + if isinstance(value, (int, float)): + return int(value) + + text = str(value).strip().lower().replace(",", "") + if not text: + return 0 + + try: + if text.endswith("万"): + return int(float(text[:-1]) * 10000) + if text.endswith("亿"): + return int(float(text[:-1]) * 100000000) + return int(float(text)) + except (TypeError, ValueError): + return 0 + + +def _timestamp_to_date_ms(ts: Any) -> Optional[str]: + """Convert millisecond timestamp to YYYY-MM-DD.""" + try: + iv = int(ts) + if iv <= 0: + return None + # API examples use milliseconds. + dt = datetime.fromtimestamp(iv / 1000.0, tz=timezone.utc) + return dt.strftime("%Y-%m-%d") + except (TypeError, ValueError, OSError): + return None + + +def _relevance_from_interactions(likes: int, comments: int, favorites: int) -> float: + """Heuristic relevance score from engagement metrics.""" + # Weighted engagement with soft caps to [0, 1]. + weighted = (likes * 1.0) + (comments * 2.5) + (favorites * 1.5) + # 5000 weighted engagement ~= strong relevance. + score = min(1.0, max(0.05, weighted / 5000.0)) + return round(score, 3) + + +def _build_note_url(feed_id: str, xsec_token: str) -> str: + """Build a stable Xiaohongshu note URL.""" + if xsec_token: + return f"https://www.xiaohongshu.com/explore/{feed_id}?xsec_token={xsec_token}" + return f"https://www.xiaohongshu.com/explore/{feed_id}" + + +def search_feeds( + topic: str, + from_date: str, + to_date: str, + base_url: str, + depth: str = "default", +) -> List[Dict[str, Any]]: + """Search Xiaohongshu feeds and normalize to web-item shape.""" + base = (base_url or "").rstrip("/") + if not base: + raise ValueError("Missing Xiaohongshu API base URL") + + # Quick login sanity check. + login = http.get(f"{base}/api/v1/login/status", timeout=8, retries=1) + is_logged_in = ( + login.get("data", {}).get("is_logged_in") + if isinstance(login, dict) else False + ) + if not is_logged_in: + raise http.HTTPError("Xiaohongshu API reachable but not logged in") + + # API supports filters; use recency-oriented defaults. + publish_time = "一天内" if depth == "quick" else "一周内" if depth == "default" else "半年内" + payload = { + "keyword": topic, + "filters": { + "sort_by": "综合", + "note_type": "不限", + "publish_time": publish_time, + "search_scope": "不限", + "location": "不限", + }, + } + + resp = http.post(f"{base}/api/v1/feeds/search", payload, timeout=20, retries=1) + feeds = resp.get("data", {}).get("feeds", []) if isinstance(resp, dict) else [] + if not isinstance(feeds, list): + feeds = [] + + # Cap source volume similarly to other web sources. + limit = {"quick": 8, "default": 15, "deep": 25}.get(depth, 15) + items: List[Dict[str, Any]] = [] + + for i, feed in enumerate(feeds[:limit]): + if not isinstance(feed, dict): + continue + note = feed.get("noteCard") or {} + if not isinstance(note, dict): + note = {} + interact = note.get("interactInfo") or {} + if not isinstance(interact, dict): + interact = {} + + feed_id = str(feed.get("id") or note.get("noteId") or "").strip() + if not feed_id: + continue + + xsec_token = str(feed.get("xsecToken") or note.get("xsecToken") or "").strip() + title = str( + note.get("displayTitle") + or note.get("title") + or "" + ).strip() + snippet = str( + note.get("desc") + or note.get("displayDesc") + or title + or "" + ).strip() + + likes = _to_int(interact.get("likedCount")) + comments = _to_int(interact.get("commentCount")) + favorites = _to_int(interact.get("collectedCount")) + + date_value = _timestamp_to_date_ms(note.get("time")) + why = f"Xiaohongshu engagement: likes={likes}, comments={comments}, favorites={favorites}" + + items.append({ + "id": f"XHS{i+1}", + "title": title[:200] if title else f"Xiaohongshu note {feed_id}", + "url": _build_note_url(feed_id, xsec_token), + "source_domain": "xiaohongshu.com", + "snippet": snippet[:500], + "date": date_value, + "date_confidence": "high" if date_value else "low", + "relevance": _relevance_from_interactions(likes, comments, favorites), + "why_relevant": why, + # Keep raw engagement for debugging/possible future rendering. + "engagement": { + "likes": likes, + "comments": comments, + "favorites": favorites, + }, + }) + + return items diff --git a/scripts/sync.sh b/scripts/sync.sh index 2eddbe1..e47eb74 100755 --- a/scripts/sync.sh +++ b/scripts/sync.sh @@ -8,7 +8,6 @@ echo "Source: $SRC" TARGETS=( "$HOME/.claude/skills/last30days" - "$HOME/.claude/skills/last30daysCROSS" "$HOME/.agents/skills/last30days" "$HOME/.codex/skills/last30days" ) @@ -18,18 +17,7 @@ for t in "${TARGETS[@]}"; do echo "--- Syncing to $t ---" mkdir -p "$t/scripts/lib" - # SKILL.md — CROSS gets patched frontmatter + skill root, others get verbatim copy - if [[ "$t" == *"last30daysCROSS"* ]]; then - sed \ - -e 's/^name: last30days$/name: last30daysCROSS/' \ - -e 's/^version: "2\.1"/version: "2.2-cross"/' \ - -e "s|^description: .*|description: \"TEST BUILD with outcome-aware Polymarket scoring + cross-source linking. Research a topic from the last 30 days. Sources: Reddit, X, YouTube, Hacker News, Polymarket, web.\"|" \ - -e "s/^argument-hint: .*/argument-hint: 'last30daysCROSS AI video tools'/" \ - -e 's|"\$HOME/.claude/skills/last30days"|"$HOME/.claude/skills/last30daysCROSS" \\\n "$HOME/.claude/skills/last30days"|' \ - "$SRC/SKILL.md" > "$t/SKILL.md" - else - cp "$SRC/SKILL.md" "$t/" - fi + cp "$SRC/SKILL.md" "$t/" # Main script + lib modules (rsync handles identical files gracefully) rsync -a "$SRC/scripts/last30days.py" "$t/scripts/" diff --git a/scripts/test-v1-vs-v2.sh b/scripts/test-v1-vs-v2.sh new file mode 100755 index 0000000..8935910 --- /dev/null +++ b/scripts/test-v1-vs-v2.sh @@ -0,0 +1,219 @@ +#!/bin/bash +set -euo pipefail + +# === V1 vs V2 Skill Test Harness === +# Runs all 17 test queries through both v1 and v2 SKILL.md +# using `claude --print` to capture real end-to-end output. + +SKILL_DIR="$HOME/.claude/skills/last30days" +REPO_DIR="/Users/mvanhorn/last30days-skill-private" + +# Safety: always restore V2 SKILL.md on exit/crash +cleanup() { + if [ -f "$SKILL_DIR/SKILL.md.v2.bak" ]; then + echo "" + echo "⚠️ Restoring V2 SKILL.md from backup (script interrupted)..." + cp "$SKILL_DIR/SKILL.md.v2.bak" "$SKILL_DIR/SKILL.md" + rm -f "$SKILL_DIR/SKILL.md.v2.bak" + echo " ✅ V2 restored" + fi +} +trap cleanup EXIT +TIMESTAMP=$(date +%Y%m%d-%H%M%S) +OUT_DIR="$REPO_DIR/docs/test-results/v1-vs-v2-${TIMESTAMP}" +V1_DIR="$OUT_DIR/v1" +V2_DIR="$OUT_DIR/v2" + +mkdir -p "$V1_DIR" "$V2_DIR" + +echo "📁 Output directory: $OUT_DIR" +echo "" + +# All 17 test queries +QUERIES=( + "prompting techniques for chatgpt for legal questions" + "best clawdbot use cases" + "how to best setup clawdbot" + "prompting tips for nano banana pro for ios designs" + "top claude code skills" + "using ChatGPT to make images of dogs" + "research best practices for beautiful remotion animation videos in claude code" + "photorealistic people in nano banana pro" + "What are the best rap songs lately" + "what are people saying about DeepSeek R1" + "best practices for cursor rules files for Cursor" + "prompt advice for using suno to make killer songs in simple mode" + "how do I use Codex with Claude Code on same app to make it better" + "kanye west" + "howie.ai" + "open claw" + "nano banana pro prompting" +) + +TYPES=( + "PROMPTING+TOOL" + "RECOMMENDATIONS" + "HOW-TO" + "PROMPTING+TOOL" + "RECOMMENDATIONS" + "GENERAL" + "PROMPTING" + "PROMPTING" + "RECOMMENDATIONS" + "NEWS" + "PROMPTING" + "PROMPTING" + "HOW-TO" + "NEWS" + "GENERAL" + "GENERAL" + "PROMPTING" +) + +slugify() { + echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | cut -c1-50 +} + +run_version() { + local version="$1" + local outdir="$2" + local total=${#QUERIES[@]} + + echo "" + echo "==========================================" + echo " Running $version — $total queries" + echo "==========================================" + echo "" + + for i in "${!QUERIES[@]}"; do + local query="${QUERIES[$i]}" + local type="${TYPES[$i]}" + local slug + slug=$(slugify "$query") + local num=$((i + 1)) + local outfile="$outdir/${num}-${slug}.txt" + local errfile="$outdir/${num}-${slug}.stderr.txt" + + echo "[$version] ($num/$total) $query [$type]" + + local start_time + start_time=$(date +%s) + + # Run claude --print with the skill invocation + # No timeout — claude --print exits on its own; kill manually if stuck + if /Users/mvanhorn/.local/bin/claude --print \ + "/last30days $query" \ + > "$outfile" 2>"$errfile"; then + local end_time + end_time=$(date +%s) + local duration=$((end_time - start_time)) + local lines + lines=$(wc -l < "$outfile") + echo " ✅ Done — ${lines} lines, ${duration}s" + else + local exit_code=$? + echo " ❌ Failed (exit $exit_code)" | tee -a "$outfile" + fi + + # Brief pause between queries to avoid rate limits + sleep 3 + done +} + +# === Phase 1: Test V1 === +echo "📦 Backing up current V2 SKILL.md..." +cp "$SKILL_DIR/SKILL.md" "$SKILL_DIR/SKILL.md.v2.bak" + +echo "📥 Installing V1 SKILL.md from upstream..." +cd "$REPO_DIR" +git show upstream/main:SKILL.md | sed '/^context: fork$/d; /^agent: Explore$/d; /^disable-model-invocation: true$/d' > "$SKILL_DIR/SKILL.md" +cp "$SKILL_DIR/SKILL.md" "$OUT_DIR/v1-SKILL.md" +echo " ✅ V1 installed (stripped: context:fork, agent:Explore, disable-model-invocation)" + +run_version "V1" "$V1_DIR" + +# === Phase 2: Test V2 === +echo "" +echo "📥 Restoring V2 SKILL.md..." +cp "$SKILL_DIR/SKILL.md.v2.bak" "$SKILL_DIR/SKILL.md" +cp "$SKILL_DIR/SKILL.md" "$OUT_DIR/v2-SKILL.md" +echo " ✅ V2 restored" + +run_version "V2" "$V2_DIR" + +# === Phase 3: Generate summary === +echo "" +echo "==========================================" +echo " Generating comparison summary" +echo "==========================================" + +SUMMARY="$OUT_DIR/comparison-summary.md" + +cat > "$SUMMARY" << EOF +# V1 vs V2 Comparison Results + +Generated: $(date) +Output directory: $OUT_DIR + +## Output Files + +| # | Query | Type | V1 Lines | V2 Lines | V1 Time | V2 Time | +|---|-------|------|----------|----------|---------|---------| +EOF + +for i in "${!QUERIES[@]}"; do + query="${QUERIES[$i]}" + type="${TYPES[$i]}" + slug=$(slugify "$query") + num=$((i + 1)) + + v1file="$V1_DIR/${num}-${slug}.txt" + v2file="$V2_DIR/${num}-${slug}.txt" + + v1lines=$(wc -l < "$v1file" 2>/dev/null || echo "ERR") + v2lines=$(wc -l < "$v2file" 2>/dev/null || echo "ERR") + + echo "| $num | \`$query\` | $type | $v1lines | $v2lines | — | — |" >> "$SUMMARY" +done + +cat >> "$SUMMARY" << 'EOF' + +## Quick Check: Key Features + +For each query, check these v2 improvements: + +- [ ] Query parsing display (`🔍 **{TOPIC}** · {QUERY_TYPE}`) +- [ ] Sparse citations (not every sentence) +- [ ] Bold topic headers in summary +- [ ] Emoji stats tree (`├─ 🟠 Reddit:`) +- [ ] Quality checklist applied to prompts +- [ ] Self-check (research grounding, not generic) + +## Scoring Guide + +Use the full scoring rubric from: +`docs/plans/2026-02-06-test-v1-vs-v2-comparison-plan.md` + +## Next Step + +Have Claude read all 34 output files and generate scored comparison: +``` +Read all files in docs/test-results/v1-vs-v2-*/v1/ and v2/ +Score each on the 7 dimensions from the test plan +Write the final analysis to docs/test-results/v1-vs-v2-*/analysis.md +``` +EOF + +# Cleanup backup +rm -f "$SKILL_DIR/SKILL.md.v2.bak" + +echo "" +echo "✅ All done!" +echo "" +echo "📁 Results: $OUT_DIR" +echo "📊 Summary: $SUMMARY" +echo "📄 V1 files: $V1_DIR/" +echo "📄 V2 files: $V2_DIR/" +echo "" +echo "To review:" +echo " open $OUT_DIR" diff --git a/test-run.log b/test-run.log new file mode 100644 index 0000000..c3b2b5a --- /dev/null +++ b/test-run.log @@ -0,0 +1,11 @@ +📁 Output directory: /Users/mvanhorn/last30days-skill-private/docs/test-results/v1-vs-v2-20260206-231338 + +📦 Backing up current V2 SKILL.md... +📥 Installing V1 SKILL.md from upstream... + ✅ V1 installed (stripped: context:fork, agent:Explore, disable-model-invocation) + +========================================== + Running V1 — 17 queries +========================================== + +[V1] (1/17) prompting techniques for chatgpt for legal questions [PROMPTING+TOOL] diff --git a/vendor/package/CHANGELOG.md b/vendor/package/CHANGELOG.md new file mode 100644 index 0000000..3773fd3 --- /dev/null +++ b/vendor/package/CHANGELOG.md @@ -0,0 +1,176 @@ +# Changelog + +## 0.8.0 — 2026-01-19 + +### Added +- `bookmarks` thread expansion controls (`--expand-root-only`, `--author-chain`, `--author-only`, `--full-chain-only`, `--include-ancestor-branches`, `--include-parent`, `--thread-meta`, `--sort-chronological`) for richer context exports (#55) — thanks @kkretschmer2. +- `--chrome-profile-dir` to point at Chromium profile directories or cookie DB files (Arc/Brave/etc) for cookie extraction (#16) — thanks @tekumara. +- `about` command to report account origin/location metadata (#51) — thanks @pjtf93. +- `follow`/`unfollow` commands to manage follows (#54) — thanks @citizenlee. +- Twitter client now supports like/unlike/retweet/unretweet/bookmark via the engagement mixin (#53) — thanks @the-vampiire. + +### Fixed +- `bookmarks` expanded JSON now preserves pagination `nextCursor`, and full-chain filtering only includes ancestor branches when requested. +- Follow/unfollow REST fallback now supports cursor pagination for followers/following (#54). +- About account live coverage now verifies data extraction paths (#51) — thanks @pjtf93. + +### Tests +- Live tests now exercise engagement mutations (opt-in) (#53) — thanks @the-vampiire. + +## 0.7.0 — 2026-01-12 + +### Added +- `home` command for the "For You" and "Following" home timelines (#31) — thanks @odysseus0. +- `news`/`trending` command for Explore tabs with AI-curated headlines (#39) — thanks @aavetis. +- `user-tweets` command to fetch a user's profile timeline (#34) — thanks @crcatala. +- `replies` and `thread` now support pagination (`--all`, `--max-pages`, `--cursor`, `--delay`) (#35) — thanks @crcatala. +- `search` now supports pagination (`--all`, `--max-pages`, `--cursor`) (#42) — thanks @pjtf93. +- `likes` now supports pagination (`--all`, `--max-pages`, `--cursor`) (#44) — thanks @jsholmes. +- `list-timeline` now supports pagination (`--all`, `--max-pages`, `--cursor`) (#30) — thanks @zheli. +- Rich text output now shows article previews, quoted tweets, and media links (#32) — thanks @odysseus0. +- Long-form article tweets now render rich Draft.js content blocks/entities (#36) — thanks @crcatala. + +### Changed +- Library typing: `SearchResult` is now a discriminated union (so `error` only exists when `success: false`). + +### Fixed +- Lists GraphQL feature flags updated to prevent 400s (#27) — thanks @zheli. +- Lists feature overrides now scope new GraphQL flags correctly (#50) — thanks @ryanh-ai. +- Tweet detail parsing now tolerates partial GraphQL errors when usable data exists (#48) — thanks @jsholmes. +- News output now respects `--tweets-per-item`, keeps unique IDs, and parses non-add entry instructions (#39) — thanks @aavetis. +- Following/followers pagination now guards repeat cursors and standardizes JSON output (#28) — thanks @malpern. +- Likes pagination now follows cursors and avoids stalling on duplicate pages (#12) — thanks @titouv. +- macOS cookie extraction now supports Brave keychain storage (#40) — thanks @gakonst. +- Terminal hyperlinks now sanitize control characters before emitting OSC 8 sequences (#29) — thanks @mafulafunk. +- `pnpm run build:dist` now succeeds after tightening JSON/pagination option typing in tweet output commands. + +### Tests +- Following: split following/likes tests + cover cursor handling (#33) — thanks @VACInc. + +## 0.6.0 — 2026-01-05 + +### Added +- Bookmark exports now support pagination (`--all`, `--max-pages`) with retries (#15) — thanks @Nano1337. +- `lists` + `list-timeline` commands for Twitter Lists (#21) — thanks @harperreed +- Tweet JSON output now includes media items (photos, videos, GIFs) (#14) — thanks @Hormold +- Bookmarks can resume pagination from a cursor (#26) — thanks @leonho +- `unbookmark` command to remove bookmarked tweets (#22) — thanks @mbelinky. + +### Changed +- Feature flags can be overridden at runtime via `features.json` (refreshable via `query-ids`). + +### Fixed +- GraphQL feature flags now include `post_ctas_fetch_enabled` to avoid 400s (#38) — thanks @philipp-spiess. + +## 0.5.1 — 2026-01-01 + +### Changed +- `bird --help` now includes explicit “Shortcuts” and “JSON Output” sections (documents `bird ` shorthand + `--json`). +- Release docs now include explicit npm publish verification steps. + +### Fixed +- `pnpm bird --help` now works (dev script runs the CLI entrypoint, not the library entrypoint). +- `following`/`followers` now fall back to internal v1.1 REST endpoints when GraphQL returns `404`. + +### Tests +- Add root help output regression test. +- Add opt-in live CLI test suite (real GraphQL calls; skipped by default; gated via `BIRD_LIVE=1`). + +## 0.5.0 — 2026-01-01 + +### Added +- `likes` command to list your liked tweets (thanks @swairshah). +- Quoted tweet data in JSON output + `--quote-depth` (thanks @alexknowshtml). +- `following`/`followers` commands to list users (thanks @lockmeister). + +### Changed +- Query ID updater now tracks the Likes GraphQL operation. +- Query ID updater now tracks Following/Followers GraphQL operations. +- Query ID updater now tracks BookmarkFolderTimeline and keeps bookmark query IDs seeded. +- `following`/`followers` JSON user fields are now camelCase (`followersCount`, `followingCount`, `isBlueVerified`, `profileImageUrl`, `createdAt`). +- Cookie extraction timeout is now configurable (default 30s on macOS) via `--cookie-timeout` / `BIRD_COOKIE_TIMEOUT_MS` (thanks @tylerseymour). +- Search now paginates beyond 20 results when using `-n` (thanks @ryanh-ai). +- Library exports are now separated from the CLI entrypoint for easier embedding. + +## 0.4.1 — 2025-12-31 + +### Added +- `bookmarks` command to list your bookmarked tweets. +- `bookmarks --folder-id` to fetch bookmark folders (thanks @tylerseymour). + +### Changed +- Cookie extraction now uses `@steipete/sweet-cookie` (drops `sqlite3` CLI + custom browser readers in `bird`). +- Query ID updater now tracks the Bookmarks GraphQL operation. +- Lint rules stricter (block statements, no-negation-else, useConst/useTemplate, top-level regex, import extension enforcement). +- `pnpm lint` now runs both Biome and oxlint (type-aware). + +### Tests +- Coverage thresholds raised to 90% statements/lines/functions (80% branches). +- Added targeted Twitter client coverage suites. + +## 0.4.0 — 2025-12-26 + +### Added +- Cookie source selection: `--cookie-source safari|chrome|firefox` (repeatable) + `cookieSource` config (string or array). + +### Fixed +- `tweet`/`reply`: fallback to `statuses/update.json` when GraphQL `CreateTweet` returns error 226 (“automated request”). + +### Breaking +- Remove `allowSafari`/`allowChrome`/`allowFirefox` config toggles in favor of `cookieSource` ordering. + +## 0.3.0 — 2025-12-26 + +### Added +- Safari cookie extraction (`Cookies.binarycookies`) + `allowSafari` config toggle. + +### Changed +- Removed the Sweetistics engine + fallback. `bird` is GraphQL-only. +- Browser cookie fallback order: Safari → Chrome → Firefox. + +### Tests +- Enforce coverage thresholds (>= 70% statements/branches/functions/lines) + expand unit coverage for version/output/Twitter client branches. + +## 0.2.0 — 2025-12-26 + +### Added +- Output controls: `--plain`, `--no-emoji`, `--no-color` (respects `NO_COLOR`). +- `help` command: `bird help `. +- Runtime GraphQL query ID refresh: `bird query-ids --fresh` (cached on disk; auto-retry on 404; override cache via `BIRD_QUERY_IDS_CACHE`). +- GraphQL media uploads via `--media` (up to 4 images/GIFs, or 1 video). + +### Fixed +- CLI `--version`: read version from `package.json`/`VERSION` (no hardcoded string) + append git sha when available. + +### Changed +- `mentions`: no hardcoded user; defaults to authenticated user or accepts `--user @handle`. +- GraphQL query ID updater: correctly pairs `operationName` ↔ `queryId` (CreateTweet/CreateRetweet/etc). +- `build:dist`: copies `src/lib/query-ids.json` into `dist/lib/query-ids.json` (keeps `dist/` in sync). +- `--engine graphql`: strict GraphQL-only (disables Sweetistics fallback). + +## 0.1.1 — 2025-12-26 + +### Changed +- Engine default now `auto` (GraphQL primary; Sweetistics only on fallback when configured). + +### Tests +- Add engine resolution tests for auto/default behavior. + +### Fixed +- GraphQL read: rotate TweetDetail query IDs with fallback to avoid 404s. + +## 0.1.0 — 2025-12-20 + +### Added +- CLI commands: `tweet`, `reply`, `read`, `replies`, `thread`, `search`, `mentions`, `whoami`, `check`. +- URL/ID shorthand for `read`, plus `--json` output where supported. +- GraphQL engine with cookie auth from Firefox/Chrome/env/flags (macOS browsers). +- Sweetistics engine (API key) with automatic fallback when configured. +- Media uploads via Sweetistics with per-item alt text (images or single video). +- Long-form Notes and Articles extraction for full text output. +- Thread + reply fetching with full conversation parsing. +- Search + mentions via GraphQL (latest timeline). +- JSON5 config files (`~/.config/bird/config.json5`, `./.birdrc.json5`) with engine defaults, profiles, allowChrome/allowFirefox, and timeoutMs. +- Request timeouts (`--timeout`, `timeoutMs`) for GraphQL and Sweetistics calls. +- Bun-compiled standalone binary via `pnpm run build`. +- Query ID refresh helper: `pnpm run graphql:update`. diff --git a/vendor/package/LICENSE b/vendor/package/LICENSE new file mode 100644 index 0000000..f7b5266 --- /dev/null +++ b/vendor/package/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Peter Steinberger + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/package/README.md b/vendor/package/README.md new file mode 100644 index 0000000..1538741 --- /dev/null +++ b/vendor/package/README.md @@ -0,0 +1,385 @@ +# bird 🐦 — fast X CLI for tweeting, replying, and reading + +`bird` is a fast X CLI for tweeting, replying, and reading via X/Twitter GraphQL (cookie auth). + +## Disclaimer + +This project uses X/Twitter’s **undocumented** web GraphQL API (and cookie auth). X can change endpoints, query IDs, +and anti-bot behavior at any time — **expect this to break without notice**. + +## Install + +```bash +npm install -g @steipete/bird +# or +pnpm add -g @steipete/bird +# or +bun add -g @steipete/bird + +# one-shot (no install) +bunx @steipete/bird whoami +``` + +Homebrew (macOS, prebuilt Bun binary): + +```bash +brew install steipete/tap/bird +``` + +## Quickstart + +```bash +# Show the logged-in account +bird whoami + +# Discover command help +bird help whoami + +# Read a tweet (URL or ID) +bird read https://x.com/user/status/1234567890123456789 +bird 1234567890123456789 --json + +# Thread + replies +bird thread https://x.com/user/status/1234567890123456789 +bird replies 1234567890123456789 +bird replies 1234567890123456789 --max-pages 3 --json +bird thread 1234567890123456789 --max-pages 3 --json + +# Search + mentions +bird search "from:steipete" -n 5 +bird mentions -n 5 +bird mentions --user @steipete -n 5 + +# User tweets (profile timeline) +bird user-tweets @steipete -n 20 +bird user-tweets @steipete -n 50 --json + +# Bookmarks +bird bookmarks -n 5 +bird bookmarks --folder-id 123456789123456789 -n 5 # https://x.com/i/bookmarks/ +bird bookmarks --all --json +bird bookmarks --all --max-pages 2 --json +bird bookmarks --include-parent --json +bird unbookmark 1234567890123456789 +bird unbookmark https://x.com/user/status/1234567890123456789 + +# Likes +bird likes -n 5 + +# News and trending topics (AI-curated from Explore tabs) +bird news --ai-only -n 10 +bird news --sports -n 5 + +# Lists +bird list-timeline 1234567890 -n 20 +bird list-timeline https://x.com/i/lists/1234567890 --all --json +bird list-timeline 1234567890 --max-pages 3 --json + +# Following (who you follow) +bird following -n 20 +bird following --user 12345678 -n 10 # by user ID + +# Followers (who follows you) +bird followers -n 20 +bird followers --user 12345678 -n 10 # by user ID + +# Refresh GraphQL query IDs cache (no rebuild) +bird query-ids --fresh +``` + +## News & Trending + +Fetch AI-curated news and trending topics from X's Explore page tabs: + +```bash +# Fetch 10 news items from all tabs (default: For You, News, Sports, Entertainment) +bird news -n 10 + +# Fetch only AI-curated news (filters out regular trends) +bird news --ai-only -n 20 + +# Fetch from specific tabs +bird news --news-only --ai-only -n 10 +bird news --sports -n 15 +bird news --entertainment --ai-only -n 5 + +# Include related tweets for each news item +bird news --with-tweets --tweets-per-item 3 -n 10 + +# Combine multiple tab filters +bird news --sports --entertainment -n 20 + +# JSON output +bird news --json -n 5 +bird news --json-full --ai-only -n 10 # includes raw API response +``` + +Tab options (can be combined): +- `--for-you` — Fetch from For You tab only +- `--news-only` — Fetch from News tab only +- `--sports` — Fetch from Sports tab only +- `--entertainment` — Fetch from Entertainment tab only +- `--trending-only` — Fetch from Trending tab only + +By default, the command fetches from For You, News, Sports, and Entertainment tabs (Trending excluded to reduce noise). Headlines are automatically deduplicated across tabs. + +## Library + +`bird` can be used as a library (same GraphQL client as the CLI): + +```ts +import { TwitterClient, resolveCredentials } from '@steipete/bird'; + +const { cookies } = await resolveCredentials({ cookieSource: 'safari' }); +const client = new TwitterClient({ cookies }); + +// Search for tweets +const searchResult = await client.search('from:steipete', 50); + +// Fetch news and trending topics from all tabs (default: For You, News, Sports, Entertainment) +const newsResult = await client.getNews(10, { aiOnly: true }); + +// Fetch from specific tabs with related tweets +const sportsNews = await client.getNews(10, { + aiOnly: true, + withTweets: true, + tabs: ['sports', 'entertainment'] +}); +``` + +Account details (About profile): + +```ts +const aboutResult = await client.getUserAboutAccount('steipete'); +if (aboutResult.success && aboutResult.aboutProfile) { + console.log(aboutResult.aboutProfile.accountBasedIn); +} +``` + +Fields: +- `accountBasedIn` +- `source` +- `createdCountryAccurate` +- `locationAccurate` +- `learnMoreUrl` + +## Commands + +- `bird tweet ""` — post a new tweet. +- `bird reply ""` — reply to a tweet using its ID or URL. +- `bird help [command]` — show help (or help for a subcommand). +- `bird query-ids [--fresh] [--json]` — inspect or refresh cached GraphQL query IDs. +- `bird home [-n count] [--following] [--json] [--json-full]` — fetch your home timeline (For You) or Following feed. +- `bird read [--json]` — fetch tweet content as text or JSON. +- `bird [--json]` — shorthand for `read` when only a URL or ID is provided. +- `bird replies [--all] [--max-pages n] [--cursor string] [--delay ms] [--json]` — list replies to a tweet. +- `bird thread [--all] [--max-pages n] [--cursor string] [--delay ms] [--json]` — show the full conversation thread. +- `bird search "" [-n count] [--all] [--max-pages n] [--cursor string] [--json]` — search for tweets matching a query; `--max-pages` requires `--all` or `--cursor`. +- `bird mentions [-n count] [--user @handle] [--json]` — find tweets mentioning a user (defaults to the authenticated user). +- `bird user-tweets <@handle> [-n count] [--cursor string] [--max-pages n] [--delay ms] [--json]` — get tweets from a user's profile timeline. +- `bird bookmarks [-n count] [--folder-id id] [--all] [--max-pages n] [--cursor string] [--expand-root-only] [--author-chain] [--author-only] [--full-chain-only] [--include-ancestor-branches] [--include-parent] [--thread-meta] [--sort-chronological] [--json]` — list your bookmarked tweets (or a specific bookmark folder); expansion flags control thread context; `--max-pages` requires `--all` or `--cursor`. +- `bird unbookmark ` — remove one or more bookmarks by tweet ID or URL. +- `bird likes [-n count] [--all] [--max-pages n] [--cursor string] [--json] [--json-full]` — list your liked tweets; `--max-pages` requires `--all` or `--cursor`. +- `bird news [-n count] [--ai-only] [--with-tweets] [--tweets-per-item n] [--for-you] [--news-only] [--sports] [--entertainment] [--trending-only] [--json]` — fetch news and trending topics from X's Explore tabs. +- `bird trending` — alias for `news` command. +- `bird lists [--member-of] [-n count] [--json]` — list your lists (owned or memberships). +- `bird list-timeline [-n count] [--all] [--max-pages n] [--cursor string] [--json]` — get tweets from a list timeline; `--max-pages` implies `--all`. +- `bird following [--user ] [-n count] [--cursor string] [--all] [--max-pages n] [--json]` — list users that you (or another user) follow; `--max-pages` requires `--all`. +- `bird followers [--user ] [-n count] [--cursor string] [--all] [--max-pages n] [--json]` — list users that follow you (or another user); `--max-pages` requires `--all`. +- `bird about <@handle> [--json]` — get account origin and location information for a user. +- `bird whoami` — print which Twitter account your cookies belong to. +- `bird check` — show which credentials are available and where they were sourced from. + +Bookmarks flags: +- `--expand-root-only`: expand threads only when the bookmark is a root tweet. +- `--author-chain`: keep only the bookmarked author's connected self-reply chain. +- `--author-only`: include all tweets from the bookmarked author within the thread. +- `--full-chain-only`: keep the entire reply chain connected to the bookmarked tweet (all authors). +- `--include-ancestor-branches`: include sibling branches for ancestors when using `--full-chain-only`. +- `--include-parent`: include the direct parent tweet for non-root bookmarks. +- `--thread-meta`: add thread metadata fields to each tweet. +- `--sort-chronological`: sort output globally oldest to newest (default preserves bookmark order). + +Global options: +- `--auth-token `: set the `auth_token` cookie manually. +- `--ct0 `: set the `ct0` cookie manually. +- `--cookie-source `: choose browser cookie source (repeatable; order matters). +- `--chrome-profile `: Chrome profile name for cookie extraction (e.g., `Default`, `Profile 2`). +- `--chrome-profile-dir `: Chrome/Chromium profile directory or cookie DB path for cookie extraction. +- `--firefox-profile `: Firefox profile for cookie extraction. +- `--cookie-timeout `: cookie extraction timeout for keychain/OS helpers (milliseconds). +- `--timeout `: abort requests after the given timeout (milliseconds). +- `--quote-depth `: max quoted tweet depth in JSON output (default: 1; 0 disables). +- `--plain`: stable output (no emoji, no color). +- `--no-emoji`: disable emoji output. +- `--no-color`: disable ANSI colors (or set `NO_COLOR=1`). +- `--media `: attach media file (repeatable, up to 4 images or 1 video). +- `--alt `: alt text for the corresponding `--media` (repeatable). + +## Authentication (GraphQL) + +GraphQL mode uses your existing X/Twitter web session (no password prompt). It sends requests to internal +X endpoints and authenticates via cookies (`auth_token`, `ct0`). + +Write operations: +- `tweet`/`reply` primarily use GraphQL (`CreateTweet`). +- If GraphQL returns error `226` (“automated request”), `bird` falls back to the legacy `statuses/update.json` endpoint. + +`bird` resolves credentials in this order: + +1. CLI flags: `--auth-token`, `--ct0` +2. Environment variables: `AUTH_TOKEN`, `CT0` (fallback: `TWITTER_AUTH_TOKEN`, `TWITTER_CT0`) +3. Browser cookies via `@steipete/sweet-cookie` (override via `--cookie-source` order) + +Browser cookie sources: +- Safari: `~/Library/Cookies/Cookies.binarycookies` (fallback: `~/Library/Containers/com.apple.Safari/Data/Library/Cookies/Cookies.binarycookies`) +- Chrome: `~/Library/Application Support/Google/Chrome//Cookies` +- Firefox: `~/Library/Application Support/Firefox/Profiles//cookies.sqlite` + - For Chromium variants (Arc/Brave/etc), pass a profile directory or cookie DB via `--chrome-profile-dir`. + +## Config (JSON5) + +Config precedence: CLI flags > env vars > project config > global config. + +- Global: `~/.config/bird/config.json5` +- Project: `./.birdrc.json5` + +Example `~/.config/bird/config.json5`: + +```json5 +{ + // Cookie source order for browser extraction (string or array) + cookieSource: ["firefox", "safari"], + chromeProfileDir: "/path/to/Chromium/Profile", + firefoxProfile: "default-release", + cookieTimeoutMs: 30000, + timeoutMs: 20000, + quoteDepth: 1 +} +``` + +Environment shortcuts: +- `BIRD_TIMEOUT_MS` +- `BIRD_COOKIE_TIMEOUT_MS` +- `BIRD_QUOTE_DEPTH` + +## Output + +- `--json` prints raw tweet objects for read/replies/thread/search/mentions/user-tweets/bookmarks/likes. +- When using `--json` with pagination (`--all`, `--cursor`, `--max-pages`, or for `user-tweets` when `-n > 20`), output is `{ tweets, nextCursor }`. +- `read` returns full text for Notes and Articles when present. +- Use `--plain` for stable, script-friendly output (no emoji, no color). + +### JSON Schema + +When using `--json`, tweet objects include: + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | Tweet ID | +| `text` | string | Full tweet text (includes Note/Article content when present) | +| `author` | object | `{ username, name }` | +| `authorId` | string? | Author's user ID | +| `createdAt` | string | Timestamp | +| `replyCount` | number | Number of replies | +| `retweetCount` | number | Number of retweets | +| `likeCount` | number | Number of likes | +| `conversationId` | string | Thread conversation ID | +| `inReplyToStatusId` | string? | Parent tweet ID (present if this is a reply) | +| `quotedTweet` | object? | Embedded quote tweet (same schema; depth controlled by `--quote-depth`) | + +When using `--json` with `following`/`followers`, user objects include: + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | User ID | +| `username` | string | Username/handle | +| `name` | string | Display name | +| `description` | string? | User bio | +| `followersCount` | number? | Followers count | +| `followingCount` | number? | Following count | +| `isBlueVerified` | boolean? | Blue verified flag | + | `profileImageUrl` | string? | Profile image URL | + | `createdAt` | string? | Account creation timestamp | + +When using `--json` with `news`/`trending`, news objects include: + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | Unique identifier for the news item | +| `headline` | string | News headline or trend title | +| `category` | string? | Category (e.g., "AI · Technology", "Trending", "News") | +| `timeAgo` | string? | Relative time (e.g., "2h ago") | +| `postCount` | number? | Number of posts | +| `description` | string? | Item description | +| `url` | string? | URL to the trend or news article | +| `tweets` | array? | Related tweets (only when `--with-tweets` is used) | +| `_raw` | object? | Raw API response (only when `--json-full` is used) | + + +## Query IDs (GraphQL) + +X rotates GraphQL “query IDs” frequently. Each GraphQL operation is addressed as: + +- `operationName` (e.g. `TweetDetail`, `CreateTweet`) +- `queryId` (rotating ID baked into X’s web client bundles) + +`bird` ships with a baseline mapping in `src/lib/query-ids.json` (copied into `dist/` on build). At runtime, +it can refresh that mapping by scraping X’s public web client bundles and caching the result on disk. + +Runtime cache: +- Default path: `~/.config/bird/query-ids-cache.json` +- Override path: `BIRD_QUERY_IDS_CACHE=/path/to/file.json` +- TTL: 24h (stale cache is still used, but marked “not fresh”) + +Auto-recovery: +- On GraphQL `404` (query ID invalid), `bird` forces a refresh once and retries. +- For `TweetDetail`/`SearchTimeline`, `bird` also rotates through a small set of known fallback IDs to reduce + breakage while refreshing. + +Refresh on demand: + +```bash +bird query-ids --fresh +``` + +Exit codes: +- `0`: success +- `1`: runtime error (network/auth/etc) +- `2`: invalid usage/validation (e.g. bad `--user` handle) + +## Version + +`bird --version` prints `package.json` version plus current git sha when available, e.g. `0.3.0 (3df7969b)`. + +## Media uploads + +- Attach media with `--media` (repeatable) and optional `--alt` per item. +- Up to 4 images/GIFs, or 1 video (no mixing). Supported: jpg, jpeg, png, webp, gif, mp4, mov. +- Images/GIFs + 1 video supported (uploads via Twitter legacy upload endpoint + cookies; video may take longer to process). + +Example: + +```bash +bird tweet "hi" --media img.png --alt "desc" +``` + +## Development + +```bash +cd ~/Projects/bird +pnpm install +pnpm run build # dist/ + bun binary +pnpm run build:dist # dist/ only +pnpm run build:binary + +pnpm run dev tweet "Test" +pnpm run dev -- --plain check +pnpm test +pnpm run lint +``` + +## Notes + +- GraphQL uses internal X endpoints and can be rate limited (429). +- Query IDs rotate; refresh at runtime with `bird query-ids --fresh` (or update the baked baseline via `pnpm run graphql:update`). diff --git a/vendor/package/dist/cli.d.ts b/vendor/package/dist/cli.d.ts new file mode 100644 index 0000000..de8e3a0 --- /dev/null +++ b/vendor/package/dist/cli.d.ts @@ -0,0 +1,12 @@ +#!/usr/bin/env node +/** + * bird - CLI tool for posting tweets and replies + * + * Usage: + * bird tweet "Hello world!" + * bird reply "This is a reply" + * bird reply "This is a reply" + * bird read + */ +export {}; +//# sourceMappingURL=cli.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/cli.d.ts.map b/vendor/package/dist/cli.d.ts.map new file mode 100644 index 0000000..c2665e7 --- /dev/null +++ b/vendor/package/dist/cli.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA;;;;;;;;GAQG"} \ No newline at end of file diff --git a/vendor/package/dist/cli.js b/vendor/package/dist/cli.js new file mode 100644 index 0000000..603db2d --- /dev/null +++ b/vendor/package/dist/cli.js @@ -0,0 +1,29 @@ +#!/usr/bin/env node +/** + * bird - CLI tool for posting tweets and replies + * + * Usage: + * bird tweet "Hello world!" + * bird reply "This is a reply" + * bird reply "This is a reply" + * bird read + */ +import { createProgram, KNOWN_COMMANDS } from './cli/program.js'; +import { createCliContext } from './cli/shared.js'; +import { resolveCliInvocation } from './lib/cli-args.js'; +const rawArgs = process.argv.slice(2); +const normalizedArgs = rawArgs[0] === '--' ? rawArgs.slice(1) : rawArgs; +const ctx = createCliContext(normalizedArgs); +const program = createProgram(ctx); +const { argv, showHelp } = resolveCliInvocation(normalizedArgs, KNOWN_COMMANDS); +if (showHelp) { + program.outputHelp(); + process.exit(0); +} +if (argv) { + program.parse(argv); +} +else { + program.parse(['node', 'bird', ...normalizedArgs]); +} +//# sourceMappingURL=cli.js.map \ No newline at end of file diff --git a/vendor/package/dist/cli.js.map b/vendor/package/dist/cli.js.map new file mode 100644 index 0000000..b732d67 --- /dev/null +++ b/vendor/package/dist/cli.js.map @@ -0,0 +1 @@ +{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA;;;;;;;;GAQG;AAEH,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAEzD,MAAM,OAAO,GAAa,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAChD,MAAM,cAAc,GAAa,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;AAElF,MAAM,GAAG,GAAG,gBAAgB,CAAC,cAAc,CAAC,CAAC;AAE7C,MAAM,OAAO,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;AAEnC,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,oBAAoB,CAAC,cAAc,EAAE,cAAc,CAAC,CAAC;AAEhF,IAAI,QAAQ,EAAE,CAAC;IACb,OAAO,CAAC,UAAU,EAAE,CAAC;IACrB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,IAAI,IAAI,EAAE,CAAC;IACT,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AACtB,CAAC;KAAM,CAAC;IACN,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,cAAc,CAAC,CAAC,CAAC;AACrD,CAAC"} \ No newline at end of file diff --git a/vendor/package/dist/cli/pagination.d.ts b/vendor/package/dist/cli/pagination.d.ts new file mode 100644 index 0000000..e8a4e26 --- /dev/null +++ b/vendor/package/dist/cli/pagination.d.ts @@ -0,0 +1,35 @@ +export type PaginationCmdOpts = { + all?: boolean; + maxPages?: string; + cursor?: string; + delay?: string; +}; +export declare function parsePositiveIntFlag(raw: string | undefined, flagName: string): { + ok: true; + value: number | undefined; +} | { + ok: false; + error: string; +}; +export declare function parseNonNegativeIntFlag(raw: string | undefined, flagName: string, defaultValue: number): { + ok: true; + value: number; +} | { + ok: false; + error: string; +}; +export declare function parsePaginationFlags(cmdOpts: PaginationCmdOpts, opts?: { + maxPagesImpliesPagination?: boolean; + defaultDelayMs?: number; + includeDelay?: boolean; +}): { + ok: true; + usePagination: boolean; + maxPages?: number; + cursor?: string; + pageDelayMs?: number; +} | { + ok: false; + error: string; +}; +//# sourceMappingURL=pagination.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/cli/pagination.d.ts.map b/vendor/package/dist/cli/pagination.d.ts.map new file mode 100644 index 0000000..ef39cbb --- /dev/null +++ b/vendor/package/dist/cli/pagination.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../../src/cli/pagination.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,QAAQ,EAAE,MAAM,GACf;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CASxE;AAED,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GACnB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAM5D;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,iBAAiB,EAC1B,IAAI,CAAC,EAAE;IACL,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,GAEC;IACE,EAAE,EAAE,IAAI,CAAC;IACT,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GACD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CA8B/B"} \ No newline at end of file diff --git a/vendor/package/dist/cli/pagination.js b/vendor/package/dist/cli/pagination.js new file mode 100644 index 0000000..3d7768a --- /dev/null +++ b/vendor/package/dist/cli/pagination.js @@ -0,0 +1,43 @@ +export function parsePositiveIntFlag(raw, flagName) { + if (raw === undefined) { + return { ok: true, value: undefined }; + } + const value = Number.parseInt(raw, 10); + if (!Number.isFinite(value) || value <= 0) { + return { ok: false, error: `Invalid ${flagName}. Expected a positive integer.` }; + } + return { ok: true, value }; +} +export function parseNonNegativeIntFlag(raw, flagName, defaultValue) { + const value = Number.parseInt(raw ?? String(defaultValue), 10); + if (!Number.isFinite(value) || value < 0) { + return { ok: false, error: `Invalid ${flagName}. Expected a non-negative integer.` }; + } + return { ok: true, value }; +} +export function parsePaginationFlags(cmdOpts, opts) { + const maxPagesImpliesPagination = opts?.maxPagesImpliesPagination ?? false; + const includeDelay = opts?.includeDelay ?? false; + const defaultDelayMs = opts?.defaultDelayMs ?? 1000; + const maxPages = parsePositiveIntFlag(cmdOpts.maxPages, '--max-pages'); + if (!maxPages.ok) { + return maxPages; + } + const usePagination = Boolean(cmdOpts.all || cmdOpts.cursor || (maxPagesImpliesPagination && maxPages.value !== undefined)); + let pageDelayMs; + if (includeDelay) { + const delay = parseNonNegativeIntFlag(cmdOpts.delay, '--delay', defaultDelayMs); + if (!delay.ok) { + return delay; + } + pageDelayMs = delay.value; + } + return { + ok: true, + usePagination, + maxPages: maxPages.value, + cursor: cmdOpts.cursor, + pageDelayMs, + }; +} +//# sourceMappingURL=pagination.js.map \ No newline at end of file diff --git a/vendor/package/dist/cli/pagination.js.map b/vendor/package/dist/cli/pagination.js.map new file mode 100644 index 0000000..3aac4df --- /dev/null +++ b/vendor/package/dist/cli/pagination.js.map @@ -0,0 +1 @@ +{"version":3,"file":"pagination.js","sourceRoot":"","sources":["../../src/cli/pagination.ts"],"names":[],"mappings":"AAOA,MAAM,UAAU,oBAAoB,CAClC,GAAuB,EACvB,QAAgB;IAEhB,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtB,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IACxC,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACvC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;QAC1C,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,WAAW,QAAQ,gCAAgC,EAAE,CAAC;IACnF,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,uBAAuB,CACrC,GAAuB,EACvB,QAAgB,EAChB,YAAoB;IAEpB,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,IAAI,MAAM,CAAC,YAAY,CAAC,EAAE,EAAE,CAAC,CAAC;IAC/D,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACzC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,WAAW,QAAQ,oCAAoC,EAAE,CAAC;IACvF,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,OAA0B,EAC1B,IAIC;IAUD,MAAM,yBAAyB,GAAG,IAAI,EAAE,yBAAyB,IAAI,KAAK,CAAC;IAC3E,MAAM,YAAY,GAAG,IAAI,EAAE,YAAY,IAAI,KAAK,CAAC;IACjD,MAAM,cAAc,GAAG,IAAI,EAAE,cAAc,IAAI,IAAI,CAAC;IAEpD,MAAM,QAAQ,GAAG,oBAAoB,CAAC,OAAO,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;IACvE,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,MAAM,aAAa,GAAG,OAAO,CAC3B,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,MAAM,IAAI,CAAC,yBAAyB,IAAI,QAAQ,CAAC,KAAK,KAAK,SAAS,CAAC,CAC7F,CAAC;IAEF,IAAI,WAA+B,CAAC;IACpC,IAAI,YAAY,EAAE,CAAC;QACjB,MAAM,KAAK,GAAG,uBAAuB,CAAC,OAAO,CAAC,KAAK,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC;QAChF,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC;YACd,OAAO,KAAK,CAAC;QACf,CAAC;QACD,WAAW,GAAG,KAAK,CAAC,KAAK,CAAC;IAC5B,CAAC;IAED,OAAO;QACL,EAAE,EAAE,IAAI;QACR,aAAa;QACb,QAAQ,EAAE,QAAQ,CAAC,KAAK;QACxB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,WAAW;KACZ,CAAC;AACJ,CAAC"} \ No newline at end of file diff --git a/vendor/package/dist/cli/program.d.ts b/vendor/package/dist/cli/program.d.ts new file mode 100644 index 0000000..3288bfa --- /dev/null +++ b/vendor/package/dist/cli/program.d.ts @@ -0,0 +1,5 @@ +import { Command } from 'commander'; +import { type CliContext } from './shared.js'; +export declare const KNOWN_COMMANDS: Set; +export declare function createProgram(ctx: CliContext): Command; +//# sourceMappingURL=program.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/cli/program.d.ts.map b/vendor/package/dist/cli/program.d.ts.map new file mode 100644 index 0000000..dd86328 --- /dev/null +++ b/vendor/package/dist/cli/program.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../../src/cli/program.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgBpC,OAAO,EAAE,KAAK,UAAU,EAAuB,MAAM,aAAa,CAAC;AAEnE,eAAO,MAAM,cAAc,aAyBzB,CAAC;AAEH,wBAAgB,aAAa,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CA+GtD"} \ No newline at end of file diff --git a/vendor/package/dist/cli/program.js b/vendor/package/dist/cli/program.js new file mode 100644 index 0000000..1feeb22 --- /dev/null +++ b/vendor/package/dist/cli/program.js @@ -0,0 +1,113 @@ +import { Command } from 'commander'; +import { registerBookmarksCommand } from '../commands/bookmarks.js'; +import { registerCheckCommand } from '../commands/check.js'; +import { registerFollowCommands } from '../commands/follow.js'; +import { registerHelpCommand } from '../commands/help.js'; +import { registerHomeCommand } from '../commands/home.js'; +import { registerListsCommand } from '../commands/lists.js'; +import { registerNewsCommand } from '../commands/news.js'; +import { registerPostCommands } from '../commands/post.js'; +import { registerQueryIdsCommand } from '../commands/query-ids.js'; +import { registerReadCommands } from '../commands/read.js'; +import { registerSearchCommands } from '../commands/search.js'; +import { registerUnbookmarkCommand } from '../commands/unbookmark.js'; +import { registerUserTweetsCommand } from '../commands/user-tweets.js'; +import { registerUserCommands } from '../commands/users.js'; +import { getCliVersion } from '../lib/version.js'; +import { collectCookieSource } from './shared.js'; +export const KNOWN_COMMANDS = new Set([ + 'tweet', + 'reply', + 'query-ids', + 'read', + 'replies', + 'thread', + 'search', + 'mentions', + 'bookmarks', + 'unbookmark', + 'follow', + 'unfollow', + 'following', + 'followers', + 'likes', + 'lists', + 'list-timeline', + 'home', + 'user-tweets', + 'news', + 'trending', + 'help', + 'whoami', + 'check', +]); +export function createProgram(ctx) { + const program = new Command(); + program.configureHelp({ + showGlobalOptions: true, + styleTitle: (t) => ctx.colors.section(t), + styleUsage: (t) => ctx.colors.description(t), + styleCommandText: (t) => ctx.colors.command(t), + styleCommandDescription: (t) => ctx.colors.muted(t), + styleOptionTerm: (t) => ctx.colors.option(t), + styleOptionText: (t) => ctx.colors.option(t), + styleOptionDescription: (t) => ctx.colors.muted(t), + styleArgumentTerm: (t) => ctx.colors.argument(t), + styleArgumentText: (t) => ctx.colors.argument(t), + styleArgumentDescription: (t) => ctx.colors.muted(t), + styleSubcommandTerm: (t) => ctx.colors.command(t), + styleSubcommandText: (t) => ctx.colors.command(t), + styleSubcommandDescription: (t) => ctx.colors.muted(t), + styleDescriptionText: (t) => ctx.colors.muted(t), + }); + const collect = (value, previous = []) => { + previous.push(value); + return previous; + }; + program.addHelpText('beforeAll', () => `${ctx.colors.banner('bird')} ${ctx.colors.muted(getCliVersion())} ${ctx.colors.subtitle('— fast X CLI for tweeting, replying, and reading')}`); + program.name('bird').description('Post tweets and replies via Twitter/X GraphQL API').version(getCliVersion()); + const formatExample = (command, description) => `${ctx.colors.command(` ${command}`)}\n${ctx.colors.muted(` ${description}`)}`; + program.addHelpText('afterAll', () => `\n${ctx.colors.section('Examples')}\n${[ + formatExample('bird whoami', 'Show the logged-in account via GraphQL cookies'), + formatExample('bird --firefox-profile default-release whoami', 'Use Firefox profile cookies'), + formatExample('bird tweet "hello from bird"', 'Send a tweet'), + formatExample('bird 1234567890123456789 --json', 'Read a tweet (ID or URL shorthand for `read`) and print JSON'), + ].join('\n\n')}\n\n${ctx.colors.section('Shortcuts')}\n${[ + formatExample('bird [--json]', 'Shorthand for `bird read `'), + ].join('\n\n')}\n\n${ctx.colors.section('JSON Output')}\n${ctx.colors.muted(` Add ${ctx.colors.option('--json')} to: read, replies, thread, search, mentions, bookmarks, likes, following, followers, about, lists, list-timeline, user-tweets, query-ids`)}\n${ctx.colors.muted(` Add ${ctx.colors.option('--json-full')} to include raw API response in ${ctx.colors.argument('_raw')} field (tweet commands only)`)}\n${ctx.colors.muted(` (Run ${ctx.colors.command('bird --help')} to see per-command flags.)`)}`); + program.addHelpText('afterAll', () => `\n\n${ctx.colors.section('Config')}\n${ctx.colors.muted(` Reads ${ctx.colors.argument('~/.config/bird/config.json5')} and ${ctx.colors.argument('./.birdrc.json5')} (JSON5)`)}\n${ctx.colors.muted(` Supports: chromeProfile, chromeProfileDir, firefoxProfile, cookieSource, cookieTimeoutMs, timeoutMs, quoteDepth`)}\n\n${ctx.colors.section('Env')}\n${ctx.colors.muted(` ${ctx.colors.option('NO_COLOR')}, ${ctx.colors.option('BIRD_TIMEOUT_MS')}, ${ctx.colors.option('BIRD_COOKIE_TIMEOUT_MS')}, ${ctx.colors.option('BIRD_QUOTE_DEPTH')}`)}`); + program + .option('--auth-token ', 'Twitter auth_token cookie') + .option('--ct0 ', 'Twitter ct0 cookie') + .option('--chrome-profile ', 'Chrome profile name for cookie extraction', ctx.config.chromeProfile) + .option('--chrome-profile-dir ', 'Chrome/Chromium profile directory or cookie DB path for cookie extraction', ctx.config.chromeProfileDir) + .option('--firefox-profile ', 'Firefox profile name for cookie extraction', ctx.config.firefoxProfile) + .option('--cookie-timeout ', 'Cookie extraction timeout in milliseconds (keychain/OS helpers)') + .option('--cookie-source ', 'Cookie source for browser cookie extraction (repeatable)', collectCookieSource) + .option('--media ', 'Attach media file (repeatable, up to 4 images or 1 video)', collect) + .option('--alt ', 'Alt text for the corresponding --media (repeatable)', collect) + .option('--timeout ', 'Request timeout in milliseconds') + .option('--quote-depth ', 'Max quoted tweet depth (default: 1; 0 disables)') + .option('--plain', 'Plain output (stable, no emoji, no color)') + .option('--no-emoji', 'Disable emoji output') + .option('--no-color', 'Disable ANSI colors (or set NO_COLOR)'); + program.hook('preAction', (_thisCommand, actionCommand) => { + ctx.applyOutputFromCommand(actionCommand); + }); + registerHelpCommand(program, ctx); + registerQueryIdsCommand(program, ctx); + registerPostCommands(program, ctx); + registerReadCommands(program, ctx); + registerSearchCommands(program, ctx); + registerBookmarksCommand(program, ctx); + registerUnbookmarkCommand(program, ctx); + registerFollowCommands(program, ctx); + registerListsCommand(program, ctx); + registerHomeCommand(program, ctx); + registerUserCommands(program, ctx); + registerUserTweetsCommand(program, ctx); + registerNewsCommand(program, ctx); + registerCheckCommand(program, ctx); + return program; +} +//# sourceMappingURL=program.js.map \ No newline at end of file diff --git a/vendor/package/dist/cli/program.js.map b/vendor/package/dist/cli/program.js.map new file mode 100644 index 0000000..c2ac975 --- /dev/null +++ b/vendor/package/dist/cli/program.js.map @@ -0,0 +1 @@ +{"version":3,"file":"program.js","sourceRoot":"","sources":["../../src/cli/program.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/cli/shared.d.ts b/vendor/package/dist/cli/shared.d.ts new file mode 100644 index 0000000..bfd7e5f --- /dev/null +++ b/vendor/package/dist/cli/shared.d.ts @@ -0,0 +1,77 @@ +import type { Command } from 'commander'; +import { type CookieSource, resolveCredentials } from '../lib/cookies.js'; +import { labelPrefix, type OutputConfig, statusPrefix } from '../lib/output.js'; +import type { TweetData } from '../lib/twitter-client.js'; +export type BirdConfig = { + chromeProfile?: string; + chromeProfileDir?: string; + firefoxProfile?: string; + cookieSource?: CookieSource | CookieSource[]; + cookieTimeoutMs?: number; + timeoutMs?: number; + quoteDepth?: number; +}; +export type MediaSpec = { + path: string; + alt?: string; + mime: string; + buffer: Buffer; +}; +export type CliContext = { + isTty: boolean; + getOutput: () => OutputConfig; + colors: { + banner: (t: string) => string; + subtitle: (t: string) => string; + section: (t: string) => string; + bullet: (t: string) => string; + command: (t: string) => string; + option: (t: string) => string; + argument: (t: string) => string; + description: (t: string) => string; + muted: (t: string) => string; + accent: (t: string) => string; + }; + p: (kind: Parameters[0]) => string; + l: (kind: Parameters[0]) => string; + config: BirdConfig; + applyOutputFromCommand: (command: Command) => void; + resolveTimeoutFromOptions: (options: { + timeout?: string | number; + }) => number | undefined; + resolveQuoteDepthFromOptions: (options: { + quoteDepth?: string | number; + }) => number | undefined; + resolveCredentialsFromOptions: (opts: CredentialsOptions) => ReturnType; + loadMedia: (opts: { + media: string[]; + alts: string[]; + }) => MediaSpec[]; + printTweets: (tweets: TweetData[], opts?: { + json?: boolean; + emptyMessage?: string; + showSeparator?: boolean; + }) => void; + printTweetsResult: (result: { + tweets?: TweetData[]; + nextCursor?: string; + }, opts: { + json: boolean; + usePagination: boolean; + emptyMessage: string; + }) => void; + extractTweetId: (tweetIdOrUrl: string) => string; +}; +export declare const collectCookieSource: (value: string, previous?: CookieSource[]) => CookieSource[]; +type CredentialsOptions = { + authToken?: string; + ct0?: string; + chromeProfile?: string; + chromeProfileDir?: string; + firefoxProfile?: string; + cookieSource?: CookieSource[]; + cookieTimeout?: string | number; +}; +export declare function createCliContext(normalizedArgs: string[], env?: NodeJS.ProcessEnv): CliContext; +export {}; +//# sourceMappingURL=shared.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/cli/shared.d.ts.map b/vendor/package/dist/cli/shared.d.ts.map new file mode 100644 index 0000000..1d4a929 --- /dev/null +++ b/vendor/package/dist/cli/shared.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/cli/shared.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/cli/shared.js b/vendor/package/dist/cli/shared.js new file mode 100644 index 0000000..8848447 --- /dev/null +++ b/vendor/package/dist/cli/shared.js @@ -0,0 +1,327 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; +import JSON5 from 'json5'; +import kleur from 'kleur'; +import { resolveCredentials } from '../lib/cookies.js'; +import { extractTweetId } from '../lib/extract-tweet-id.js'; +import { hyperlink, labelPrefix, resolveOutputConfigFromArgv, resolveOutputConfigFromCommander, statusPrefix, } from '../lib/output.js'; +const COOKIE_SOURCES = ['safari', 'chrome', 'firefox']; +function parseCookieSource(value) { + const normalized = value.trim().toLowerCase(); + if (normalized === 'safari' || normalized === 'chrome' || normalized === 'firefox') { + return normalized; + } + throw new Error(`Invalid --cookie-source "${value}". Allowed: safari, chrome, firefox.`); +} +export const collectCookieSource = (value, previous = []) => { + previous.push(parseCookieSource(value)); + return previous; +}; +function resolveCookieSourceOrder(input) { + if (typeof input === 'string') { + return [parseCookieSource(input)]; + } + if (Array.isArray(input)) { + const result = []; + for (const entry of input) { + if (typeof entry !== 'string') { + continue; + } + result.push(parseCookieSource(entry)); + } + return result.length > 0 ? result : undefined; + } + return undefined; +} +function resolveTimeoutMs(...values) { + for (const value of values) { + if (value === undefined || value === null || value === '') { + continue; + } + const parsed = typeof value === 'number' ? value : Number(value); + if (Number.isFinite(parsed) && parsed > 0) { + return parsed; + } + } + return undefined; +} +function resolveQuoteDepth(...values) { + for (const value of values) { + if (value === undefined || value === null || value === '') { + continue; + } + const parsed = typeof value === 'number' ? value : Number.parseInt(value, 10); + if (Number.isFinite(parsed) && parsed >= 0) { + return Math.floor(parsed); + } + } + return undefined; +} +function detectMime(path) { + const ext = path.toLowerCase(); + if (ext.endsWith('.jpg') || ext.endsWith('.jpeg')) { + return 'image/jpeg'; + } + if (ext.endsWith('.png')) { + return 'image/png'; + } + if (ext.endsWith('.webp')) { + return 'image/webp'; + } + if (ext.endsWith('.gif')) { + return 'image/gif'; + } + if (ext.endsWith('.mp4') || ext.endsWith('.m4v')) { + return 'video/mp4'; + } + if (ext.endsWith('.mov')) { + return 'video/quicktime'; + } + return null; +} +function readConfigFile(path, warn) { + if (!existsSync(path)) { + return {}; + } + try { + const raw = readFileSync(path, 'utf8'); + const parsed = JSON5.parse(raw); + return parsed ?? {}; + } + catch (error) { + warn(`Failed to parse config at ${path}: ${error instanceof Error ? error.message : String(error)}`); + return {}; + } +} +function loadConfig(warn) { + const globalPath = join(homedir(), '.config', 'bird', 'config.json5'); + const localPath = join(process.cwd(), '.birdrc.json5'); + return { + ...readConfigFile(globalPath, warn), + ...readConfigFile(localPath, warn), + }; +} +export function createCliContext(normalizedArgs, env = process.env) { + const isTty = process.stdout.isTTY; + let output = resolveOutputConfigFromArgv(normalizedArgs, env, isTty); + kleur.enabled = output.color; + const wrap = (styler) => (text) => isTty ? styler(text) : text; + const colors = { + banner: wrap((t) => kleur.bold().blue(t)), + subtitle: wrap((t) => kleur.dim(t)), + section: wrap((t) => kleur.bold().white(t)), + bullet: wrap((t) => kleur.blue(t)), + command: wrap((t) => kleur.bold().cyan(t)), + option: wrap((t) => kleur.cyan(t)), + argument: wrap((t) => kleur.magenta(t)), + description: wrap((t) => kleur.white(t)), + muted: wrap((t) => kleur.gray(t)), + accent: wrap((t) => kleur.green(t)), + }; + const p = (kind) => { + const prefix = statusPrefix(kind, output); + if (output.plain || !output.color) { + return prefix; + } + if (kind === 'ok') { + return kleur.green(prefix); + } + if (kind === 'warn') { + return kleur.yellow(prefix); + } + if (kind === 'err') { + return kleur.red(prefix); + } + if (kind === 'info') { + return kleur.cyan(prefix); + } + return kleur.gray(prefix); + }; + const l = (kind) => { + const prefix = labelPrefix(kind, output); + if (output.plain || !output.color) { + return prefix; + } + if (kind === 'url') { + return kleur.cyan(prefix); + } + if (kind === 'date') { + return kleur.magenta(prefix); + } + if (kind === 'source') { + return kleur.gray(prefix); + } + if (kind === 'engine') { + return kleur.blue(prefix); + } + if (kind === 'credentials') { + return kleur.yellow(prefix); + } + if (kind === 'user') { + return kleur.cyan(prefix); + } + if (kind === 'userId') { + return kleur.magenta(prefix); + } + if (kind === 'email') { + return kleur.green(prefix); + } + return kleur.gray(prefix); + }; + const config = loadConfig((message) => { + console.error(colors.muted(`${p('warn')}${message}`)); + }); + function applyOutputFromCommand(command) { + const opts = command.optsWithGlobals(); + output = resolveOutputConfigFromCommander(opts, env, isTty); + kleur.enabled = output.color; + } + function resolveTimeoutFromOptions(options) { + return resolveTimeoutMs(options.timeout, config.timeoutMs, env.BIRD_TIMEOUT_MS); + } + function resolveCookieTimeoutFromOptions(options) { + return resolveTimeoutMs(options.cookieTimeout, config.cookieTimeoutMs, env.BIRD_COOKIE_TIMEOUT_MS); + } + function resolveQuoteDepthFromOptions(options) { + return resolveQuoteDepth(options.quoteDepth, config.quoteDepth, env.BIRD_QUOTE_DEPTH); + } + function resolveCredentialsFromOptions(opts) { + const cookieSource = opts.cookieSource?.length + ? opts.cookieSource + : (resolveCookieSourceOrder(config.cookieSource) ?? COOKIE_SOURCES); + const chromeProfile = opts.chromeProfileDir || opts.chromeProfile || config.chromeProfileDir || config.chromeProfile; + return resolveCredentials({ + authToken: opts.authToken, + ct0: opts.ct0, + cookieSource, + chromeProfile, + firefoxProfile: opts.firefoxProfile || config.firefoxProfile, + cookieTimeoutMs: resolveCookieTimeoutFromOptions(opts), + }); + } + function loadMedia(opts) { + if (opts.media.length === 0) { + return []; + } + const specs = []; + for (const [index, path] of opts.media.entries()) { + const mime = detectMime(path); + if (!mime) { + throw new Error(`Unsupported media type for ${path}. Supported: jpg, jpeg, png, webp, gif, mp4, mov`); + } + const buffer = readFileSync(path); + specs.push({ path, mime, buffer, alt: opts.alts[index] }); + } + const videoCount = specs.filter((m) => m.mime.startsWith('video/')).length; + if (videoCount > 1) { + throw new Error('Only one video can be attached'); + } + if (videoCount === 1 && specs.length > 1) { + throw new Error('Video cannot be combined with other media'); + } + if (specs.length > 4) { + throw new Error('Maximum 4 media attachments'); + } + return specs; + } + function printTweets(tweets, opts = {}) { + if (opts.json) { + console.log(JSON.stringify(tweets, null, 2)); + return; + } + if (tweets.length === 0) { + console.log(opts.emptyMessage ?? 'No tweets found.'); + return; + } + const useEmoji = output.emoji && !output.plain; + const articleLabel = useEmoji ? '📰' : 'Article:'; + const mediaLabel = (type) => { + if (useEmoji) { + return type === 'video' ? '🎬' : type === 'animated_gif' ? '🔄' : '🖼️'; + } + return type === 'video' ? 'VIDEO:' : type === 'animated_gif' ? 'GIF:' : 'PHOTO:'; + }; + const quotePrefix = useEmoji ? { top: '┌─', mid: '│ ', bot: '└─' } : { top: '> ', mid: '> ', bot: '> ' }; + for (const tweet of tweets) { + console.log(`\n@${tweet.author.username} (${tweet.author.name}):`); + // Display tweet text, with article indicator if present + if (tweet.article) { + // Full body mode: text starts with article title (from extractArticleText) + // Preview mode: text is short tweet intro that doesn't start with title + const hasFullBody = tweet.text.startsWith(tweet.article.title); + if (hasFullBody) { + console.log(`${articleLabel} ${tweet.text}`); + } + else { + console.log(`${articleLabel} ${tweet.article.title}`); + if (tweet.article.previewText) { + console.log(` ${tweet.article.previewText}`); + } + } + } + else { + console.log(tweet.text); + } + // Display media attachments + if (tweet.media && tweet.media.length > 0) { + for (const m of tweet.media) { + console.log(`${mediaLabel(m.type)} ${m.url}`); + } + } + // Display quoted tweet + if (tweet.quotedTweet) { + console.log(`${quotePrefix.top} QT @${tweet.quotedTweet.author.username}:`); + const qtText = tweet.quotedTweet.article + ? `${articleLabel} ${tweet.quotedTweet.article.title}` + : tweet.quotedTweet.text; + // Indent and truncate quoted tweet text + const maxLen = 280; + const truncated = qtText.length > maxLen ? `${qtText.slice(0, maxLen)}...` : qtText; + for (const line of truncated.split('\n').slice(0, 4)) { + console.log(`${quotePrefix.mid}${line}`); + } + // Display quoted tweet media + if (tweet.quotedTweet.media && tweet.quotedTweet.media.length > 0) { + for (const m of tweet.quotedTweet.media) { + console.log(`${quotePrefix.mid}${mediaLabel(m.type)} ${m.url}`); + } + } + console.log(`${quotePrefix.bot} https://x.com/${tweet.quotedTweet.author.username}/status/${tweet.quotedTweet.id}`); + } + if (tweet.createdAt) { + console.log(`${l('date')}${tweet.createdAt}`); + } + const tweetUrl = `https://x.com/${tweet.author.username}/status/${tweet.id}`; + console.log(`${l('url')}${hyperlink(tweetUrl, tweetUrl, output)}`); + if (opts.showSeparator ?? true) { + console.log('─'.repeat(50)); + } + } + } + function printTweetsResult(result, opts) { + const tweets = result.tweets ?? []; + if (opts.json && opts.usePagination) { + console.log(JSON.stringify({ tweets, nextCursor: result.nextCursor ?? null }, null, 2)); + return; + } + printTweets(tweets, { json: opts.json, emptyMessage: opts.emptyMessage }); + } + return { + isTty, + getOutput: () => output, + colors, + p, + l, + config, + applyOutputFromCommand, + resolveTimeoutFromOptions, + resolveQuoteDepthFromOptions, + resolveCredentialsFromOptions, + loadMedia, + printTweets, + printTweetsResult, + extractTweetId, + }; +} +//# sourceMappingURL=shared.js.map \ No newline at end of file diff --git a/vendor/package/dist/cli/shared.js.map b/vendor/package/dist/cli/shared.js.map new file mode 100644 index 0000000..84f92cf --- /dev/null +++ b/vendor/package/dist/cli/shared.js.map @@ -0,0 +1 @@ +{"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/cli/shared.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/commands/bookmarks.d.ts b/vendor/package/dist/commands/bookmarks.d.ts new file mode 100644 index 0000000..9c04f03 --- /dev/null +++ b/vendor/package/dist/commands/bookmarks.d.ts @@ -0,0 +1,4 @@ +import type { Command } from 'commander'; +import type { CliContext } from '../cli/shared.js'; +export declare function registerBookmarksCommand(program: Command, ctx: CliContext): void; +//# sourceMappingURL=bookmarks.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/commands/bookmarks.d.ts.map b/vendor/package/dist/commands/bookmarks.d.ts.map new file mode 100644 index 0000000..e81b302 --- /dev/null +++ b/vendor/package/dist/commands/bookmarks.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"bookmarks.d.ts","sourceRoot":"","sources":["../../src/commands/bookmarks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAMnD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,GAAG,IAAI,CAsOhF"} \ No newline at end of file diff --git a/vendor/package/dist/commands/bookmarks.js b/vendor/package/dist/commands/bookmarks.js new file mode 100644 index 0000000..339e35a --- /dev/null +++ b/vendor/package/dist/commands/bookmarks.js @@ -0,0 +1,189 @@ +import { parsePaginationFlags } from '../cli/pagination.js'; +import { extractBookmarkFolderId } from '../lib/extract-bookmark-folder-id.js'; +import { addThreadMetadata, filterAuthorChain, filterAuthorOnly, filterFullChain } from '../lib/thread-filters.js'; +import { TwitterClient } from '../lib/twitter-client.js'; +export function registerBookmarksCommand(program, ctx) { + program + .command('bookmarks') + .description('Get your bookmarked tweets') + .option('-n, --count ', 'Number of bookmarks to fetch', '20') + .option('--folder-id ', 'Bookmark folder (collection) id') + .option('--all', 'Fetch all bookmarks (paged)') + .option('--max-pages ', 'Stop after N pages when using --all') + .option('--cursor ', 'Resume pagination from a cursor') + .option('--expand-root-only', 'Only expand threads when bookmarked tweet is root') + .option('--author-chain', 'Only include author self-reply chains connected to the bookmark') + .option('--author-only', 'Include all tweets from bookmarked tweet author in thread') + .option('--full-chain-only', 'Save entire reply chain connected to the bookmarked tweet') + .option('--include-ancestor-branches', 'Include sibling branches for ancestors when using --full-chain-only') + .option('--include-parent', 'Include direct parent tweet for non-root bookmarks') + .option('--thread-meta', 'Add metadata fields (isThread, threadPosition, etc.)') + .option('--sort-chronological', 'Sort output globally oldest -> newest') + .option('--json', 'Output as JSON') + .option('--json-full', 'Output as JSON with full raw API response in _raw field') + .action(async (cmdOpts) => { + const opts = program.opts(); + const timeoutMs = ctx.resolveTimeoutFromOptions(opts); + const count = Number.parseInt(cmdOpts.count || '20', 10); + const pagination = parsePaginationFlags(cmdOpts); + if (!pagination.ok) { + console.error(`${ctx.p('err')}${pagination.error}`); + process.exit(1); + } + const maxPages = pagination.maxPages; + const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); + for (const warning of warnings) { + console.error(`${ctx.p('warn')}${warning}`); + } + if (!cookies.authToken || !cookies.ct0) { + console.error(`${ctx.p('err')}Missing required credentials`); + process.exit(1); + } + const usePagination = pagination.usePagination; + if (maxPages !== undefined && !usePagination) { + console.error(`${ctx.p('err')}--max-pages requires --all or --cursor.`); + process.exit(1); + } + if (!usePagination && (!Number.isFinite(count) || count <= 0)) { + console.error(`${ctx.p('err')}Invalid --count. Expected a positive integer.`); + process.exit(1); + } + const client = new TwitterClient({ cookies, timeoutMs }); + const folderId = cmdOpts.folderId ? extractBookmarkFolderId(cmdOpts.folderId) : null; + if (cmdOpts.folderId && !folderId) { + console.error(`${ctx.p('err')}Invalid --folder-id. Expected numeric ID or https://x.com/i/bookmarks/.`); + process.exit(1); + } + const includeRaw = cmdOpts.jsonFull ?? false; + const timelineOptions = { includeRaw }; + const paginationOptions = { includeRaw, maxPages, cursor: pagination.cursor }; + const result = folderId + ? usePagination + ? await client.getAllBookmarkFolderTimeline(folderId, paginationOptions) + : await client.getBookmarkFolderTimeline(folderId, count, timelineOptions) + : usePagination + ? await client.getAllBookmarks(paginationOptions) + : await client.getBookmarks(count, timelineOptions); + if (!result.success) { + console.error(`${ctx.p('err')}Failed to fetch bookmarks: ${result.error}`); + process.exit(1); + } + if (cmdOpts.authorChain && (cmdOpts.authorOnly || cmdOpts.fullChainOnly)) { + console.error(`${ctx.p('warn')}--author-chain already limits to the connected self-reply chain; ` + + 'other chain filters are redundant.'); + } + if (cmdOpts.includeAncestorBranches && !cmdOpts.fullChainOnly) { + console.error(`${ctx.p('warn')}--include-ancestor-branches only applies with --full-chain-only.`); + } + const bookmarks = result.tweets; + if (!bookmarks || bookmarks.length === 0) { + const emptyMessage = folderId ? 'No bookmarks found in folder.' : 'No bookmarks found.'; + const isJson = Boolean(cmdOpts.json || cmdOpts.jsonFull); + ctx.printTweetsResult(result, { json: isJson, usePagination, emptyMessage }); + return; + } + const expandedResults = []; + const threadCache = new Map(); + const includeMeta = Boolean(cmdOpts.threadMeta); + const includeParent = Boolean(cmdOpts.includeParent); + const expandRootOnly = Boolean(cmdOpts.expandRootOnly); + const filterAuthorChainFlag = Boolean(cmdOpts.authorChain); + const filterAuthorOnlyFlag = Boolean(cmdOpts.authorOnly); + const filterFullChainFlag = Boolean(cmdOpts.fullChainOnly); + const includeAncestorBranches = Boolean(cmdOpts.includeAncestorBranches) && filterFullChainFlag; + const useChronologicalSort = Boolean(cmdOpts.sortChronological); + const shouldAttemptExpand = expandRootOnly || filterAuthorChainFlag || filterAuthorOnlyFlag || filterFullChainFlag; + const shouldFetchThread = shouldAttemptExpand || includeMeta; + const fetchThread = async (tweet) => { + const cachedKey = tweet.conversationId ?? tweet.id; + const cached = threadCache.get(cachedKey); + if (cached) { + return cached; + } + const threadResult = await client.getThread(tweet.id, { includeRaw }); + if (!threadResult.success) { + console.error(`${ctx.p('warn')}Failed to expand thread for ${tweet.id}: ${threadResult.error ?? 'Unknown error'}`); + return null; + } + if (!threadResult.tweets) { + console.error(`${ctx.p('warn')}No thread tweets returned for ${tweet.id}.`); + return null; + } + const rootKey = threadResult.tweets[0]?.conversationId ?? cachedKey; + threadCache.set(rootKey, threadResult.tweets); + return threadResult.tweets; + }; + const delayBetweenExpansionsMs = 1000; + for (let index = 0; index < bookmarks.length; index += 1) { + const bookmark = bookmarks[index]; + const isRoot = !bookmark.inReplyToStatusId; + let threadTweets = null; + if (shouldFetchThread) { + if (!expandRootOnly || isRoot || includeMeta) { + if (index > 0) { + await new Promise((resolve) => setTimeout(resolve, delayBetweenExpansionsMs)); + } + threadTweets = await fetchThread(bookmark); + } + } + let outputTweets = [bookmark]; + if (shouldAttemptExpand) { + if (expandRootOnly && !isRoot) { + outputTweets = [bookmark]; + } + else if (threadTweets) { + if (filterAuthorChainFlag) { + outputTweets = filterAuthorChain(threadTweets, bookmark); + } + else { + outputTweets = filterFullChainFlag + ? filterFullChain(threadTweets, bookmark, { includeAncestorBranches }) + : threadTweets; + if (filterAuthorOnlyFlag) { + outputTweets = filterAuthorOnly(outputTweets, bookmark); + } + } + } + } + if (includeParent && bookmark.inReplyToStatusId) { + const alreadyIncluded = outputTweets.some((tweet) => tweet.id === bookmark.inReplyToStatusId); + if (!alreadyIncluded) { + const parentFromThread = threadTweets?.find((tweet) => tweet.id === bookmark.inReplyToStatusId); + if (parentFromThread) { + expandedResults.push(parentFromThread); + } + else { + const parentResult = await client.getTweet(bookmark.inReplyToStatusId, { includeRaw }); + if (parentResult.success && parentResult.tweet) { + expandedResults.push(parentResult.tweet); + } + } + } + } + expandedResults.push(...outputTweets); + } + let finalResults = expandedResults; + if (includeMeta) { + finalResults = expandedResults.map((tweet) => { + const cacheKey = tweet.conversationId ?? tweet.id; + let conversationTweets = threadCache.get(cacheKey); + if (!conversationTweets) { + conversationTweets = [tweet]; + } + return addThreadMetadata(tweet, conversationTweets); + }); + } + const uniqueTweets = Array.from(new Map(finalResults.map((tweet) => [tweet.id, tweet])).values()); + if (useChronologicalSort) { + uniqueTweets.sort((a, b) => { + const aTime = a.createdAt ? Date.parse(a.createdAt) : 0; + const bTime = b.createdAt ? Date.parse(b.createdAt) : 0; + return aTime - bTime; + }); + } + const emptyMessage = folderId ? 'No bookmarks found in folder.' : 'No bookmarks found.'; + const isJson = Boolean(cmdOpts.json || cmdOpts.jsonFull); + ctx.printTweetsResult({ tweets: uniqueTweets, nextCursor: result.nextCursor }, { json: isJson, usePagination, emptyMessage }); + }); +} +//# sourceMappingURL=bookmarks.js.map \ No newline at end of file diff --git a/vendor/package/dist/commands/bookmarks.js.map b/vendor/package/dist/commands/bookmarks.js.map new file mode 100644 index 0000000..80a0666 --- /dev/null +++ b/vendor/package/dist/commands/bookmarks.js.map @@ -0,0 +1 @@ +{"version":3,"file":"bookmarks.js","sourceRoot":"","sources":["../../src/commands/bookmarks.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/commands/check.d.ts b/vendor/package/dist/commands/check.d.ts new file mode 100644 index 0000000..d4ca6df --- /dev/null +++ b/vendor/package/dist/commands/check.d.ts @@ -0,0 +1,4 @@ +import type { Command } from 'commander'; +import type { CliContext } from '../cli/shared.js'; +export declare function registerCheckCommand(program: Command, ctx: CliContext): void; +//# sourceMappingURL=check.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/commands/check.d.ts.map b/vendor/package/dist/commands/check.d.ts.map new file mode 100644 index 0000000..20ee921 --- /dev/null +++ b/vendor/package/dist/commands/check.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/commands/check.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,GAAG,IAAI,CA4C5E"} \ No newline at end of file diff --git a/vendor/package/dist/commands/check.js b/vendor/package/dist/commands/check.js new file mode 100644 index 0000000..a6e2108 --- /dev/null +++ b/vendor/package/dist/commands/check.js @@ -0,0 +1,43 @@ +export function registerCheckCommand(program, ctx) { + program + .command('check') + .description('Check credential availability') + .action(async () => { + const opts = program.opts(); + const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); + console.log(`${ctx.p('info')}Credential check`); + console.log('─'.repeat(40)); + if (cookies.authToken) { + console.log(`${ctx.p('ok')}auth_token: ${cookies.authToken.slice(0, 10)}...`); + } + else { + console.log(`${ctx.p('err')}auth_token: not found`); + } + if (cookies.ct0) { + console.log(`${ctx.p('ok')}ct0: ${cookies.ct0.slice(0, 10)}...`); + } + else { + console.log(`${ctx.p('err')}ct0: not found`); + } + if (cookies.source) { + console.log(`${ctx.l('source')}${cookies.source}`); + } + if (warnings.length > 0) { + console.log(`\n${ctx.p('warn')}Warnings:`); + for (const warning of warnings) { + console.log(` - ${warning}`); + } + } + if (cookies.authToken && cookies.ct0) { + console.log(`\n${ctx.p('ok')}Ready to tweet!`); + } + else { + console.log(`\n${ctx.p('err')}Missing credentials. Options:`); + console.log(' 1. Login to x.com in Safari/Chrome/Firefox'); + console.log(' 2. Set AUTH_TOKEN and CT0 environment variables'); + console.log(' 3. Use --auth-token and --ct0 flags'); + process.exit(1); + } + }); +} +//# sourceMappingURL=check.js.map \ No newline at end of file diff --git a/vendor/package/dist/commands/check.js.map b/vendor/package/dist/commands/check.js.map new file mode 100644 index 0000000..6332a11 --- /dev/null +++ b/vendor/package/dist/commands/check.js.map @@ -0,0 +1 @@ +{"version":3,"file":"check.js","sourceRoot":"","sources":["../../src/commands/check.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,oBAAoB,CAAC,OAAgB,EAAE,GAAe;IACpE,OAAO;SACJ,OAAO,CAAC,OAAO,CAAC;SAChB,WAAW,CAAC,+BAA+B,CAAC;SAC5C,MAAM,CAAC,KAAK,IAAI,EAAE;QACjB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC5B,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,MAAM,GAAG,CAAC,6BAA6B,CAAC,IAAI,CAAC,CAAC;QAE5E,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC;QAChD,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;QAE5B,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;YACtB,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC;QAChF,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC;QACtD,CAAC;QAED,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;YAChB,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC;QACnE,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC;QAC/C,CAAC;QAED,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YACnB,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QACrD,CAAC;QAED,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,OAAO,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;YAC3C,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;gBAC/B,OAAO,CAAC,GAAG,CAAC,QAAQ,OAAO,EAAE,CAAC,CAAC;YACjC,CAAC;QACH,CAAC;QAED,IAAI,OAAO,CAAC,SAAS,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;YACrC,OAAO,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;QACjD,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,+BAA+B,CAAC,CAAC;YAC9D,OAAO,CAAC,GAAG,CAAC,+CAA+C,CAAC,CAAC;YAC7D,OAAO,CAAC,GAAG,CAAC,oDAAoD,CAAC,CAAC;YAClE,OAAO,CAAC,GAAG,CAAC,wCAAwC,CAAC,CAAC;YACtD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC"} \ No newline at end of file diff --git a/vendor/package/dist/commands/follow.d.ts b/vendor/package/dist/commands/follow.d.ts new file mode 100644 index 0000000..f80d3e1 --- /dev/null +++ b/vendor/package/dist/commands/follow.d.ts @@ -0,0 +1,4 @@ +import type { Command } from 'commander'; +import type { CliContext } from '../cli/shared.js'; +export declare function registerFollowCommands(program: Command, ctx: CliContext): void; +//# sourceMappingURL=follow.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/commands/follow.d.ts.map b/vendor/package/dist/commands/follow.d.ts.map new file mode 100644 index 0000000..3d1e942 --- /dev/null +++ b/vendor/package/dist/commands/follow.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"follow.d.ts","sourceRoot":"","sources":["../../src/commands/follow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAmCnD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,GAAG,IAAI,CA8E9E"} \ No newline at end of file diff --git a/vendor/package/dist/commands/follow.js b/vendor/package/dist/commands/follow.js new file mode 100644 index 0000000..908ddee --- /dev/null +++ b/vendor/package/dist/commands/follow.js @@ -0,0 +1,91 @@ +import { normalizeHandle } from '../lib/normalize-handle.js'; +import { TwitterClient } from '../lib/twitter-client.js'; +const ONLY_DIGITS_REGEX = /^\d+$/; +async function resolveUserId(client, usernameOrId, ctx) { + const raw = usernameOrId.trim(); + const isNumeric = ONLY_DIGITS_REGEX.test(raw); + // Otherwise, treat as username and look up + const handle = normalizeHandle(raw); + if (handle) { + const lookup = await client.getUserIdByUsername(handle); + if (lookup.success && lookup.userId) { + return { userId: lookup.userId, username: lookup.username }; + } + if (!isNumeric) { + console.error(`${ctx.p('err')}Failed to find user @${handle}: ${lookup.error ?? 'Unknown error'}`); + return null; + } + } + if (isNumeric) { + return { userId: raw }; + } + console.error(`${ctx.p('err')}Invalid username: ${usernameOrId}`); + return null; +} +export function registerFollowCommands(program, ctx) { + program + .command('follow') + .description('Follow a user') + .argument('', 'Username (with or without @) or user ID to follow') + .action(async (usernameOrId) => { + const opts = program.opts(); + const timeoutMs = ctx.resolveTimeoutFromOptions(opts); + const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); + for (const warning of warnings) { + console.error(`${ctx.p('warn')}${warning}`); + } + if (!cookies.authToken || !cookies.ct0) { + console.error(`${ctx.p('err')}Missing required credentials`); + process.exit(1); + } + const client = new TwitterClient({ cookies, timeoutMs }); + const resolved = await resolveUserId(client, usernameOrId, ctx); + if (!resolved) { + process.exit(1); + } + const { userId, username } = resolved; + const displayName = username ? `@${username}` : userId; + const result = await client.follow(userId); + if (result.success) { + const finalName = result.username ? `@${result.username}` : displayName; + console.log(`${ctx.p('ok')}Now following ${finalName}`); + } + else { + console.error(`${ctx.p('err')}Failed to follow ${displayName}: ${result.error}`); + process.exit(1); + } + }); + program + .command('unfollow') + .description('Unfollow a user') + .argument('', 'Username (with or without @) or user ID to unfollow') + .action(async (usernameOrId) => { + const opts = program.opts(); + const timeoutMs = ctx.resolveTimeoutFromOptions(opts); + const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); + for (const warning of warnings) { + console.error(`${ctx.p('warn')}${warning}`); + } + if (!cookies.authToken || !cookies.ct0) { + console.error(`${ctx.p('err')}Missing required credentials`); + process.exit(1); + } + const client = new TwitterClient({ cookies, timeoutMs }); + const resolved = await resolveUserId(client, usernameOrId, ctx); + if (!resolved) { + process.exit(1); + } + const { userId, username } = resolved; + const displayName = username ? `@${username}` : userId; + const result = await client.unfollow(userId); + if (result.success) { + const finalName = result.username ? `@${result.username}` : displayName; + console.log(`${ctx.p('ok')}Unfollowed ${finalName}`); + } + else { + console.error(`${ctx.p('err')}Failed to unfollow ${displayName}: ${result.error}`); + process.exit(1); + } + }); +} +//# sourceMappingURL=follow.js.map \ No newline at end of file diff --git a/vendor/package/dist/commands/follow.js.map b/vendor/package/dist/commands/follow.js.map new file mode 100644 index 0000000..b21dcc4 --- /dev/null +++ b/vendor/package/dist/commands/follow.js.map @@ -0,0 +1 @@ +{"version":3,"file":"follow.js","sourceRoot":"","sources":["../../src/commands/follow.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/commands/help.d.ts b/vendor/package/dist/commands/help.d.ts new file mode 100644 index 0000000..335fbe0 --- /dev/null +++ b/vendor/package/dist/commands/help.d.ts @@ -0,0 +1,4 @@ +import type { Command } from 'commander'; +import type { CliContext } from '../cli/shared.js'; +export declare function registerHelpCommand(program: Command, ctx: CliContext): void; +//# sourceMappingURL=help.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/commands/help.d.ts.map b/vendor/package/dist/commands/help.d.ts.map new file mode 100644 index 0000000..d89e7eb --- /dev/null +++ b/vendor/package/dist/commands/help.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../src/commands/help.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,GAAG,IAAI,CAmB3E"} \ No newline at end of file diff --git a/vendor/package/dist/commands/help.js b/vendor/package/dist/commands/help.js new file mode 100644 index 0000000..b607c7e --- /dev/null +++ b/vendor/package/dist/commands/help.js @@ -0,0 +1,19 @@ +export function registerHelpCommand(program, ctx) { + program + .command('help [command]') + .description('Show help for a command') + .action((commandName) => { + if (!commandName) { + program.outputHelp(); + return; + } + const cmd = program.commands.find((c) => c.name() === commandName); + if (!cmd) { + console.error(`${ctx.p('err')}Unknown command: ${commandName}`); + process.exitCode = 2; + return; + } + cmd.outputHelp(); + }); +} +//# sourceMappingURL=help.js.map \ No newline at end of file diff --git a/vendor/package/dist/commands/help.js.map b/vendor/package/dist/commands/help.js.map new file mode 100644 index 0000000..e4c9333 --- /dev/null +++ b/vendor/package/dist/commands/help.js.map @@ -0,0 +1 @@ +{"version":3,"file":"help.js","sourceRoot":"","sources":["../../src/commands/help.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,mBAAmB,CAAC,OAAgB,EAAE,GAAe;IACnE,OAAO;SACJ,OAAO,CAAC,gBAAgB,CAAC;SACzB,WAAW,CAAC,yBAAyB,CAAC;SACtC,MAAM,CAAC,CAAC,WAAoB,EAAE,EAAE;QAC/B,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,OAAO,CAAC,UAAU,EAAE,CAAC;YACrB,OAAO;QACT,CAAC;QAED,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,WAAW,CAAC,CAAC;QACnE,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,oBAAoB,WAAW,EAAE,CAAC,CAAC;YAChE,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;YACrB,OAAO;QACT,CAAC;QAED,GAAG,CAAC,UAAU,EAAE,CAAC;IACnB,CAAC,CAAC,CAAC;AACP,CAAC"} \ No newline at end of file diff --git a/vendor/package/dist/commands/home.d.ts b/vendor/package/dist/commands/home.d.ts new file mode 100644 index 0000000..1e21040 --- /dev/null +++ b/vendor/package/dist/commands/home.d.ts @@ -0,0 +1,4 @@ +import type { Command } from 'commander'; +import type { CliContext } from '../cli/shared.js'; +export declare function registerHomeCommand(program: Command, ctx: CliContext): void; +//# sourceMappingURL=home.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/commands/home.d.ts.map b/vendor/package/dist/commands/home.d.ts.map new file mode 100644 index 0000000..5271787 --- /dev/null +++ b/vendor/package/dist/commands/home.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"home.d.ts","sourceRoot":"","sources":["../../src/commands/home.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,GAAG,IAAI,CA8C3E"} \ No newline at end of file diff --git a/vendor/package/dist/commands/home.js b/vendor/package/dist/commands/home.js new file mode 100644 index 0000000..ab1da93 --- /dev/null +++ b/vendor/package/dist/commands/home.js @@ -0,0 +1,43 @@ +import { TwitterClient } from '../lib/twitter-client.js'; +export function registerHomeCommand(program, ctx) { + program + .command('home') + .description('Get your home timeline ("For You" feed)') + .option('-n, --count ', 'Number of tweets to fetch', '20') + .option('--following', 'Get "Following" feed (chronological) instead of "For You"') + .option('--json', 'Output as JSON') + .option('--json-full', 'Output as JSON with full raw API response in _raw field') + .action(async (cmdOpts) => { + const opts = program.opts(); + const timeoutMs = ctx.resolveTimeoutFromOptions(opts); + const count = Number.parseInt(cmdOpts.count || '20', 10); + const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); + for (const warning of warnings) { + console.error(`${ctx.p('warn')}${warning}`); + } + if (!cookies.authToken || !cookies.ct0) { + console.error(`${ctx.p('err')}Missing required credentials`); + process.exit(1); + } + if (!Number.isFinite(count) || count <= 0) { + console.error(`${ctx.p('err')}Invalid --count. Expected a positive integer.`); + process.exit(1); + } + const client = new TwitterClient({ cookies, timeoutMs }); + const includeRaw = cmdOpts.jsonFull ?? false; + const result = cmdOpts.following + ? await client.getHomeLatestTimeline(count, { includeRaw }) + : await client.getHomeTimeline(count, { includeRaw }); + if (result.success) { + const feedType = cmdOpts.following ? 'Following' : 'For You'; + const emptyMessage = `No tweets found in ${feedType} timeline.`; + const isJson = Boolean(cmdOpts.json || cmdOpts.jsonFull); + ctx.printTweets(result.tweets, { json: isJson, emptyMessage }); + } + else { + console.error(`${ctx.p('err')}Failed to fetch home timeline: ${result.error}`); + process.exit(1); + } + }); +} +//# sourceMappingURL=home.js.map \ No newline at end of file diff --git a/vendor/package/dist/commands/home.js.map b/vendor/package/dist/commands/home.js.map new file mode 100644 index 0000000..d7de109 --- /dev/null +++ b/vendor/package/dist/commands/home.js.map @@ -0,0 +1 @@ +{"version":3,"file":"home.js","sourceRoot":"","sources":["../../src/commands/home.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAEzD,MAAM,UAAU,mBAAmB,CAAC,OAAgB,EAAE,GAAe;IACnE,OAAO;SACJ,OAAO,CAAC,MAAM,CAAC;SACf,WAAW,CAAC,yCAAyC,CAAC;SACtD,MAAM,CAAC,sBAAsB,EAAE,2BAA2B,EAAE,IAAI,CAAC;SACjE,MAAM,CAAC,aAAa,EAAE,2DAA2D,CAAC;SAClF,MAAM,CAAC,QAAQ,EAAE,gBAAgB,CAAC;SAClC,MAAM,CAAC,aAAa,EAAE,yDAAyD,CAAC;SAChF,MAAM,CAAC,KAAK,EAAE,OAAoF,EAAE,EAAE;QACrG,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC5B,MAAM,SAAS,GAAG,GAAG,CAAC,yBAAyB,CAAC,IAAI,CAAC,CAAC;QACtD,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC;QAEzD,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,MAAM,GAAG,CAAC,6BAA6B,CAAC,IAAI,CAAC,CAAC;QAE5E,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,OAAO,EAAE,CAAC,CAAC;QAC9C,CAAC;QAED,IAAI,CAAC,OAAO,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;YACvC,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC;YAC7D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;YAC1C,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,+CAA+C,CAAC,CAAC;YAC9E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC;QACzD,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ,IAAI,KAAK,CAAC;QAE7C,MAAM,MAAM,GAAG,OAAO,CAAC,SAAS;YAC9B,CAAC,CAAC,MAAM,MAAM,CAAC,qBAAqB,CAAC,KAAK,EAAE,EAAE,UAAU,EAAE,CAAC;YAC3D,CAAC,CAAC,MAAM,MAAM,CAAC,eAAe,CAAC,KAAK,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC;QAExD,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,MAAM,QAAQ,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC;YAC7D,MAAM,YAAY,GAAG,sBAAsB,QAAQ,YAAY,CAAC;YAChE,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC;YACzD,GAAG,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;QACjE,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,kCAAkC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;YAC/E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC"} \ No newline at end of file diff --git a/vendor/package/dist/commands/lists.d.ts b/vendor/package/dist/commands/lists.d.ts new file mode 100644 index 0000000..aa365d6 --- /dev/null +++ b/vendor/package/dist/commands/lists.d.ts @@ -0,0 +1,4 @@ +import type { Command } from 'commander'; +import type { CliContext } from '../cli/shared.js'; +export declare function registerListsCommand(program: Command, ctx: CliContext): void; +//# sourceMappingURL=lists.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/commands/lists.d.ts.map b/vendor/package/dist/commands/lists.d.ts.map new file mode 100644 index 0000000..5c78aaa --- /dev/null +++ b/vendor/package/dist/commands/lists.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"lists.d.ts","sourceRoot":"","sources":["../../src/commands/lists.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AA4BnD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,GAAG,IAAI,CAyH5E"} \ No newline at end of file diff --git a/vendor/package/dist/commands/lists.js b/vendor/package/dist/commands/lists.js new file mode 100644 index 0000000..6b7aeab --- /dev/null +++ b/vendor/package/dist/commands/lists.js @@ -0,0 +1,125 @@ +// ABOUTME: CLI command for fetching Twitter Lists. +// ABOUTME: Supports listing owned lists, memberships, and list timelines. +import { parsePaginationFlags } from '../cli/pagination.js'; +import { extractListId } from '../lib/extract-list-id.js'; +import { hyperlink } from '../lib/output.js'; +import { TwitterClient } from '../lib/twitter-client.js'; +function printLists(lists, ctx) { + if (lists.length === 0) { + console.log('No lists found.'); + return; + } + for (const list of lists) { + const visibility = list.isPrivate ? '[private]' : '[public]'; + console.log(`${list.name} ${ctx.colors.muted(visibility)}`); + if (list.description) { + console.log(` ${list.description.slice(0, 100)}${list.description.length > 100 ? '...' : ''}`); + } + console.log(` ${ctx.p('info')}${list.memberCount?.toLocaleString() ?? 0} members`); + if (list.owner) { + console.log(` ${ctx.colors.muted(`Owner: @${list.owner.username}`)}`); + } + const listUrl = `https://x.com/i/lists/${list.id}`; + console.log(` ${ctx.colors.accent(hyperlink(listUrl, listUrl, ctx.getOutput()))}`); + console.log('──────────────────────────────────────────────────'); + } +} +export function registerListsCommand(program, ctx) { + program + .command('lists') + .description('Get your Twitter lists') + .option('--member-of', 'Show lists you are a member of (instead of owned lists)') + .option('-n, --count ', 'Number of lists to fetch', '100') + .option('--json', 'Output as JSON') + .action(async (cmdOpts) => { + const opts = program.opts(); + const timeoutMs = ctx.resolveTimeoutFromOptions(opts); + const count = Number.parseInt(cmdOpts.count || '100', 10); + const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); + for (const warning of warnings) { + console.error(`${ctx.p('warn')}${warning}`); + } + if (!cookies.authToken || !cookies.ct0) { + console.error(`${ctx.p('err')}Missing required credentials`); + process.exit(1); + } + const client = new TwitterClient({ cookies, timeoutMs }); + const result = cmdOpts.memberOf ? await client.getListMemberships(count) : await client.getOwnedLists(count); + if (result.success && result.lists) { + if (cmdOpts.json) { + console.log(JSON.stringify(result.lists, null, 2)); + } + else { + const emptyMessage = cmdOpts.memberOf ? 'You are not a member of any lists.' : 'You do not own any lists.'; + if (result.lists.length === 0) { + console.log(emptyMessage); + } + else { + printLists(result.lists, ctx); + } + } + } + else { + console.error(`${ctx.p('err')}Failed to fetch lists: ${result.error}`); + process.exit(1); + } + }); + program + .command('list-timeline ') + .description('Get tweets from a list timeline') + .option('-n, --count ', 'Number of tweets to fetch', '20') + .option('--all', 'Fetch all tweets from list (paged). WARNING: your account might get banned using this flag') + .option('--max-pages ', 'Fetch N pages (implies --all)') + .option('--cursor ', 'Resume pagination from a cursor') + .option('--json', 'Output as JSON') + .option('--json-full', 'Output as JSON with full raw API response in _raw field') + .action(async (listIdOrUrl, cmdOpts) => { + const opts = program.opts(); + const timeoutMs = ctx.resolveTimeoutFromOptions(opts); + const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts); + const count = Number.parseInt(cmdOpts.count || '20', 10); + const pagination = parsePaginationFlags(cmdOpts, { maxPagesImpliesPagination: true }); + if (!pagination.ok) { + console.error(`${ctx.p('err')}${pagination.error}`); + process.exit(1); + } + const listId = extractListId(listIdOrUrl); + if (!listId) { + console.error(`${ctx.p('err')}Invalid list ID or URL. Expected numeric ID or https://x.com/i/lists/.`); + process.exit(2); + } + const usePagination = pagination.usePagination; + if (!usePagination && (!Number.isFinite(count) || count <= 0)) { + console.error(`${ctx.p('err')}Invalid --count. Expected a positive integer.`); + process.exit(1); + } + const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); + for (const warning of warnings) { + console.error(`${ctx.p('warn')}${warning}`); + } + if (!cookies.authToken || !cookies.ct0) { + console.error(`${ctx.p('err')}Missing required credentials`); + process.exit(1); + } + const client = new TwitterClient({ cookies, timeoutMs, quoteDepth }); + const includeRaw = cmdOpts.jsonFull ?? false; + const timelineOptions = { includeRaw }; + const paginationOptions = { includeRaw, maxPages: pagination.maxPages, cursor: pagination.cursor }; + const result = usePagination + ? await client.getAllListTimeline(listId, paginationOptions) + : await client.getListTimeline(listId, count, timelineOptions); + if (result.success) { + const isJson = Boolean(cmdOpts.json || cmdOpts.jsonFull); + ctx.printTweetsResult(result, { + json: isJson, + usePagination, + emptyMessage: 'No tweets found in this list.', + }); + } + else { + console.error(`${ctx.p('err')}Failed to fetch list timeline: ${result.error}`); + process.exit(1); + } + }); +} +//# sourceMappingURL=lists.js.map \ No newline at end of file diff --git a/vendor/package/dist/commands/lists.js.map b/vendor/package/dist/commands/lists.js.map new file mode 100644 index 0000000..9919aed --- /dev/null +++ b/vendor/package/dist/commands/lists.js.map @@ -0,0 +1 @@ +{"version":3,"file":"lists.js","sourceRoot":"","sources":["../../src/commands/lists.ts"],"names":[],"mappings":"AAAA,mDAAmD;AACnD,0EAA0E;AAG1E,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAEzD,SAAS,UAAU,CAAC,KAAoB,EAAE,GAAe;IACvD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;QAC/B,OAAO;IACT,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,UAAU,CAAC;QAC7D,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;QAC5D,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACrB,OAAO,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAClG,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,WAAW,EAAE,cAAc,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;QACpF,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC;QACzE,CAAC;QACD,MAAM,OAAO,GAAG,yBAAyB,IAAI,CAAC,EAAE,EAAE,CAAC;QACnD,OAAO,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;QACpF,OAAO,CAAC,GAAG,CAAC,oDAAoD,CAAC,CAAC;IACpE,CAAC;AACH,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,OAAgB,EAAE,GAAe;IACpE,OAAO;SACJ,OAAO,CAAC,OAAO,CAAC;SAChB,WAAW,CAAC,wBAAwB,CAAC;SACrC,MAAM,CAAC,aAAa,EAAE,yDAAyD,CAAC;SAChF,MAAM,CAAC,sBAAsB,EAAE,0BAA0B,EAAE,KAAK,CAAC;SACjE,MAAM,CAAC,QAAQ,EAAE,gBAAgB,CAAC;SAClC,MAAM,CAAC,KAAK,EAAE,OAA+D,EAAE,EAAE;QAChF,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC5B,MAAM,SAAS,GAAG,GAAG,CAAC,yBAAyB,CAAC,IAAI,CAAC,CAAC;QACtD,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,IAAI,KAAK,EAAE,EAAE,CAAC,CAAC;QAE1D,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,MAAM,GAAG,CAAC,6BAA6B,CAAC,IAAI,CAAC,CAAC;QAE5E,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,OAAO,EAAE,CAAC,CAAC;QAC9C,CAAC;QAED,IAAI,CAAC,OAAO,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;YACvC,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC;YAC7D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC;QAEzD,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,MAAM,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAE7G,IAAI,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;YACnC,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;gBACjB,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;YACrD,CAAC;iBAAM,CAAC;gBACN,MAAM,YAAY,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,oCAAoC,CAAC,CAAC,CAAC,2BAA2B,CAAC;gBAC3G,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBAC9B,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;gBAC5B,CAAC;qBAAM,CAAC;oBACN,UAAU,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;gBAChC,CAAC;YACH,CAAC;QACH,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,0BAA0B,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;YACvE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,CAAC,CAAC;IAEL,OAAO;SACJ,OAAO,CAAC,gCAAgC,CAAC;SACzC,WAAW,CAAC,iCAAiC,CAAC;SAC9C,MAAM,CAAC,sBAAsB,EAAE,2BAA2B,EAAE,IAAI,CAAC;SACjE,MAAM,CAAC,OAAO,EAAE,4FAA4F,CAAC;SAC7G,MAAM,CAAC,sBAAsB,EAAE,+BAA+B,CAAC;SAC/D,MAAM,CAAC,mBAAmB,EAAE,iCAAiC,CAAC;SAC9D,MAAM,CAAC,QAAQ,EAAE,gBAAgB,CAAC;SAClC,MAAM,CAAC,aAAa,EAAE,yDAAyD,CAAC;SAChF,MAAM,CACL,KAAK,EACH,WAAmB,EACnB,OAOC,EACD,EAAE;QACF,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC5B,MAAM,SAAS,GAAG,GAAG,CAAC,yBAAyB,CAAC,IAAI,CAAC,CAAC;QACtD,MAAM,UAAU,GAAG,GAAG,CAAC,4BAA4B,CAAC,IAAI,CAAC,CAAC;QAC1D,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC;QAEzD,MAAM,UAAU,GAAG,oBAAoB,CAAC,OAAO,EAAE,EAAE,yBAAyB,EAAE,IAAI,EAAE,CAAC,CAAC;QACtF,IAAI,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC;YACnB,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,UAAU,CAAC,KAAK,EAAE,CAAC,CAAC;YACpD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,MAAM,MAAM,GAAG,aAAa,CAAC,WAAW,CAAC,CAAC;QAC1C,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,4EAA4E,CAAC,CAAC;YAC3G,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,MAAM,aAAa,GAAG,UAAU,CAAC,aAAa,CAAC;QAC/C,IAAI,CAAC,aAAa,IAAI,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,EAAE,CAAC;YAC9D,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,+CAA+C,CAAC,CAAC;YAC9E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,MAAM,GAAG,CAAC,6BAA6B,CAAC,IAAI,CAAC,CAAC;QAE5E,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,OAAO,EAAE,CAAC,CAAC;QAC9C,CAAC;QAED,IAAI,CAAC,OAAO,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;YACvC,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC;YAC7D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAC;QACrE,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ,IAAI,KAAK,CAAC;QAC7C,MAAM,eAAe,GAAG,EAAE,UAAU,EAAE,CAAC;QACvC,MAAM,iBAAiB,GAAG,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC;QAEnG,MAAM,MAAM,GAAG,aAAa;YAC1B,CAAC,CAAC,MAAM,MAAM,CAAC,kBAAkB,CAAC,MAAM,EAAE,iBAAiB,CAAC;YAC5D,CAAC,CAAC,MAAM,MAAM,CAAC,eAAe,CAAC,MAAM,EAAE,KAAK,EAAE,eAAe,CAAC,CAAC;QAEjE,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC;YACzD,GAAG,CAAC,iBAAiB,CAAC,MAAM,EAAE;gBAC5B,IAAI,EAAE,MAAM;gBACZ,aAAa;gBACb,YAAY,EAAE,+BAA+B;aAC9C,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,kCAAkC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;YAC/E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,CACF,CAAC;AACN,CAAC"} \ No newline at end of file diff --git a/vendor/package/dist/commands/news.d.ts b/vendor/package/dist/commands/news.d.ts new file mode 100644 index 0000000..5ab9191 --- /dev/null +++ b/vendor/package/dist/commands/news.d.ts @@ -0,0 +1,4 @@ +import type { Command } from 'commander'; +import type { CliContext } from '../cli/shared.js'; +export declare function registerNewsCommand(program: Command, ctx: CliContext): void; +//# sourceMappingURL=news.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/commands/news.d.ts.map b/vendor/package/dist/commands/news.d.ts.map new file mode 100644 index 0000000..9022ebe --- /dev/null +++ b/vendor/package/dist/commands/news.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"news.d.ts","sourceRoot":"","sources":["../../src/commands/news.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAmEnD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,GAAG,IAAI,CAuG3E"} \ No newline at end of file diff --git a/vendor/package/dist/commands/news.js b/vendor/package/dist/commands/news.js new file mode 100644 index 0000000..949a8b1 --- /dev/null +++ b/vendor/package/dist/commands/news.js @@ -0,0 +1,131 @@ +import { TwitterClient } from '../lib/twitter-client.js'; +function formatPostCount(count) { + if (count >= 1_000_000) { + return `${(count / 1_000_000).toFixed(1)}M`; + } + if (count >= 1_000) { + return `${(count / 1_000).toFixed(1)}K`; + } + return String(count); +} +function printNewsItems(items, ctx, opts = {}) { + if (opts.json) { + console.log(JSON.stringify(items, null, 2)); + return; + } + if (items.length === 0) { + console.log(opts.emptyMessage ?? 'No news items found.'); + return; + } + for (const item of items) { + const categoryLabel = item.category ? `[${item.category}]` : ''; + console.log(`\n${ctx.colors.accent(categoryLabel)} ${ctx.colors.command(item.headline)}`); + if (item.description) { + console.log(` ${ctx.colors.muted(item.description)}`); + } + const meta = []; + if (item.timeAgo) { + meta.push(item.timeAgo); + } + if (item.postCount) { + meta.push(`${formatPostCount(item.postCount)} posts`); + } + if (meta.length > 0) { + console.log(` ${ctx.colors.muted(meta.join(' | '))}`); + } + if (item.url) { + console.log(` ${ctx.l('url')}${item.url}`); + } + // Print related tweets if available + if (item.tweets && item.tweets.length > 0) { + console.log(` ${ctx.colors.section('Related tweets:')}`); + const tweetLimit = opts.tweetLimit ?? item.tweets.length; + for (const tweet of item.tweets.slice(0, tweetLimit)) { + console.log(` @${tweet.author.username}: ${tweet.text.slice(0, 100)}${tweet.text.length > 100 ? '...' : ''}`); + } + } + console.log(ctx.colors.muted('─'.repeat(50))); + } +} +export function registerNewsCommand(program, ctx) { + program + .command('news') + .alias('trending') + .description('Fetch AI-curated news and trending topics from Explore tabs') + .option('-n, --count ', 'Number of items to fetch', '10') + .option('--ai-only', 'Show only AI-curated news items') + .option('--with-tweets', 'Also fetch related tweets for each news item') + .option('--tweets-per-item ', 'Number of tweets to fetch per news item (default: 5)', '5') + .option('--for-you', 'Fetch only from For You tab') + .option('--news-only', 'Fetch only from News tab') + .option('--sports', 'Fetch only from Sports tab') + .option('--entertainment', 'Fetch only from Entertainment tab') + .option('--trending-only', 'Fetch only from Trending tab') + .option('--json', 'Output as JSON') + .option('--json-full', 'Output as JSON with full raw API response in _raw field') + .action(async (cmdOpts) => { + const opts = program.opts(); + const timeoutMs = ctx.resolveTimeoutFromOptions(opts); + const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts); + const count = Number.parseInt(cmdOpts.count || '10', 10); + const tweetsPerItem = Number.parseInt(cmdOpts.tweetsPerItem || '5', 10); + const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); + for (const warning of warnings) { + console.error(`${ctx.p('warn')}${warning}`); + } + if (Number.isNaN(count) || count < 1) { + console.error(`${ctx.p('err')}--count must be a positive number`); + process.exit(1); + } + if (Number.isNaN(tweetsPerItem) || tweetsPerItem < 1) { + console.error(`${ctx.p('err')}--tweets-per-item must be a positive number`); + process.exit(1); + } + if (!cookies.authToken || !cookies.ct0) { + console.error(`${ctx.p('err')}Missing required credentials`); + process.exit(1); + } + // Determine which tabs to fetch from + const tabs = []; + if (cmdOpts.forYou) { + tabs.push('forYou'); + } + if (cmdOpts.newsOnly) { + tabs.push('news'); + } + if (cmdOpts.sports) { + tabs.push('sports'); + } + if (cmdOpts.entertainment) { + tabs.push('entertainment'); + } + if (cmdOpts.trendingOnly) { + tabs.push('trending'); + } + // If no specific tabs selected, use defaults (all tabs except trending) + const tabsToFetch = tabs.length > 0 ? tabs : undefined; + const client = new TwitterClient({ cookies, timeoutMs, quoteDepth }); + const includeRaw = cmdOpts.jsonFull ?? false; + const withTweets = cmdOpts.withTweets ?? false; + const aiOnly = cmdOpts.aiOnly ?? false; + const result = await client.getNews(count, { + includeRaw, + withTweets, + tweetsPerItem, + aiOnly, + tabs: tabsToFetch, + }); + if (result.success) { + printNewsItems(result.items, ctx, { + json: cmdOpts.json || cmdOpts.jsonFull, + emptyMessage: 'No news items found.', + tweetLimit: withTweets ? tweetsPerItem : undefined, + }); + } + else { + console.error(`${ctx.p('err')}Failed to fetch news: ${result.error}`); + process.exit(1); + } + }); +} +//# sourceMappingURL=news.js.map \ No newline at end of file diff --git a/vendor/package/dist/commands/news.js.map b/vendor/package/dist/commands/news.js.map new file mode 100644 index 0000000..bb0c56c --- /dev/null +++ b/vendor/package/dist/commands/news.js.map @@ -0,0 +1 @@ +{"version":3,"file":"news.js","sourceRoot":"","sources":["../../src/commands/news.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/commands/post.d.ts b/vendor/package/dist/commands/post.d.ts new file mode 100644 index 0000000..e15ffa0 --- /dev/null +++ b/vendor/package/dist/commands/post.d.ts @@ -0,0 +1,4 @@ +import type { Command } from 'commander'; +import type { CliContext } from '../cli/shared.js'; +export declare function registerPostCommands(program: Command, ctx: CliContext): void; +//# sourceMappingURL=post.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/commands/post.d.ts.map b/vendor/package/dist/commands/post.d.ts.map new file mode 100644 index 0000000..87b133a --- /dev/null +++ b/vendor/package/dist/commands/post.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"post.d.ts","sourceRoot":"","sources":["../../src/commands/post.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,UAAU,EAAa,MAAM,kBAAkB,CAAC;AAyB9D,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,GAAG,IAAI,CA4F5E"} \ No newline at end of file diff --git a/vendor/package/dist/commands/post.js b/vendor/package/dist/commands/post.js new file mode 100644 index 0000000..7ffa6ff --- /dev/null +++ b/vendor/package/dist/commands/post.js @@ -0,0 +1,101 @@ +import { formatTweetUrlLine } from '../lib/output.js'; +import { TwitterClient } from '../lib/twitter-client.js'; +async function uploadMediaOrExit(client, media, ctx) { + if (media.length === 0) { + return undefined; + } + const uploaded = []; + for (const item of media) { + const res = await client.uploadMedia({ data: item.buffer, mimeType: item.mime, alt: item.alt }); + if (!res.success || !res.mediaId) { + console.error(`${ctx.p('err')}Media upload failed: ${res.error ?? 'Unknown error'}`); + process.exit(1); + } + uploaded.push(res.mediaId); + } + return uploaded; +} +export function registerPostCommands(program, ctx) { + program + .command('tweet') + .description('Post a new tweet') + .argument('', 'Tweet text') + .action(async (text) => { + const opts = program.opts(); + const timeoutMs = ctx.resolveTimeoutFromOptions(opts); + const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts); + let media = []; + try { + media = ctx.loadMedia({ media: opts.media ?? [], alts: opts.alt ?? [] }); + } + catch (error) { + console.error(`${ctx.p('err')}${error instanceof Error ? error.message : String(error)}`); + process.exit(1); + } + const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); + for (const warning of warnings) { + console.error(`${ctx.p('warn')}${warning}`); + } + if (!cookies.authToken || !cookies.ct0) { + console.error(`${ctx.p('err')}Missing required credentials`); + process.exit(1); + } + if (cookies.source) { + console.error(`${ctx.l('source')}${cookies.source}`); + } + const client = new TwitterClient({ cookies, timeoutMs, quoteDepth }); + const mediaIds = await uploadMediaOrExit(client, media, ctx); + const result = await client.tweet(text, mediaIds); + if (result.success) { + console.log(`${ctx.p('ok')}Tweet posted successfully!`); + console.log(formatTweetUrlLine(result.tweetId, ctx.getOutput())); + } + else { + console.error(`${ctx.p('err')}Failed to post tweet: ${result.error}`); + process.exit(1); + } + }); + program + .command('reply') + .description('Reply to an existing tweet') + .argument('', 'Tweet ID or URL to reply to') + .argument('', 'Reply text') + .action(async (tweetIdOrUrl, text) => { + const opts = program.opts(); + const timeoutMs = ctx.resolveTimeoutFromOptions(opts); + const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts); + let media = []; + try { + media = ctx.loadMedia({ media: opts.media ?? [], alts: opts.alt ?? [] }); + } + catch (error) { + console.error(`${ctx.p('err')}${error instanceof Error ? error.message : String(error)}`); + process.exit(1); + } + const tweetId = ctx.extractTweetId(tweetIdOrUrl); + const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); + for (const warning of warnings) { + console.error(`${ctx.p('warn')}${warning}`); + } + if (!cookies.authToken || !cookies.ct0) { + console.error(`${ctx.p('err')}Missing required credentials`); + process.exit(1); + } + if (cookies.source) { + console.error(`${ctx.l('source')}${cookies.source}`); + } + console.error(`${ctx.p('info')}Replying to tweet: ${tweetId}`); + const client = new TwitterClient({ cookies, timeoutMs, quoteDepth }); + const mediaIds = await uploadMediaOrExit(client, media, ctx); + const result = await client.reply(text, tweetId, mediaIds); + if (result.success) { + console.log(`${ctx.p('ok')}Reply posted successfully!`); + console.log(formatTweetUrlLine(result.tweetId, ctx.getOutput())); + } + else { + console.error(`${ctx.p('err')}Failed to post reply: ${result.error}`); + process.exit(1); + } + }); +} +//# sourceMappingURL=post.js.map \ No newline at end of file diff --git a/vendor/package/dist/commands/post.js.map b/vendor/package/dist/commands/post.js.map new file mode 100644 index 0000000..be6200e --- /dev/null +++ b/vendor/package/dist/commands/post.js.map @@ -0,0 +1 @@ +{"version":3,"file":"post.js","sourceRoot":"","sources":["../../src/commands/post.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAEzD,KAAK,UAAU,iBAAiB,CAC9B,MAAqB,EACrB,KAAkB,EAClB,GAAe;IAEf,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QAChG,IAAI,CAAC,GAAG,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;YACjC,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,wBAAwB,GAAG,CAAC,KAAK,IAAI,eAAe,EAAE,CAAC,CAAC;YACrF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,OAAgB,EAAE,GAAe;IACpE,OAAO;SACJ,OAAO,CAAC,OAAO,CAAC;SAChB,WAAW,CAAC,kBAAkB,CAAC;SAC/B,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;SAChC,MAAM,CAAC,KAAK,EAAE,IAAY,EAAE,EAAE;QAC7B,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC5B,MAAM,SAAS,GAAG,GAAG,CAAC,yBAAyB,CAAC,IAAI,CAAC,CAAC;QACtD,MAAM,UAAU,GAAG,GAAG,CAAC,4BAA4B,CAAC,IAAI,CAAC,CAAC;QAC1D,IAAI,KAAK,GAAgB,EAAE,CAAC;QAC5B,IAAI,CAAC;YACH,KAAK,GAAG,GAAG,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,IAAI,EAAE,EAAE,CAAC,CAAC;QAC3E,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAC1F,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,MAAM,GAAG,CAAC,6BAA6B,CAAC,IAAI,CAAC,CAAC;QAE5E,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,OAAO,EAAE,CAAC,CAAC;QAC9C,CAAC;QAED,IAAI,CAAC,OAAO,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;YACvC,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC;YAC7D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YACnB,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QACvD,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAC;QACrE,MAAM,QAAQ,GAAG,MAAM,iBAAiB,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;QAC7D,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QAElD,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC;YACxD,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;QACnE,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,yBAAyB,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;YACtE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,CAAC,CAAC;IAEL,OAAO;SACJ,OAAO,CAAC,OAAO,CAAC;SAChB,WAAW,CAAC,4BAA4B,CAAC;SACzC,QAAQ,CAAC,mBAAmB,EAAE,6BAA6B,CAAC;SAC5D,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;SAChC,MAAM,CAAC,KAAK,EAAE,YAAoB,EAAE,IAAY,EAAE,EAAE;QACnD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC5B,MAAM,SAAS,GAAG,GAAG,CAAC,yBAAyB,CAAC,IAAI,CAAC,CAAC;QACtD,MAAM,UAAU,GAAG,GAAG,CAAC,4BAA4B,CAAC,IAAI,CAAC,CAAC;QAC1D,IAAI,KAAK,GAAgB,EAAE,CAAC;QAC5B,IAAI,CAAC;YACH,KAAK,GAAG,GAAG,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,IAAI,EAAE,EAAE,CAAC,CAAC;QAC3E,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAC1F,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QACD,MAAM,OAAO,GAAG,GAAG,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC;QAEjD,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,MAAM,GAAG,CAAC,6BAA6B,CAAC,IAAI,CAAC,CAAC;QAE5E,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,OAAO,EAAE,CAAC,CAAC;QAC9C,CAAC;QAED,IAAI,CAAC,OAAO,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;YACvC,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC;YAC7D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YACnB,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QACvD,CAAC;QAED,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,sBAAsB,OAAO,EAAE,CAAC,CAAC;QAE/D,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAC;QACrE,MAAM,QAAQ,GAAG,MAAM,iBAAiB,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;QAC7D,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;QAE3D,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC;YACxD,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;QACnE,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,yBAAyB,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;YACtE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC"} \ No newline at end of file diff --git a/vendor/package/dist/commands/query-ids.d.ts b/vendor/package/dist/commands/query-ids.d.ts new file mode 100644 index 0000000..2c02e55 --- /dev/null +++ b/vendor/package/dist/commands/query-ids.d.ts @@ -0,0 +1,4 @@ +import type { Command } from 'commander'; +import type { CliContext } from '../cli/shared.js'; +export declare function registerQueryIdsCommand(program: Command, ctx: CliContext): void; +//# sourceMappingURL=query-ids.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/commands/query-ids.d.ts.map b/vendor/package/dist/commands/query-ids.d.ts.map new file mode 100644 index 0000000..c216075 --- /dev/null +++ b/vendor/package/dist/commands/query-ids.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"query-ids.d.ts","sourceRoot":"","sources":["../../src/commands/query-ids.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAqBnD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,GAAG,IAAI,CAgF/E"} \ No newline at end of file diff --git a/vendor/package/dist/commands/query-ids.js b/vendor/package/dist/commands/query-ids.js new file mode 100644 index 0000000..dfb1482 --- /dev/null +++ b/vendor/package/dist/commands/query-ids.js @@ -0,0 +1,80 @@ +import { getFeatureOverridesSnapshot, refreshFeatureOverridesCache, } from '../lib/runtime-features.js'; +import { runtimeQueryIds } from '../lib/runtime-query-ids.js'; +function countFeatureOverrides(overrides) { + let count = 0; + if (overrides.global) { + count += Object.keys(overrides.global).length; + } + if (overrides.sets) { + for (const setOverrides of Object.values(overrides.sets)) { + count += Object.keys(setOverrides).length; + } + } + return count; +} +export function registerQueryIdsCommand(program, ctx) { + program + .command('query-ids') + .description('Show or refresh cached Twitter GraphQL query IDs') + .option('--json', 'Output as JSON') + .option('--fresh', 'Force refresh (downloads X client bundles)', false) + .action(async (cmdOpts) => { + const operations = [ + 'CreateTweet', + 'CreateRetweet', + 'FavoriteTweet', + 'TweetDetail', + 'SearchTimeline', + 'UserArticlesTweets', + 'Bookmarks', + 'Following', + 'Followers', + 'Likes', + ]; + if (cmdOpts.fresh) { + console.error(`${ctx.p('info')}Refreshing GraphQL query IDs…`); + await runtimeQueryIds.refresh(operations, { force: true }); + console.error(`${ctx.p('info')}Refreshing feature overrides…`); + await refreshFeatureOverridesCache(); + } + const featureSnapshot = getFeatureOverridesSnapshot(); + const info = await runtimeQueryIds.getSnapshotInfo(); + if (!info) { + if (cmdOpts.json) { + console.log(JSON.stringify({ + cached: false, + cachePath: runtimeQueryIds.cachePath, + featuresPath: featureSnapshot.cachePath, + features: featureSnapshot.overrides, + }, null, 2)); + return; + } + console.log(`${ctx.p('warn')}No cached query IDs yet.`); + console.log(`${ctx.p('info')}Run: bird query-ids --fresh`); + console.log(`features_path: ${featureSnapshot.cachePath}`); + return; + } + if (cmdOpts.json) { + console.log(JSON.stringify({ + cached: true, + cachePath: info.cachePath, + fetchedAt: info.snapshot.fetchedAt, + isFresh: info.isFresh, + ageMs: info.ageMs, + ids: info.snapshot.ids, + discovery: info.snapshot.discovery, + featuresPath: featureSnapshot.cachePath, + features: featureSnapshot.overrides, + }, null, 2)); + return; + } + console.log(`${ctx.p('ok')}GraphQL query IDs cached`); + console.log(`path: ${info.cachePath}`); + console.log(`fetched_at: ${info.snapshot.fetchedAt}`); + console.log(`fresh: ${info.isFresh ? 'yes' : 'no'}`); + console.log(`ops: ${Object.keys(info.snapshot.ids).length}`); + console.log(`features_path: ${featureSnapshot.cachePath}`); + console.log(`features: ${countFeatureOverrides(featureSnapshot.overrides)}`); + }); +} +//# sourceMappingURL=query-ids.js.map \ No newline at end of file diff --git a/vendor/package/dist/commands/query-ids.js.map b/vendor/package/dist/commands/query-ids.js.map new file mode 100644 index 0000000..65a6b7c --- /dev/null +++ b/vendor/package/dist/commands/query-ids.js.map @@ -0,0 +1 @@ +{"version":3,"file":"query-ids.js","sourceRoot":"","sources":["../../src/commands/query-ids.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/commands/read.d.ts b/vendor/package/dist/commands/read.d.ts new file mode 100644 index 0000000..ed56932 --- /dev/null +++ b/vendor/package/dist/commands/read.d.ts @@ -0,0 +1,4 @@ +import type { Command } from 'commander'; +import type { CliContext } from '../cli/shared.js'; +export declare function registerReadCommands(program: Command, ctx: CliContext): void; +//# sourceMappingURL=read.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/commands/read.d.ts.map b/vendor/package/dist/commands/read.d.ts.map new file mode 100644 index 0000000..4aa4aa8 --- /dev/null +++ b/vendor/package/dist/commands/read.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../src/commands/read.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAInD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,GAAG,IAAI,CAqM5E"} \ No newline at end of file diff --git a/vendor/package/dist/commands/read.js b/vendor/package/dist/commands/read.js new file mode 100644 index 0000000..9d239d6 --- /dev/null +++ b/vendor/package/dist/commands/read.js @@ -0,0 +1,152 @@ +import { parsePaginationFlags } from '../cli/pagination.js'; +import { formatStatsLine } from '../lib/output.js'; +import { TwitterClient } from '../lib/twitter-client.js'; +export function registerReadCommands(program, ctx) { + program + .command('read') + .description('Read/fetch a tweet by ID or URL') + .argument('', 'Tweet ID or URL to read') + .option('--json', 'Output as JSON') + .option('--json-full', 'Output as JSON with full raw API response in _raw field') + .action(async (tweetIdOrUrl, cmdOpts) => { + const opts = program.opts(); + const timeoutMs = ctx.resolveTimeoutFromOptions(opts); + const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts); + const tweetId = ctx.extractTweetId(tweetIdOrUrl); + const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); + for (const warning of warnings) { + console.error(`${ctx.p('warn')}${warning}`); + } + if (!cookies.authToken || !cookies.ct0) { + console.error(`${ctx.p('err')}Missing required credentials`); + process.exit(1); + } + const client = new TwitterClient({ cookies, timeoutMs, quoteDepth }); + const includeRaw = cmdOpts.jsonFull ?? false; + const result = await client.getTweet(tweetId, { includeRaw }); + if (result.success && result.tweet) { + if (cmdOpts.json || cmdOpts.jsonFull) { + console.log(JSON.stringify(result.tweet, null, 2)); + } + else { + ctx.printTweets([result.tweet], { showSeparator: false }); + console.log(formatStatsLine(result.tweet, ctx.getOutput())); + } + } + else { + console.error(`${ctx.p('err')}Failed to read tweet: ${result.error}`); + process.exit(1); + } + }); + program + .command('replies') + .description('List replies to a tweet (by ID or URL)') + .argument('', 'Tweet ID or URL') + .option('--all', 'Fetch all replies (paged)') + .option('--max-pages ', 'Fetch N pages (implies pagination)') + .option('--delay ', 'Delay in ms between page fetches', '1000') + .option('--cursor ', 'Resume pagination from a cursor') + .option('--json', 'Output as JSON') + .option('--json-full', 'Output as JSON with full raw API response in _raw field') + .action(async (tweetIdOrUrl, cmdOpts) => { + const opts = program.opts(); + const timeoutMs = ctx.resolveTimeoutFromOptions(opts); + const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts); + const tweetId = ctx.extractTweetId(tweetIdOrUrl); + const pagination = parsePaginationFlags(cmdOpts, { maxPagesImpliesPagination: true, includeDelay: true }); + if (!pagination.ok) { + console.error(`${ctx.p('err')}${pagination.error}`); + process.exit(1); + } + const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); + for (const warning of warnings) { + console.error(`${ctx.p('warn')}${warning}`); + } + if (!cookies.authToken || !cookies.ct0) { + console.error(`${ctx.p('err')}Missing required credentials`); + process.exit(1); + } + const client = new TwitterClient({ cookies, timeoutMs, quoteDepth }); + const includeRaw = cmdOpts.jsonFull ?? false; + const result = pagination.usePagination + ? await client.getRepliesPaged(tweetId, { + includeRaw, + maxPages: pagination.maxPages, + cursor: pagination.cursor, + pageDelayMs: pagination.pageDelayMs, + }) + : await client.getReplies(tweetId, { includeRaw }); + const isJson = Boolean(cmdOpts.json || cmdOpts.jsonFull); + if (result.tweets) { + ctx.printTweetsResult(result, { + json: isJson, + usePagination: pagination.usePagination, + emptyMessage: 'No replies found.', + }); + // Show pagination hint if there's more + if (result.nextCursor && !isJson) { + console.error(`${ctx.p('info')}More replies available. Use --cursor "${result.nextCursor}" to continue.`); + } + } + if (!result.success) { + console.error(`${ctx.p('err')}Failed to fetch replies: ${result.error}`); + process.exit(1); + } + }); + program + .command('thread') + .description('Show the full conversation thread containing the tweet') + .argument('', 'Tweet ID or URL') + .option('--all', 'Fetch all thread tweets (paged)') + .option('--max-pages ', 'Fetch N pages (implies pagination)') + .option('--delay ', 'Delay in ms between page fetches', '1000') + .option('--cursor ', 'Resume pagination from a cursor') + .option('--json', 'Output as JSON') + .option('--json-full', 'Output as JSON with full raw API response in _raw field') + .action(async (tweetIdOrUrl, cmdOpts) => { + const opts = program.opts(); + const timeoutMs = ctx.resolveTimeoutFromOptions(opts); + const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts); + const tweetId = ctx.extractTweetId(tweetIdOrUrl); + const pagination = parsePaginationFlags(cmdOpts, { maxPagesImpliesPagination: true, includeDelay: true }); + if (!pagination.ok) { + console.error(`${ctx.p('err')}${pagination.error}`); + process.exit(1); + } + const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); + for (const warning of warnings) { + console.error(`${ctx.p('warn')}${warning}`); + } + if (!cookies.authToken || !cookies.ct0) { + console.error(`${ctx.p('err')}Missing required credentials`); + process.exit(1); + } + const client = new TwitterClient({ cookies, timeoutMs, quoteDepth }); + const includeRaw = cmdOpts.jsonFull ?? false; + const result = pagination.usePagination + ? await client.getThreadPaged(tweetId, { + includeRaw, + maxPages: pagination.maxPages, + cursor: pagination.cursor, + pageDelayMs: pagination.pageDelayMs, + }) + : await client.getThread(tweetId, { includeRaw }); + const isJson = Boolean(cmdOpts.json || cmdOpts.jsonFull); + if (result.tweets) { + ctx.printTweetsResult(result, { + json: isJson, + usePagination: pagination.usePagination, + emptyMessage: 'No thread tweets found.', + }); + // Show pagination hint if there's more + if (result.nextCursor && !isJson) { + console.error(`${ctx.p('info')}More thread tweets available. Use --cursor "${result.nextCursor}" to continue.`); + } + } + if (!result.success) { + console.error(`${ctx.p('err')}Failed to fetch thread: ${result.error}`); + process.exit(1); + } + }); +} +//# sourceMappingURL=read.js.map \ No newline at end of file diff --git a/vendor/package/dist/commands/read.js.map b/vendor/package/dist/commands/read.js.map new file mode 100644 index 0000000..de87637 --- /dev/null +++ b/vendor/package/dist/commands/read.js.map @@ -0,0 +1 @@ +{"version":3,"file":"read.js","sourceRoot":"","sources":["../../src/commands/read.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/commands/search.d.ts b/vendor/package/dist/commands/search.d.ts new file mode 100644 index 0000000..a2178b7 --- /dev/null +++ b/vendor/package/dist/commands/search.d.ts @@ -0,0 +1,4 @@ +import type { Command } from 'commander'; +import type { CliContext } from '../cli/shared.js'; +export declare function registerSearchCommands(program: Command, ctx: CliContext): void; +//# sourceMappingURL=search.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/commands/search.d.ts.map b/vendor/package/dist/commands/search.d.ts.map new file mode 100644 index 0000000..04db00d --- /dev/null +++ b/vendor/package/dist/commands/search.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/commands/search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAInD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,GAAG,IAAI,CA0I9E"} \ No newline at end of file diff --git a/vendor/package/dist/commands/search.js b/vendor/package/dist/commands/search.js new file mode 100644 index 0000000..4b5c2cc --- /dev/null +++ b/vendor/package/dist/commands/search.js @@ -0,0 +1,115 @@ +import { parsePaginationFlags } from '../cli/pagination.js'; +import { mentionsQueryFromUserOption, normalizeHandle } from '../lib/normalize-handle.js'; +import { TwitterClient } from '../lib/twitter-client.js'; +export function registerSearchCommands(program, ctx) { + program + .command('search') + .description('Search for tweets') + .argument('', 'Search query (e.g., "@clawdbot" or "from:clawdbot")') + .option('-n, --count ', 'Number of tweets to fetch', '10') + .option('--all', 'Fetch all search results (paged)') + .option('--max-pages ', 'Stop after N pages when using --all') + .option('--cursor ', 'Resume pagination from a cursor') + .option('--json', 'Output as JSON') + .option('--json-full', 'Output as JSON with full raw API response in _raw field') + .action(async (query, cmdOpts) => { + const opts = program.opts(); + const timeoutMs = ctx.resolveTimeoutFromOptions(opts); + const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts); + const count = Number.parseInt(cmdOpts.count || '10', 10); + const pagination = parsePaginationFlags(cmdOpts); + if (!pagination.ok) { + console.error(`${ctx.p('err')}${pagination.error}`); + process.exit(1); + } + const maxPages = pagination.maxPages; + const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); + for (const warning of warnings) { + console.error(`${ctx.p('warn')}${warning}`); + } + if (!cookies.authToken || !cookies.ct0) { + console.error(`${ctx.p('err')}Missing required credentials`); + process.exit(1); + } + const usePagination = pagination.usePagination; + if (maxPages !== undefined && !usePagination) { + console.error(`${ctx.p('err')}--max-pages requires --all or --cursor.`); + process.exit(1); + } + if (!usePagination && (!Number.isFinite(count) || count <= 0)) { + console.error(`${ctx.p('err')}Invalid --count. Expected a positive integer.`); + process.exit(1); + } + const client = new TwitterClient({ cookies, timeoutMs, quoteDepth }); + const includeRaw = cmdOpts.jsonFull ?? false; + const searchOptions = { includeRaw }; + const paginationOptions = { includeRaw, maxPages, cursor: pagination.cursor }; + const result = usePagination + ? await client.getAllSearchResults(query, paginationOptions) + : await client.search(query, count, searchOptions); + if (result.success) { + const isJson = Boolean(cmdOpts.json || cmdOpts.jsonFull); + ctx.printTweetsResult(result, { + json: isJson, + usePagination: Boolean(usePagination), + emptyMessage: 'No tweets found.', + }); + } + else { + console.error(`${ctx.p('err')}Search failed: ${result.error}`); + process.exit(1); + } + }); + program + .command('mentions') + .description('Find tweets mentioning a user (defaults to current user)') + .option('-u, --user ', 'User handle (e.g. @steipete)') + .option('-n, --count ', 'Number of tweets to fetch', '10') + .option('--json', 'Output as JSON') + .option('--json-full', 'Output as JSON with full raw API response in _raw field') + .action(async (cmdOpts) => { + const opts = program.opts(); + const timeoutMs = ctx.resolveTimeoutFromOptions(opts); + const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts); + const count = Number.parseInt(cmdOpts.count || '10', 10); + const fromUserOpt = mentionsQueryFromUserOption(cmdOpts.user); + if (fromUserOpt.error) { + console.error(`${ctx.p('err')}${fromUserOpt.error}`); + process.exit(2); + } + let query = fromUserOpt.query; + const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); + for (const warning of warnings) { + console.error(`${ctx.p('warn')}${warning}`); + } + if (!cookies.authToken || !cookies.ct0) { + console.error(`${ctx.p('err')}Missing required credentials`); + process.exit(1); + } + const client = new TwitterClient({ cookies, timeoutMs, quoteDepth }); + if (!query) { + const who = await client.getCurrentUser(); + const handle = normalizeHandle(who.user?.username); + if (handle) { + query = `@${handle}`; + } + else { + console.error(`${ctx.p('err')}Could not determine current user (${who.error ?? 'Unknown error'}). Use --user .`); + process.exit(1); + } + } + const includeRaw = cmdOpts.jsonFull ?? false; + const result = await client.search(query, count, { includeRaw }); + if (result.success) { + ctx.printTweets(result.tweets, { + json: cmdOpts.json || cmdOpts.jsonFull, + emptyMessage: 'No mentions found.', + }); + } + else { + console.error(`${ctx.p('err')}Failed to fetch mentions: ${result.error}`); + process.exit(1); + } + }); +} +//# sourceMappingURL=search.js.map \ No newline at end of file diff --git a/vendor/package/dist/commands/search.js.map b/vendor/package/dist/commands/search.js.map new file mode 100644 index 0000000..354c892 --- /dev/null +++ b/vendor/package/dist/commands/search.js.map @@ -0,0 +1 @@ +{"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/commands/search.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/commands/unbookmark.d.ts b/vendor/package/dist/commands/unbookmark.d.ts new file mode 100644 index 0000000..eebb34a --- /dev/null +++ b/vendor/package/dist/commands/unbookmark.d.ts @@ -0,0 +1,4 @@ +import type { Command } from 'commander'; +import type { CliContext } from '../cli/shared.js'; +export declare function registerUnbookmarkCommand(program: Command, ctx: CliContext): void; +//# sourceMappingURL=unbookmark.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/commands/unbookmark.d.ts.map b/vendor/package/dist/commands/unbookmark.d.ts.map new file mode 100644 index 0000000..f940b3b --- /dev/null +++ b/vendor/package/dist/commands/unbookmark.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"unbookmark.d.ts","sourceRoot":"","sources":["../../src/commands/unbookmark.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,GAAG,IAAI,CAsCjF"} \ No newline at end of file diff --git a/vendor/package/dist/commands/unbookmark.js b/vendor/package/dist/commands/unbookmark.js new file mode 100644 index 0000000..5115b95 --- /dev/null +++ b/vendor/package/dist/commands/unbookmark.js @@ -0,0 +1,36 @@ +import { TwitterClient } from '../lib/twitter-client.js'; +export function registerUnbookmarkCommand(program, ctx) { + program + .command('unbookmark') + .description('Remove bookmarked tweets') + .argument('', 'Tweet IDs or URLs to remove from bookmarks') + .action(async (tweetIdOrUrls) => { + const opts = program.opts(); + const timeoutMs = ctx.resolveTimeoutFromOptions(opts); + const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); + for (const warning of warnings) { + console.error(`${ctx.p('warn')}${warning}`); + } + if (!cookies.authToken || !cookies.ct0) { + console.error(`${ctx.p('err')}Missing required credentials`); + process.exit(1); + } + const client = new TwitterClient({ cookies, timeoutMs }); + let failures = 0; + for (const input of tweetIdOrUrls) { + const tweetId = ctx.extractTweetId(input); + const result = await client.unbookmark(tweetId); + if (result.success) { + console.log(`${ctx.p('ok')}Removed bookmark for ${tweetId}`); + } + else { + failures += 1; + console.error(`${ctx.p('err')}Failed to remove bookmark for ${tweetId}: ${result.error}`); + } + } + if (failures > 0) { + process.exit(1); + } + }); +} +//# sourceMappingURL=unbookmark.js.map \ No newline at end of file diff --git a/vendor/package/dist/commands/unbookmark.js.map b/vendor/package/dist/commands/unbookmark.js.map new file mode 100644 index 0000000..6f85d0f --- /dev/null +++ b/vendor/package/dist/commands/unbookmark.js.map @@ -0,0 +1 @@ +{"version":3,"file":"unbookmark.js","sourceRoot":"","sources":["../../src/commands/unbookmark.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAEzD,MAAM,UAAU,yBAAyB,CAAC,OAAgB,EAAE,GAAe;IACzE,OAAO;SACJ,OAAO,CAAC,YAAY,CAAC;SACrB,WAAW,CAAC,0BAA0B,CAAC;SACvC,QAAQ,CAAC,sBAAsB,EAAE,4CAA4C,CAAC;SAC9E,MAAM,CAAC,KAAK,EAAE,aAAuB,EAAE,EAAE;QACxC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC5B,MAAM,SAAS,GAAG,GAAG,CAAC,yBAAyB,CAAC,IAAI,CAAC,CAAC;QAEtD,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,MAAM,GAAG,CAAC,6BAA6B,CAAC,IAAI,CAAC,CAAC;QAE5E,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,OAAO,EAAE,CAAC,CAAC;QAC9C,CAAC;QAED,IAAI,CAAC,OAAO,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;YACvC,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC;YAC7D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC;QACzD,IAAI,QAAQ,GAAG,CAAC,CAAC;QAEjB,KAAK,MAAM,KAAK,IAAI,aAAa,EAAE,CAAC;YAClC,MAAM,OAAO,GAAG,GAAG,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;YAC1C,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;YAChD,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;gBACnB,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,wBAAwB,OAAO,EAAE,CAAC,CAAC;YAC/D,CAAC;iBAAM,CAAC;gBACN,QAAQ,IAAI,CAAC,CAAC;gBACd,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,iCAAiC,OAAO,KAAK,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;YAC5F,CAAC;QACH,CAAC;QAED,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;YACjB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC"} \ No newline at end of file diff --git a/vendor/package/dist/commands/user-tweets.d.ts b/vendor/package/dist/commands/user-tweets.d.ts new file mode 100644 index 0000000..c239634 --- /dev/null +++ b/vendor/package/dist/commands/user-tweets.d.ts @@ -0,0 +1,4 @@ +import type { Command } from 'commander'; +import type { CliContext } from '../cli/shared.js'; +export declare function registerUserTweetsCommand(program: Command, ctx: CliContext): void; +//# sourceMappingURL=user-tweets.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/commands/user-tweets.d.ts.map b/vendor/package/dist/commands/user-tweets.d.ts.map new file mode 100644 index 0000000..deed518 --- /dev/null +++ b/vendor/package/dist/commands/user-tweets.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"user-tweets.d.ts","sourceRoot":"","sources":["../../src/commands/user-tweets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAInD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,GAAG,IAAI,CAwIjF"} \ No newline at end of file diff --git a/vendor/package/dist/commands/user-tweets.js b/vendor/package/dist/commands/user-tweets.js new file mode 100644 index 0000000..8c718e3 --- /dev/null +++ b/vendor/package/dist/commands/user-tweets.js @@ -0,0 +1,109 @@ +import { parseNonNegativeIntFlag, parsePositiveIntFlag } from '../cli/pagination.js'; +import { normalizeHandle } from '../lib/normalize-handle.js'; +import { TwitterClient } from '../lib/twitter-client.js'; +export function registerUserTweetsCommand(program, ctx) { + const formatExample = (cmd, desc) => ` ${ctx.colors.command(cmd)}\n ${ctx.colors.muted(desc)}`; + program + .command('user-tweets') + .description("Get tweets from a user's profile timeline") + .argument('', 'Username to fetch tweets from (e.g., @steipete or steipete)') + .option('-n, --count ', 'Number of tweets to fetch', '20') + .option('--max-pages ', 'Stop after N pages (max: 10)') + .option('--delay ', 'Delay in ms between page fetches', '1000') + .option('--cursor ', 'Resume pagination from a cursor') + .option('--json', 'Output as JSON') + .option('--json-full', 'Output as JSON with full raw API response in _raw field') + .addHelpText('after', () => `\n${ctx.colors.section('Command Examples')}\n${[ + formatExample('bird user-tweets @steipete', 'Get recent tweets from a user'), + formatExample('bird user-tweets steipete -n 10', 'Get 10 tweets (@ is optional)'), + formatExample('bird user-tweets @steipete -n 50', 'Fetch 50 tweets (paged)'), + formatExample('bird user-tweets @steipete --max-pages 3 -n 200', 'Safety cap (max 3 pages)'), + formatExample('bird user-tweets @steipete --json', 'Output as JSON'), + formatExample('bird user-tweets @steipete --cursor "DAABCg..."', 'Resume from cursor'), + ].join('\n')}`) + .action(async (handle, cmdOpts) => { + const opts = program.opts(); + const timeoutMs = ctx.resolveTimeoutFromOptions(opts); + const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts); + const count = Number.parseInt(cmdOpts.count || '20', 10); + const maxPagesParsed = parsePositiveIntFlag(cmdOpts.maxPages, '--max-pages'); + if (!maxPagesParsed.ok) { + console.error(`${ctx.p('err')}${maxPagesParsed.error}`); + process.exit(2); + } + const maxPages = maxPagesParsed.value; + const delayParsed = parseNonNegativeIntFlag(cmdOpts.delay, '--delay', 1000); + if (!delayParsed.ok) { + console.error(`${ctx.p('err')}${delayParsed.error}`); + process.exit(2); + } + const pageDelayMs = delayParsed.value; + // Validate inputs + if (!Number.isFinite(count) || count <= 0) { + console.error(`${ctx.p('err')}Invalid --count. Expected a positive integer.`); + process.exit(2); + } + const pageSize = 20; + const hardMaxPages = 10; + const hardMaxTweets = pageSize * hardMaxPages; + if (count > hardMaxTweets) { + console.error(`${ctx.p('err')}Invalid --count. Max ${hardMaxTweets} tweets per run (safety cap: ${hardMaxPages} pages). Use --cursor to continue.`); + process.exit(2); + } + if (maxPages !== undefined && maxPages > hardMaxPages) { + console.error(`${ctx.p('err')}Invalid --max-pages. Expected a positive integer (max: ${hardMaxPages}).`); + process.exit(2); + } + // Normalize handle (strip @ if present) + const username = normalizeHandle(handle); + if (!username) { + console.error(`${ctx.p('err')}Invalid handle: ${handle}`); + process.exit(2); + } + const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); + for (const warning of warnings) { + console.error(`${ctx.p('warn')}${warning}`); + } + if (!cookies.authToken || !cookies.ct0) { + console.error(`${ctx.p('err')}Missing required credentials`); + process.exit(1); + } + const client = new TwitterClient({ cookies, timeoutMs, quoteDepth }); + // Look up user ID from username + console.error(`${ctx.p('info')}Looking up @${username}...`); + const userLookup = await client.getUserIdByUsername(username); + if (!userLookup.success || !userLookup.userId) { + console.error(`${ctx.p('err')}${userLookup.error || `Could not find user @${username}`}`); + process.exit(1); + } + const displayName = userLookup.name + ? `${userLookup.name} (@${userLookup.username})` + : `@${userLookup.username}`; + console.error(`${ctx.p('info')}Fetching tweets from ${displayName}...`); + const includeRaw = cmdOpts.jsonFull ?? false; + const wantsPaginationOutput = Boolean(cmdOpts.cursor) || maxPages !== undefined || count > pageSize; + const result = await client.getUserTweetsPaged(userLookup.userId, count, { + includeRaw, + maxPages, + cursor: cmdOpts.cursor, + pageDelayMs, + }); + if (result.success) { + const isJson = Boolean(cmdOpts.json || cmdOpts.jsonFull); + ctx.printTweetsResult(result, { + json: isJson, + usePagination: wantsPaginationOutput, + emptyMessage: `No tweets found for @${username}.`, + }); + // Show pagination hint if there's more + if (result.nextCursor && !cmdOpts.json && !cmdOpts.jsonFull) { + console.error(`${ctx.p('info')}More tweets available. Use --cursor "${result.nextCursor}" to continue.`); + } + } + else { + console.error(`${ctx.p('err')}Failed to fetch tweets: ${result.error}`); + process.exit(1); + } + }); +} +//# sourceMappingURL=user-tweets.js.map \ No newline at end of file diff --git a/vendor/package/dist/commands/user-tweets.js.map b/vendor/package/dist/commands/user-tweets.js.map new file mode 100644 index 0000000..6a5e639 --- /dev/null +++ b/vendor/package/dist/commands/user-tweets.js.map @@ -0,0 +1 @@ +{"version":3,"file":"user-tweets.js","sourceRoot":"","sources":["../../src/commands/user-tweets.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/commands/users.d.ts b/vendor/package/dist/commands/users.d.ts new file mode 100644 index 0000000..1ca7c91 --- /dev/null +++ b/vendor/package/dist/commands/users.d.ts @@ -0,0 +1,4 @@ +import type { Command } from 'commander'; +import type { CliContext } from '../cli/shared.js'; +export declare function registerUserCommands(program: Command, ctx: CliContext): void; +//# sourceMappingURL=users.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/commands/users.d.ts.map b/vendor/package/dist/commands/users.d.ts.map new file mode 100644 index 0000000..1b89906 --- /dev/null +++ b/vendor/package/dist/commands/users.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../src/commands/users.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAwMnD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,GAAG,IAAI,CAkL5E"} \ No newline at end of file diff --git a/vendor/package/dist/commands/users.js b/vendor/package/dist/commands/users.js new file mode 100644 index 0000000..be3a2ea --- /dev/null +++ b/vendor/package/dist/commands/users.js @@ -0,0 +1,295 @@ +import { normalizeHandle } from '../lib/normalize-handle.js'; +import { TwitterClient } from '../lib/twitter-client.js'; +function formatAboutProfile(profile, ctx, handle) { + const lines = [`${ctx.p('info')}Account information for @${handle}:`]; + if (profile.accountBasedIn) { + lines.push(` Account based in: ${profile.accountBasedIn}`); + } + if (profile.createdCountryAccurate !== undefined) { + lines.push(` Creation country accurate: ${profile.createdCountryAccurate ? 'Yes' : 'No'}`); + } + if (profile.locationAccurate !== undefined) { + lines.push(` Location accurate: ${profile.locationAccurate ? 'Yes' : 'No'}`); + } + if (profile.source) { + lines.push(`${ctx.l('source')}${profile.source}`); + } + if (profile.learnMoreUrl) { + lines.push(` Learn more: ${profile.learnMoreUrl}`); + } + return lines; +} +function printUsers(users, ctx) { + for (const user of users) { + console.log(`@${user.username} (${user.name})`); + if (user.description) { + console.log(` ${user.description.slice(0, 100)}${user.description.length > 100 ? '...' : ''}`); + } + if (user.followersCount !== undefined) { + console.log(` ${ctx.p('info')}${user.followersCount.toLocaleString()} followers`); + } + console.log('──────────────────────────────────────────────────'); + } +} +async function resolveUserIdOrExit(client, requestedUserId, ctx) { + if (requestedUserId) { + return requestedUserId; + } + const currentUser = await client.getCurrentUser(); + if (!currentUser.success || !currentUser.user?.id) { + console.error(`${ctx.p('err')}Failed to get current user: ${currentUser.error || 'Unknown error'}`); + process.exit(1); + } + return currentUser.user.id; +} +async function runUserListCommand(program, ctx, spec, cmdOpts) { + const opts = program.opts(); + const timeoutMs = ctx.resolveTimeoutFromOptions(opts); + const count = Number.parseInt(cmdOpts.count || '20', 10); + const maxPages = cmdOpts.maxPages ? Number.parseInt(cmdOpts.maxPages, 10) : undefined; + const usePagination = cmdOpts.all || cmdOpts.cursor; + if (maxPages !== undefined && !cmdOpts.all) { + console.error(`${ctx.p('err')}--max-pages requires --all.`); + process.exit(1); + } + if (maxPages !== undefined && (!Number.isFinite(maxPages) || maxPages <= 0)) { + console.error(`${ctx.p('err')}Invalid --max-pages. Expected a positive integer.`); + process.exit(1); + } + const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); + for (const warning of warnings) { + console.error(`${ctx.p('warn')}${warning}`); + } + if (!cookies.authToken || !cookies.ct0) { + console.error(`${ctx.p('err')}Missing required credentials`); + process.exit(1); + } + const client = new TwitterClient({ cookies, timeoutMs }); + const userId = await resolveUserIdOrExit(client, cmdOpts.user, ctx); + if (cmdOpts.all) { + const allUsers = []; + const seen = new Set(); + let cursor = cmdOpts.cursor; + let pageNum = 0; + let nextCursor; + while (true) { + pageNum += 1; + if (!cmdOpts.json) { + console.error(`${ctx.p('info')}Fetching page ${pageNum}...`); + } + const result = await spec.fetch(client, userId, count, cursor); + if (!result.success || !result.users) { + console.error(`${ctx.p('err')}Failed to fetch ${spec.name}: ${result.error}`); + process.exit(1); + } + let added = 0; + for (const user of result.users) { + if (!seen.has(user.id)) { + seen.add(user.id); + allUsers.push(user); + added += 1; + } + } + const pageCursor = result.nextCursor; + if (!pageCursor || result.users.length === 0 || added === 0 || pageCursor === cursor) { + nextCursor = undefined; + break; + } + if (maxPages && pageNum >= maxPages) { + nextCursor = pageCursor; + break; + } + cursor = pageCursor; + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + if (cmdOpts.json) { + console.log(JSON.stringify({ users: allUsers, nextCursor: nextCursor ?? null }, null, 2)); + } + else { + console.error(`${ctx.p('info')}Total: ${allUsers.length} users`); + if (nextCursor) { + console.error(`${ctx.p('info')}Stopped at --max-pages. Use --cursor to continue.`); + console.error(`${ctx.p('info')}Next cursor: ${nextCursor}`); + } + printUsers(allUsers, ctx); + } + return; + } + const result = await spec.fetch(client, userId, count, cmdOpts.cursor); + if (result.success && result.users) { + if (cmdOpts.json) { + if (usePagination) { + console.log(JSON.stringify({ users: result.users, nextCursor: result.nextCursor ?? null }, null, 2)); + } + else { + console.log(JSON.stringify(result.users, null, 2)); + } + } + else { + if (result.users.length === 0) { + console.log('No users found.'); + } + else { + printUsers(result.users, ctx); + if (result.nextCursor) { + console.error(`${ctx.p('info')}Next cursor: ${result.nextCursor}`); + } + } + } + } + else { + console.error(`${ctx.p('err')}Failed to fetch ${spec.name}: ${result.error}`); + process.exit(1); + } +} +export function registerUserCommands(program, ctx) { + const registerUserListCommand = (spec) => { + program + .command(spec.name) + .description(spec.description) + .option('--user ', `User ID to get ${spec.name} for (defaults to current user)`) + .option('-n, --count ', 'Number of users to fetch per page', '20') + .option('--cursor ', 'Cursor for pagination (from previous response)') + .option('--all', 'Fetch all users (paginate automatically)') + .option('--max-pages ', 'Stop after N pages when using --all') + .option('--json', 'Output as JSON') + .action(async (cmdOpts) => runUserListCommand(program, ctx, spec, cmdOpts)); + }; + registerUserListCommand({ + name: 'following', + description: 'Get users that you (or another user) follow', + fetch: (client, userId, count, cursor) => client.getFollowing(userId, count, cursor), + }); + registerUserListCommand({ + name: 'followers', + description: 'Get users that follow you (or another user)', + fetch: (client, userId, count, cursor) => client.getFollowers(userId, count, cursor), + }); + program + .command('likes') + .description('Get your liked tweets') + .option('-n, --count ', 'Number of likes to fetch', '20') + .option('--all', 'Fetch all likes (paged)') + .option('--max-pages ', 'Stop after N pages when using --all') + .option('--cursor ', 'Resume pagination from a cursor') + .option('--json', 'Output as JSON') + .option('--json-full', 'Output as JSON with full raw API response in _raw field') + .action(async (cmdOpts) => { + const opts = program.opts(); + const timeoutMs = ctx.resolveTimeoutFromOptions(opts); + const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts); + const count = Number.parseInt(cmdOpts.count || '20', 10); + const maxPages = cmdOpts.maxPages ? Number.parseInt(cmdOpts.maxPages, 10) : undefined; + const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); + for (const warning of warnings) { + console.error(`${ctx.p('warn')}${warning}`); + } + if (!cookies.authToken || !cookies.ct0) { + console.error(`${ctx.p('err')}Missing required credentials`); + process.exit(1); + } + const usePagination = cmdOpts.all || cmdOpts.cursor; + if (maxPages !== undefined && !usePagination) { + console.error(`${ctx.p('err')}--max-pages requires --all or --cursor.`); + process.exit(1); + } + if (!usePagination && (!Number.isFinite(count) || count <= 0)) { + console.error(`${ctx.p('err')}Invalid --count. Expected a positive integer.`); + process.exit(1); + } + if (maxPages !== undefined && (!Number.isFinite(maxPages) || maxPages <= 0)) { + console.error(`${ctx.p('err')}Invalid --max-pages. Expected a positive integer.`); + process.exit(1); + } + const client = new TwitterClient({ cookies, timeoutMs, quoteDepth }); + const includeRaw = cmdOpts.jsonFull ?? false; + const timelineOptions = { includeRaw }; + const paginationOptions = { includeRaw, maxPages, cursor: cmdOpts.cursor }; + const result = usePagination + ? await client.getAllLikes(paginationOptions) + : await client.getLikes(count, timelineOptions); + if (result.success) { + const isJson = Boolean(cmdOpts.json || cmdOpts.jsonFull); + ctx.printTweetsResult(result, { + json: isJson, + usePagination: Boolean(usePagination), + emptyMessage: 'No liked tweets found.', + }); + } + else { + console.error(`${ctx.p('err')}Failed to fetch likes: ${result.error}`); + process.exit(1); + } + }); + program + .command('whoami') + .description('Show which Twitter account the current credentials belong to') + .action(async () => { + const opts = program.opts(); + const timeoutMs = ctx.resolveTimeoutFromOptions(opts); + const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts); + const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); + for (const warning of warnings) { + console.error(`${ctx.p('warn')}${warning}`); + } + if (!cookies.authToken || !cookies.ct0) { + console.error(`${ctx.p('err')}Missing required credentials`); + process.exit(1); + } + if (cookies.source) { + console.error(`${ctx.l('source')}${cookies.source}`); + } + const client = new TwitterClient({ cookies, timeoutMs, quoteDepth }); + const result = await client.getCurrentUser(); + const credentialSource = cookies.source ?? 'env/auto-detected cookies'; + if (result.success && result.user) { + console.log(`${ctx.l('user')}@${result.user.username} (${result.user.name})`); + console.log(`${ctx.l('userId')}${result.user.id}`); + console.log(`${ctx.l('engine')}graphql`); + console.log(`${ctx.l('credentials')}${credentialSource}`); + } + else { + console.error(`${ctx.p('err')}Failed to determine current user: ${result.error ?? 'Unknown error'}`); + process.exit(1); + } + }); + program + .command('about') + .description('Get account origin and location information for a user') + .argument('', 'Twitter username (with or without @)') + .option('--json', 'Output as JSON') + .action(async (username, cmdOpts) => { + const opts = program.opts(); + const timeoutMs = ctx.resolveTimeoutFromOptions(opts); + const normalizedHandle = normalizeHandle(username); + if (!normalizedHandle) { + console.error(`${ctx.p('err')}Invalid username: ${username}`); + process.exit(1); + } + const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); + for (const warning of warnings) { + console.error(`${ctx.p('warn')}${warning}`); + } + if (!cookies.authToken || !cookies.ct0) { + console.error(`${ctx.p('err')}Missing required credentials`); + process.exit(1); + } + const client = new TwitterClient({ cookies, timeoutMs }); + const result = await client.getUserAboutAccount(normalizedHandle); + if (result.success && result.aboutProfile) { + if (cmdOpts.json) { + console.log(JSON.stringify(result.aboutProfile, null, 2)); + } + else { + for (const line of formatAboutProfile(result.aboutProfile, ctx, normalizedHandle)) { + console.log(line); + } + } + } + else { + console.error(`${ctx.p('err')}Failed to fetch account information: ${result.error ?? 'Unknown error'}`); + process.exit(1); + } + }); +} +//# sourceMappingURL=users.js.map \ No newline at end of file diff --git a/vendor/package/dist/commands/users.js.map b/vendor/package/dist/commands/users.js.map new file mode 100644 index 0000000..b8d4c3c --- /dev/null +++ b/vendor/package/dist/commands/users.js.map @@ -0,0 +1 @@ +{"version":3,"file":"users.js","sourceRoot":"","sources":["../../src/commands/users.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/index.d.ts b/vendor/package/dist/index.d.ts new file mode 100644 index 0000000..345f6ae --- /dev/null +++ b/vendor/package/dist/index.d.ts @@ -0,0 +1,2 @@ +export * from './lib/index.js'; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/index.d.ts.map b/vendor/package/dist/index.d.ts.map new file mode 100644 index 0000000..7b421cf --- /dev/null +++ b/vendor/package/dist/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC"} \ No newline at end of file diff --git a/vendor/package/dist/index.js b/vendor/package/dist/index.js new file mode 100644 index 0000000..6be7947 --- /dev/null +++ b/vendor/package/dist/index.js @@ -0,0 +1,2 @@ +export * from './lib/index.js'; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/vendor/package/dist/index.js.map b/vendor/package/dist/index.js.map new file mode 100644 index 0000000..52c226c --- /dev/null +++ b/vendor/package/dist/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC"} \ No newline at end of file diff --git a/vendor/package/dist/lib/cli-args.d.ts b/vendor/package/dist/lib/cli-args.d.ts new file mode 100644 index 0000000..cb3c2fc --- /dev/null +++ b/vendor/package/dist/lib/cli-args.d.ts @@ -0,0 +1,7 @@ +export type CliInvocation = { + argv: string[] | null; + showHelp: boolean; +}; +export declare function looksLikeTweetInput(value: string): boolean; +export declare function resolveCliInvocation(rawArgs: string[], knownCommands: Set): CliInvocation; +//# sourceMappingURL=cli-args.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/cli-args.d.ts.map b/vendor/package/dist/lib/cli-args.d.ts.map new file mode 100644 index 0000000..ed66475 --- /dev/null +++ b/vendor/package/dist/lib/cli-args.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"cli-args.d.ts","sourceRoot":"","sources":["../../src/lib/cli-args.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAKF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAM1D;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,aAAa,CAiBjG"} \ No newline at end of file diff --git a/vendor/package/dist/lib/cli-args.js b/vendor/package/dist/lib/cli-args.js new file mode 100644 index 0000000..5c2737d --- /dev/null +++ b/vendor/package/dist/lib/cli-args.js @@ -0,0 +1,25 @@ +const TWEET_URL_REGEX = /^(?:https?:\/\/)?(?:www\.)?(?:twitter\.com|x\.com)\/[^/]+\/status\/\d+/i; +const TWEET_ID_REGEX = /^\d{8,}$/; +export function looksLikeTweetInput(value) { + const trimmed = value.trim(); + if (!trimmed) { + return false; + } + return TWEET_URL_REGEX.test(trimmed) || TWEET_ID_REGEX.test(trimmed); +} +export function resolveCliInvocation(rawArgs, knownCommands) { + if (rawArgs.length === 0) { + return { argv: null, showHelp: true }; + } + const hasKnownCommand = rawArgs.some((arg) => knownCommands.has(arg)); + if (!hasKnownCommand) { + const tweetArgIndex = rawArgs.findIndex(looksLikeTweetInput); + if (tweetArgIndex >= 0) { + const rewrittenArgs = [...rawArgs]; + rewrittenArgs.splice(tweetArgIndex, 0, 'read'); + return { argv: ['node', 'bird', ...rewrittenArgs], showHelp: false }; + } + } + return { argv: null, showHelp: false }; +} +//# sourceMappingURL=cli-args.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/cli-args.js.map b/vendor/package/dist/lib/cli-args.js.map new file mode 100644 index 0000000..c6f1fb6 --- /dev/null +++ b/vendor/package/dist/lib/cli-args.js.map @@ -0,0 +1 @@ +{"version":3,"file":"cli-args.js","sourceRoot":"","sources":["../../src/lib/cli-args.ts"],"names":[],"mappings":"AAKA,MAAM,eAAe,GAAG,yEAAyE,CAAC;AAClG,MAAM,cAAc,GAAG,UAAU,CAAC;AAElC,MAAM,UAAU,mBAAmB,CAAC,KAAa;IAC/C,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACvE,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,OAAiB,EAAE,aAA0B;IAChF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACxC,CAAC;IAED,MAAM,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IAEtE,IAAI,CAAC,eAAe,EAAE,CAAC;QACrB,MAAM,aAAa,GAAG,OAAO,CAAC,SAAS,CAAC,mBAAmB,CAAC,CAAC;QAC7D,IAAI,aAAa,IAAI,CAAC,EAAE,CAAC;YACvB,MAAM,aAAa,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC;YACnC,aAAa,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC;YAC/C,OAAO,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,aAAa,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;QACvE,CAAC;IACH,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AACzC,CAAC"} \ No newline at end of file diff --git a/vendor/package/dist/lib/cookies.d.ts b/vendor/package/dist/lib/cookies.d.ts new file mode 100644 index 0000000..3f52794 --- /dev/null +++ b/vendor/package/dist/lib/cookies.d.ts @@ -0,0 +1,31 @@ +/** + * Browser cookie extraction for Twitter authentication. + * Delegates to @steipete/sweet-cookie for Safari/Chrome/Firefox reads. + */ +export interface TwitterCookies { + authToken: string | null; + ct0: string | null; + cookieHeader: string | null; + source: string | null; +} +export interface CookieExtractionResult { + cookies: TwitterCookies; + warnings: string[]; +} +export type CookieSource = 'safari' | 'chrome' | 'firefox'; +export declare function extractCookiesFromSafari(): Promise; +export declare function extractCookiesFromChrome(profile?: string): Promise; +export declare function extractCookiesFromFirefox(profile?: string): Promise; +/** + * Resolve Twitter credentials from multiple sources. + * Priority: CLI args > environment variables > browsers (ordered). + */ +export declare function resolveCredentials(options: { + authToken?: string; + ct0?: string; + cookieSource?: CookieSource | CookieSource[]; + chromeProfile?: string; + firefoxProfile?: string; + cookieTimeoutMs?: number; +}): Promise; +//# sourceMappingURL=cookies.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/cookies.d.ts.map b/vendor/package/dist/lib/cookies.d.ts.map new file mode 100644 index 0000000..35bf7c3 --- /dev/null +++ b/vendor/package/dist/lib/cookies.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../../src/lib/cookies.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,cAAc,CAAC;IACxB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;AAsI3D,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,sBAAsB,CAAC,CAEhF;AAED,wBAAsB,wBAAwB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAEhG;AAED,wBAAsB,yBAAyB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAEjG;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,YAAY,GAAG,YAAY,EAAE,CAAC;IAC7C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,GAAG,OAAO,CAAC,sBAAsB,CAAC,CA0DlC"} \ No newline at end of file diff --git a/vendor/package/dist/lib/cookies.js b/vendor/package/dist/lib/cookies.js new file mode 100644 index 0000000..83ca799 --- /dev/null +++ b/vendor/package/dist/lib/cookies.js @@ -0,0 +1,173 @@ +/** + * Browser cookie extraction for Twitter authentication. + * Delegates to @steipete/sweet-cookie for Safari/Chrome/Firefox reads. + */ +import { getCookies } from '@steipete/sweet-cookie'; +const TWITTER_COOKIE_NAMES = ['auth_token', 'ct0']; +const TWITTER_URL = 'https://x.com/'; +const TWITTER_ORIGINS = ['https://x.com/', 'https://twitter.com/']; +const DEFAULT_COOKIE_TIMEOUT_MS = 30_000; +function normalizeValue(value) { + if (typeof value !== 'string') { + return null; + } + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; +} +function cookieHeader(authToken, ct0) { + return `auth_token=${authToken}; ct0=${ct0}`; +} +function buildEmpty() { + return { authToken: null, ct0: null, cookieHeader: null, source: null }; +} +function readEnvCookie(cookies, keys, field) { + if (cookies[field]) { + return; + } + for (const key of keys) { + const value = normalizeValue(process.env[key]); + if (!value) { + continue; + } + cookies[field] = value; + if (!cookies.source) { + cookies.source = `env ${key}`; + } + break; + } +} +function resolveSources(cookieSource) { + if (Array.isArray(cookieSource)) { + return cookieSource; + } + if (cookieSource) { + return [cookieSource]; + } + return ['safari', 'chrome', 'firefox']; +} +function labelForSource(source, profile) { + if (source === 'safari') { + return 'Safari'; + } + if (source === 'chrome') { + return profile ? `Chrome profile "${profile}"` : 'Chrome default profile'; + } + return profile ? `Firefox profile "${profile}"` : 'Firefox default profile'; +} +function pickCookieValue(cookies, name) { + const matches = cookies.filter((c) => c?.name === name && typeof c.value === 'string'); + if (matches.length === 0) { + return null; + } + const preferred = matches.find((c) => (c.domain ?? '').endsWith('x.com')); + if (preferred?.value) { + return preferred.value; + } + const twitter = matches.find((c) => (c.domain ?? '').endsWith('twitter.com')); + if (twitter?.value) { + return twitter.value; + } + return matches[0]?.value ?? null; +} +async function readTwitterCookiesFromBrowser(options) { + const warnings = []; + const out = buildEmpty(); + const { cookies, warnings: providerWarnings } = await getCookies({ + url: TWITTER_URL, + origins: TWITTER_ORIGINS, + names: [...TWITTER_COOKIE_NAMES], + browsers: [options.source], + mode: 'merge', + chromeProfile: options.chromeProfile, + firefoxProfile: options.firefoxProfile, + timeoutMs: options.cookieTimeoutMs, + }); + warnings.push(...providerWarnings); + const authToken = pickCookieValue(cookies, 'auth_token'); + const ct0 = pickCookieValue(cookies, 'ct0'); + if (authToken) { + out.authToken = authToken; + } + if (ct0) { + out.ct0 = ct0; + } + if (out.authToken && out.ct0) { + out.cookieHeader = cookieHeader(out.authToken, out.ct0); + out.source = labelForSource(options.source, options.source === 'chrome' ? options.chromeProfile : options.firefoxProfile); + return { cookies: out, warnings }; + } + if (options.source === 'safari') { + warnings.push('No Twitter cookies found in Safari. Make sure you are logged into x.com in Safari.'); + } + else if (options.source === 'chrome') { + warnings.push('No Twitter cookies found in Chrome. Make sure you are logged into x.com in Chrome.'); + } + else { + warnings.push('No Twitter cookies found in Firefox. Make sure you are logged into x.com in Firefox and the profile exists.'); + } + return { cookies: out, warnings }; +} +export async function extractCookiesFromSafari() { + return readTwitterCookiesFromBrowser({ source: 'safari' }); +} +export async function extractCookiesFromChrome(profile) { + return readTwitterCookiesFromBrowser({ source: 'chrome', chromeProfile: profile }); +} +export async function extractCookiesFromFirefox(profile) { + return readTwitterCookiesFromBrowser({ source: 'firefox', firefoxProfile: profile }); +} +/** + * Resolve Twitter credentials from multiple sources. + * Priority: CLI args > environment variables > browsers (ordered). + */ +export async function resolveCredentials(options) { + const warnings = []; + const cookies = buildEmpty(); + const cookieTimeoutMs = typeof options.cookieTimeoutMs === 'number' && + Number.isFinite(options.cookieTimeoutMs) && + options.cookieTimeoutMs > 0 + ? options.cookieTimeoutMs + : process.platform === 'darwin' + ? DEFAULT_COOKIE_TIMEOUT_MS + : undefined; + if (options.authToken) { + cookies.authToken = options.authToken; + cookies.source = 'CLI argument'; + } + if (options.ct0) { + cookies.ct0 = options.ct0; + if (!cookies.source) { + cookies.source = 'CLI argument'; + } + } + readEnvCookie(cookies, ['AUTH_TOKEN', 'TWITTER_AUTH_TOKEN'], 'authToken'); + readEnvCookie(cookies, ['CT0', 'TWITTER_CT0'], 'ct0'); + if (cookies.authToken && cookies.ct0) { + cookies.cookieHeader = cookieHeader(cookies.authToken, cookies.ct0); + return { cookies, warnings }; + } + const sourcesToTry = resolveSources(options.cookieSource); + for (const source of sourcesToTry) { + const res = await readTwitterCookiesFromBrowser({ + source, + chromeProfile: options.chromeProfile, + firefoxProfile: options.firefoxProfile, + cookieTimeoutMs, + }); + warnings.push(...res.warnings); + if (res.cookies.authToken && res.cookies.ct0) { + return { cookies: res.cookies, warnings }; + } + } + if (!cookies.authToken) { + warnings.push('Missing auth_token - provide via --auth-token, AUTH_TOKEN env var, or login to x.com in Safari/Chrome/Firefox'); + } + if (!cookies.ct0) { + warnings.push('Missing ct0 - provide via --ct0, CT0 env var, or login to x.com in Safari/Chrome/Firefox'); + } + if (cookies.authToken && cookies.ct0) { + cookies.cookieHeader = cookieHeader(cookies.authToken, cookies.ct0); + } + return { cookies, warnings }; +} +//# sourceMappingURL=cookies.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/cookies.js.map b/vendor/package/dist/lib/cookies.js.map new file mode 100644 index 0000000..b2ac953 --- /dev/null +++ b/vendor/package/dist/lib/cookies.js.map @@ -0,0 +1 @@ +{"version":3,"file":"cookies.js","sourceRoot":"","sources":["../../src/lib/cookies.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/lib/extract-bookmark-folder-id.d.ts b/vendor/package/dist/lib/extract-bookmark-folder-id.d.ts new file mode 100644 index 0000000..af97eb5 --- /dev/null +++ b/vendor/package/dist/lib/extract-bookmark-folder-id.d.ts @@ -0,0 +1,2 @@ +export declare function extractBookmarkFolderId(input: string): string | null; +//# sourceMappingURL=extract-bookmark-folder-id.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/extract-bookmark-folder-id.d.ts.map b/vendor/package/dist/lib/extract-bookmark-folder-id.d.ts.map new file mode 100644 index 0000000..b492723 --- /dev/null +++ b/vendor/package/dist/lib/extract-bookmark-folder-id.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"extract-bookmark-folder-id.d.ts","sourceRoot":"","sources":["../../src/lib/extract-bookmark-folder-id.ts"],"names":[],"mappings":"AAMA,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAapE"} \ No newline at end of file diff --git a/vendor/package/dist/lib/extract-bookmark-folder-id.js b/vendor/package/dist/lib/extract-bookmark-folder-id.js new file mode 100644 index 0000000..af68602 --- /dev/null +++ b/vendor/package/dist/lib/extract-bookmark-folder-id.js @@ -0,0 +1,20 @@ +/** + * Extract bookmark folder (collection) ID from an X/Twitter bookmarks URL. + */ +const BOOKMARK_FOLDER_URL_REGEX = /(?:twitter\.com|x\.com)\/i\/bookmarks\/(\d+)/i; +const BOOKMARK_FOLDER_ID_REGEX = /^\d{5,}$/; +export function extractBookmarkFolderId(input) { + const trimmed = input.trim(); + if (!trimmed) { + return null; + } + const urlMatch = BOOKMARK_FOLDER_URL_REGEX.exec(trimmed); + if (urlMatch) { + return urlMatch[1]; + } + if (BOOKMARK_FOLDER_ID_REGEX.test(trimmed)) { + return trimmed; + } + return null; +} +//# sourceMappingURL=extract-bookmark-folder-id.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/extract-bookmark-folder-id.js.map b/vendor/package/dist/lib/extract-bookmark-folder-id.js.map new file mode 100644 index 0000000..ed681e0 --- /dev/null +++ b/vendor/package/dist/lib/extract-bookmark-folder-id.js.map @@ -0,0 +1 @@ +{"version":3,"file":"extract-bookmark-folder-id.js","sourceRoot":"","sources":["../../src/lib/extract-bookmark-folder-id.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,yBAAyB,GAAG,+CAA+C,CAAC;AAClF,MAAM,wBAAwB,GAAG,UAAU,CAAC;AAE5C,MAAM,UAAU,uBAAuB,CAAC,KAAa;IACnD,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,QAAQ,GAAG,yBAAyB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACzD,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IACD,IAAI,wBAAwB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3C,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"} \ No newline at end of file diff --git a/vendor/package/dist/lib/extract-list-id.d.ts b/vendor/package/dist/lib/extract-list-id.d.ts new file mode 100644 index 0000000..42427bc --- /dev/null +++ b/vendor/package/dist/lib/extract-list-id.d.ts @@ -0,0 +1,2 @@ +export declare function extractListId(input: string): string | null; +//# sourceMappingURL=extract-list-id.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/extract-list-id.d.ts.map b/vendor/package/dist/lib/extract-list-id.d.ts.map new file mode 100644 index 0000000..e70d7c7 --- /dev/null +++ b/vendor/package/dist/lib/extract-list-id.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"extract-list-id.d.ts","sourceRoot":"","sources":["../../src/lib/extract-list-id.ts"],"names":[],"mappings":"AAMA,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAa1D"} \ No newline at end of file diff --git a/vendor/package/dist/lib/extract-list-id.js b/vendor/package/dist/lib/extract-list-id.js new file mode 100644 index 0000000..450ffb0 --- /dev/null +++ b/vendor/package/dist/lib/extract-list-id.js @@ -0,0 +1,19 @@ +// ABOUTME: Extract list ID from an X/Twitter list URL. +// ABOUTME: Returns list ID or null if input is invalid. +const LIST_URL_REGEX = /(?:twitter\.com|x\.com)\/i\/lists\/(\d+)/i; +const LIST_ID_REGEX = /^\d{5,}$/; +export function extractListId(input) { + const trimmed = input.trim(); + if (!trimmed) { + return null; + } + const urlMatch = LIST_URL_REGEX.exec(trimmed); + if (urlMatch) { + return urlMatch[1]; + } + if (LIST_ID_REGEX.test(trimmed)) { + return trimmed; + } + return null; +} +//# sourceMappingURL=extract-list-id.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/extract-list-id.js.map b/vendor/package/dist/lib/extract-list-id.js.map new file mode 100644 index 0000000..327f5d4 --- /dev/null +++ b/vendor/package/dist/lib/extract-list-id.js.map @@ -0,0 +1 @@ +{"version":3,"file":"extract-list-id.js","sourceRoot":"","sources":["../../src/lib/extract-list-id.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,wDAAwD;AAExD,MAAM,cAAc,GAAG,2CAA2C,CAAC;AACnE,MAAM,aAAa,GAAG,UAAU,CAAC;AAEjC,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,QAAQ,GAAG,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC9C,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IACD,IAAI,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAChC,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"} \ No newline at end of file diff --git a/vendor/package/dist/lib/extract-tweet-id.d.ts b/vendor/package/dist/lib/extract-tweet-id.d.ts new file mode 100644 index 0000000..f86631d --- /dev/null +++ b/vendor/package/dist/lib/extract-tweet-id.d.ts @@ -0,0 +1,2 @@ +export declare function extractTweetId(input: string): string; +//# sourceMappingURL=extract-tweet-id.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/extract-tweet-id.d.ts.map b/vendor/package/dist/lib/extract-tweet-id.d.ts.map new file mode 100644 index 0000000..b17ca49 --- /dev/null +++ b/vendor/package/dist/lib/extract-tweet-id.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"extract-tweet-id.d.ts","sourceRoot":"","sources":["../../src/lib/extract-tweet-id.ts"],"names":[],"mappings":"AAKA,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQpD"} \ No newline at end of file diff --git a/vendor/package/dist/lib/extract-tweet-id.js b/vendor/package/dist/lib/extract-tweet-id.js new file mode 100644 index 0000000..b27fbb1 --- /dev/null +++ b/vendor/package/dist/lib/extract-tweet-id.js @@ -0,0 +1,14 @@ +/** + * Extract tweet ID from a Twitter/X URL or return the input unchanged if it's already an ID. + */ +const TWEET_URL_REGEX = /(?:twitter\.com|x\.com)\/(?:\w+\/status|i\/web\/status)\/(\d+)/i; +export function extractTweetId(input) { + // If it's a URL, extract the tweet ID + const urlMatch = TWEET_URL_REGEX.exec(input); + if (urlMatch) { + return urlMatch[1]; + } + // Assume it's already an ID + return input; +} +//# sourceMappingURL=extract-tweet-id.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/extract-tweet-id.js.map b/vendor/package/dist/lib/extract-tweet-id.js.map new file mode 100644 index 0000000..3e2eb7d --- /dev/null +++ b/vendor/package/dist/lib/extract-tweet-id.js.map @@ -0,0 +1 @@ +{"version":3,"file":"extract-tweet-id.js","sourceRoot":"","sources":["../../src/lib/extract-tweet-id.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,eAAe,GAAG,iEAAiE,CAAC;AAE1F,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,sCAAsC;IACtC,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7C,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IACD,4BAA4B;IAC5B,OAAO,KAAK,CAAC;AACf,CAAC"} \ No newline at end of file diff --git a/vendor/package/dist/lib/features.json b/vendor/package/dist/lib/features.json new file mode 100644 index 0000000..c43e03c --- /dev/null +++ b/vendor/package/dist/lib/features.json @@ -0,0 +1,17 @@ +{ + "global": { + "responsive_web_grok_annotations_enabled": false, + "post_ctas_fetch_enabled": true, + "responsive_web_graphql_exclude_directive_enabled": true + }, + "sets": { + "lists": { + "blue_business_profile_image_shape_enabled": true, + "tweetypie_unmention_optimization_enabled": true, + "responsive_web_text_conversations_enabled": false, + "interactive_text_enabled": true, + "vibe_api_enabled": true, + "responsive_web_twitter_blue_verified_badge_is_enabled": true + } + } +} diff --git a/vendor/package/dist/lib/index.d.ts b/vendor/package/dist/lib/index.d.ts new file mode 100644 index 0000000..14a511f --- /dev/null +++ b/vendor/package/dist/lib/index.d.ts @@ -0,0 +1,10 @@ +export { type CookieExtractionResult, type CookieSource, extractCookiesFromChrome, extractCookiesFromFirefox, extractCookiesFromSafari, resolveCredentials, type TwitterCookies, } from './cookies.js'; +export { runtimeQueryIds } from './runtime-query-ids.js'; +export { type CurrentUserResult, type FollowingResult, type GetTweetResult, type SearchResult, type TweetData, TwitterClient, type TwitterClientOptions, type TwitterUser, } from './twitter-client.js'; +export type { HomeTimelineFetchOptions } from './twitter-client-home.js'; +export type { ExploreTab, NewsFetchOptions, NewsItem, NewsResult } from './twitter-client-news.js'; +export type { SearchFetchOptions } from './twitter-client-search.js'; +export type { TimelineFetchOptions } from './twitter-client-timelines.js'; +export type { TweetFetchOptions } from './twitter-client-tweet-detail.js'; +export type { AboutAccountProfile, AboutAccountResult, TweetResult, UploadMediaResult, } from './twitter-client-types.js'; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/index.d.ts.map b/vendor/package/dist/lib/index.d.ts.map new file mode 100644 index 0000000..466c5c7 --- /dev/null +++ b/vendor/package/dist/lib/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,YAAY,EACjB,wBAAwB,EACxB,yBAAyB,EACzB,wBAAwB,EACxB,kBAAkB,EAClB,KAAK,cAAc,GACpB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,aAAa,EACb,KAAK,oBAAoB,EACzB,KAAK,WAAW,GACjB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACzE,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACnG,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AACrE,YAAY,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAC1E,YAAY,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,YAAY,EACV,mBAAmB,EACnB,kBAAkB,EAClB,WAAW,EACX,iBAAiB,GAClB,MAAM,2BAA2B,CAAC"} \ No newline at end of file diff --git a/vendor/package/dist/lib/index.js b/vendor/package/dist/lib/index.js new file mode 100644 index 0000000..e3b0eca --- /dev/null +++ b/vendor/package/dist/lib/index.js @@ -0,0 +1,4 @@ +export { extractCookiesFromChrome, extractCookiesFromFirefox, extractCookiesFromSafari, resolveCredentials, } from './cookies.js'; +export { runtimeQueryIds } from './runtime-query-ids.js'; +export { TwitterClient, } from './twitter-client.js'; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/index.js.map b/vendor/package/dist/lib/index.js.map new file mode 100644 index 0000000..1f1ce56 --- /dev/null +++ b/vendor/package/dist/lib/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,wBAAwB,EACxB,yBAAyB,EACzB,wBAAwB,EACxB,kBAAkB,GAEnB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAML,aAAa,GAGd,MAAM,qBAAqB,CAAC"} \ No newline at end of file diff --git a/vendor/package/dist/lib/normalize-handle.d.ts b/vendor/package/dist/lib/normalize-handle.d.ts new file mode 100644 index 0000000..9bab3be --- /dev/null +++ b/vendor/package/dist/lib/normalize-handle.d.ts @@ -0,0 +1,6 @@ +export declare function normalizeHandle(input?: string | null): string | null; +export declare function mentionsQueryFromUserOption(userOption?: string | null): { + query: string | null; + error: string | null; +}; +//# sourceMappingURL=normalize-handle.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/normalize-handle.d.ts.map b/vendor/package/dist/lib/normalize-handle.d.ts.map new file mode 100644 index 0000000..b3f7cda --- /dev/null +++ b/vendor/package/dist/lib/normalize-handle.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"normalize-handle.d.ts","sourceRoot":"","sources":["../../src/lib/normalize-handle.ts"],"names":[],"mappings":"AAEA,wBAAgB,eAAe,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAkBpE;AAED,wBAAgB,2BAA2B,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG;IACvE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAcA"} \ No newline at end of file diff --git a/vendor/package/dist/lib/normalize-handle.js b/vendor/package/dist/lib/normalize-handle.js new file mode 100644 index 0000000..c75011a --- /dev/null +++ b/vendor/package/dist/lib/normalize-handle.js @@ -0,0 +1,31 @@ +const HANDLE_REGEX = /^[A-Za-z0-9_]{1,15}$/; +export function normalizeHandle(input) { + const raw = (input ?? '').trim(); + if (!raw) { + return null; + } + const withoutAt = raw.startsWith('@') ? raw.slice(1) : raw; + const handle = withoutAt.trim(); + if (!handle) { + return null; + } + // X/Twitter handles are traditionally max 15 chars; keep strict to avoid surprising queries. + if (!HANDLE_REGEX.test(handle)) { + return null; + } + return handle; +} +export function mentionsQueryFromUserOption(userOption) { + if (typeof userOption === 'undefined') { + return { query: null, error: null }; + } + const handle = normalizeHandle(userOption); + if (!handle) { + return { + query: null, + error: 'Invalid --user handle. Expected something like @steipete (letters, digits, underscore; max 15).', + }; + } + return { query: `@${handle}`, error: null }; +} +//# sourceMappingURL=normalize-handle.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/normalize-handle.js.map b/vendor/package/dist/lib/normalize-handle.js.map new file mode 100644 index 0000000..bf3c747 --- /dev/null +++ b/vendor/package/dist/lib/normalize-handle.js.map @@ -0,0 +1 @@ +{"version":3,"file":"normalize-handle.js","sourceRoot":"","sources":["../../src/lib/normalize-handle.ts"],"names":[],"mappings":"AAAA,MAAM,YAAY,GAAG,sBAAsB,CAAC;AAE5C,MAAM,UAAU,eAAe,CAAC,KAAqB;IACnD,MAAM,GAAG,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACjC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,SAAS,GAAG,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IAC3D,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC;IAChC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,IAAI,CAAC;IACd,CAAC;IAED,6FAA6F;IAC7F,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAC/B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,2BAA2B,CAAC,UAA0B;IAIpE,IAAI,OAAO,UAAU,KAAK,WAAW,EAAE,CAAC;QACtC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IACtC,CAAC;IAED,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;IAC3C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO;YACL,KAAK,EAAE,IAAI;YACX,KAAK,EAAE,iGAAiG;SACzG,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,IAAI,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AAC9C,CAAC"} \ No newline at end of file diff --git a/vendor/package/dist/lib/output.d.ts b/vendor/package/dist/lib/output.d.ts new file mode 100644 index 0000000..e29cc87 --- /dev/null +++ b/vendor/package/dist/lib/output.d.ts @@ -0,0 +1,29 @@ +export type OutputConfig = { + plain: boolean; + emoji: boolean; + color: boolean; + hyperlinks: boolean; +}; +export type StatusKind = 'ok' | 'warn' | 'err' | 'info' | 'hint'; +export type LabelKind = 'url' | 'date' | 'source' | 'engine' | 'credentials' | 'user' | 'userId' | 'email'; +export declare function resolveOutputConfigFromArgv(argv: string[], env: NodeJS.ProcessEnv, isTty: boolean): OutputConfig; +export declare function resolveOutputConfigFromCommander(opts: { + plain?: boolean; + emoji?: boolean; + color?: boolean; +}, env: NodeJS.ProcessEnv, isTty: boolean): OutputConfig; +export declare function statusPrefix(kind: StatusKind, cfg: OutputConfig): string; +export declare function labelPrefix(kind: LabelKind, cfg: OutputConfig): string; +export declare function formatStatsLine(stats: { + likeCount?: number | null; + retweetCount?: number | null; + replyCount?: number | null; +}, cfg: OutputConfig): string; +export declare function formatTweetUrl(tweetId: string): string; +/** + * Wraps a URL in OSC 8 escape sequences to make it clickable in supported terminals. + * Falls back to plain text when not in a TTY or when hyperlinks are disabled. + */ +export declare function hyperlink(url: string, text?: string, cfg?: OutputConfig): string; +export declare function formatTweetUrlLine(tweetId: string, cfg: OutputConfig): string; +//# sourceMappingURL=output.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/output.d.ts.map b/vendor/package/dist/lib/output.d.ts.map new file mode 100644 index 0000000..88929cf --- /dev/null +++ b/vendor/package/dist/lib/output.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../../src/lib/output.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;AACjE,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,aAAa,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AAqB3G,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,EAAE,KAAK,EAAE,OAAO,GAAG,YAAY,CAUhH;AAED,wBAAgB,gCAAgC,CAC9C,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,EAC3D,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,KAAK,EAAE,OAAO,GACb,YAAY,CAUd;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,YAAY,GAAG,MAAM,CAQxE;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,YAAY,GAAG,MAAM,CAQtE;AAED,wBAAgB,eAAe,CAC7B,KAAK,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EAC9F,GAAG,EAAE,YAAY,GAChB,MAAM,CAYR;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,YAAY,GAAG,MAAM,CAUhF;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,MAAM,CAG7E"} \ No newline at end of file diff --git a/vendor/package/dist/lib/output.js b/vendor/package/dist/lib/output.js new file mode 100644 index 0000000..c51e1f3 --- /dev/null +++ b/vendor/package/dist/lib/output.js @@ -0,0 +1,88 @@ +const STATUS = { + ok: { emoji: '✅', text: 'OK:', plain: '[ok]' }, + warn: { emoji: '⚠️', text: 'Warning:', plain: '[warn]' }, + err: { emoji: '❌', text: 'Error:', plain: '[err]' }, + info: { emoji: 'ℹ️', text: 'Info:', plain: '[info]' }, + hint: { emoji: 'ℹ️', text: 'Hint:', plain: '[hint]' }, +}; +const LABELS = { + url: { emoji: '🔗', text: 'URL:', plain: 'url:' }, + date: { emoji: '📅', text: 'Date:', plain: 'date:' }, + source: { emoji: '📍', text: 'Source:', plain: 'source:' }, + engine: { emoji: '⚙️', text: 'Engine:', plain: 'engine:' }, + credentials: { emoji: '🔑', text: 'Credentials:', plain: 'credentials:' }, + user: { emoji: '🙋', text: 'User:', plain: 'user:' }, + userId: { emoji: '🪪', text: 'User ID:', plain: 'user_id:' }, + email: { emoji: '📧', text: 'Email:', plain: 'email:' }, +}; +export function resolveOutputConfigFromArgv(argv, env, isTty) { + const hasNoColorEnv = Object.hasOwn(env, 'NO_COLOR') || env.TERM === 'dumb'; + const defaultColor = isTty && !hasNoColorEnv; + const plain = argv.includes('--plain'); + const emoji = !plain && !argv.includes('--no-emoji'); + const color = !plain && !argv.includes('--no-color') && defaultColor; + const hyperlinks = !plain && isTty; + return { plain, emoji, color, hyperlinks }; +} +export function resolveOutputConfigFromCommander(opts, env, isTty) { + const hasNoColorEnv = Object.hasOwn(env, 'NO_COLOR') || env.TERM === 'dumb'; + const defaultColor = isTty && !hasNoColorEnv; + const plain = Boolean(opts.plain); + const emoji = !plain && (opts.emoji ?? true); + const color = !plain && (opts.color ?? true) && defaultColor; + const hyperlinks = !plain && isTty; + return { plain, emoji, color, hyperlinks }; +} +export function statusPrefix(kind, cfg) { + if (cfg.plain) { + return `${STATUS[kind].plain} `; + } + if (cfg.emoji) { + return `${STATUS[kind].emoji} `; + } + return `${STATUS[kind].text} `; +} +export function labelPrefix(kind, cfg) { + if (cfg.plain) { + return `${LABELS[kind].plain} `; + } + if (cfg.emoji) { + return `${LABELS[kind].emoji} `; + } + return `${LABELS[kind].text} `; +} +export function formatStatsLine(stats, cfg) { + const likeCount = stats.likeCount ?? 0; + const retweetCount = stats.retweetCount ?? 0; + const replyCount = stats.replyCount ?? 0; + if (cfg.plain) { + return `likes: ${likeCount} retweets: ${retweetCount} replies: ${replyCount}`; + } + if (!cfg.emoji) { + return `Likes ${likeCount} Retweets ${retweetCount} Replies ${replyCount}`; + } + return `❤️ ${likeCount} 🔁 ${retweetCount} 💬 ${replyCount}`; +} +export function formatTweetUrl(tweetId) { + return `https://x.com/i/status/${tweetId}`; +} +/** + * Wraps a URL in OSC 8 escape sequences to make it clickable in supported terminals. + * Falls back to plain text when not in a TTY or when hyperlinks are disabled. + */ +export function hyperlink(url, text, cfg) { + const displayText = text ?? url; + // Only use hyperlinks when explicitly enabled (requires TTY and not plain mode) + if (!cfg?.hyperlinks) { + return displayText; + } + const safeUrl = url.replaceAll('\x1b', '').replaceAll('\x07', ''); + const safeText = displayText.replaceAll('\x1b', '').replaceAll('\x07', ''); + // OSC 8 hyperlink: \x1b]8;;URL\x07TEXT\x1b]8;;\x07 + return `\x1b]8;;${safeUrl}\x07${safeText}\x1b]8;;\x07`; +} +export function formatTweetUrlLine(tweetId, cfg) { + const url = formatTweetUrl(tweetId); + return `${labelPrefix('url', cfg)}${hyperlink(url, url, cfg)}`; +} +//# sourceMappingURL=output.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/output.js.map b/vendor/package/dist/lib/output.js.map new file mode 100644 index 0000000..24363ea --- /dev/null +++ b/vendor/package/dist/lib/output.js.map @@ -0,0 +1 @@ +{"version":3,"file":"output.js","sourceRoot":"","sources":["../../src/lib/output.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/lib/paginate-cursor.d.ts b/vendor/package/dist/lib/paginate-cursor.d.ts new file mode 100644 index 0000000..5ce6e84 --- /dev/null +++ b/vendor/package/dist/lib/paginate-cursor.d.ts @@ -0,0 +1,27 @@ +export type CursorPage = { + success: true; + items: T[]; + cursor?: string; +} | { + success: false; + error: string; +}; +export type CursorPaginationResult = { + success: true; + items: T[]; + nextCursor?: string; +} | { + success: false; + error: string; + items?: T[]; + nextCursor?: string; +}; +export declare function paginateCursor(opts: { + cursor?: string; + maxPages?: number; + pageDelayMs?: number; + sleep: (ms: number) => Promise; + getKey: (item: T) => string; + fetchPage: (cursor?: string) => Promise>; +}): Promise>; +//# sourceMappingURL=paginate-cursor.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/paginate-cursor.d.ts.map b/vendor/package/dist/lib/paginate-cursor.d.ts.map new file mode 100644 index 0000000..b5d4cf1 --- /dev/null +++ b/vendor/package/dist/lib/paginate-cursor.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"paginate-cursor.d.ts","sourceRoot":"","sources":["../../src/lib/paginate-cursor.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,CAAC,CAAC,IACpB;IACE,OAAO,EAAE,IAAI,CAAC;IACd,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GACD;IACE,OAAO,EAAE,KAAK,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEN,MAAM,MAAM,sBAAsB,CAAC,CAAC,IAChC;IACE,OAAO,EAAE,IAAI,CAAC;IACd,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GACD;IACE,OAAO,EAAE,KAAK,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEN,wBAAsB,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC;IAC5B,SAAS,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;CACxD,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAyCrC"} \ No newline at end of file diff --git a/vendor/package/dist/lib/paginate-cursor.js b/vendor/package/dist/lib/paginate-cursor.js new file mode 100644 index 0000000..6c061fe --- /dev/null +++ b/vendor/package/dist/lib/paginate-cursor.js @@ -0,0 +1,37 @@ +export async function paginateCursor(opts) { + const { maxPages, pageDelayMs = 1000 } = opts; + const seen = new Set(); + const items = []; + let cursor = opts.cursor; + let pagesFetched = 0; + while (true) { + if (pagesFetched > 0 && pageDelayMs > 0) { + await opts.sleep(pageDelayMs); + } + const page = await opts.fetchPage(cursor); + if (!page.success) { + if (items.length > 0) { + return { success: false, error: page.error, items, nextCursor: cursor }; + } + return page; + } + pagesFetched += 1; + for (const item of page.items) { + const key = opts.getKey(item); + if (seen.has(key)) { + continue; + } + seen.add(key); + items.push(item); + } + const pageCursor = page.cursor; + if (!pageCursor || pageCursor === cursor) { + return { success: true, items, nextCursor: undefined }; + } + if (maxPages !== undefined && pagesFetched >= maxPages) { + return { success: true, items, nextCursor: pageCursor }; + } + cursor = pageCursor; + } +} +//# sourceMappingURL=paginate-cursor.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/paginate-cursor.js.map b/vendor/package/dist/lib/paginate-cursor.js.map new file mode 100644 index 0000000..6a5c1c8 --- /dev/null +++ b/vendor/package/dist/lib/paginate-cursor.js.map @@ -0,0 +1 @@ +{"version":3,"file":"paginate-cursor.js","sourceRoot":"","sources":["../../src/lib/paginate-cursor.ts"],"names":[],"mappings":"AAwBA,MAAM,CAAC,KAAK,UAAU,cAAc,CAAI,IAOvC;IACC,MAAM,EAAE,QAAQ,EAAE,WAAW,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IAC9C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,KAAK,GAAQ,EAAE,CAAC;IACtB,IAAI,MAAM,GAAuB,IAAI,CAAC,MAAM,CAAC;IAC7C,IAAI,YAAY,GAAG,CAAC,CAAC;IAErB,OAAO,IAAI,EAAE,CAAC;QACZ,IAAI,YAAY,GAAG,CAAC,IAAI,WAAW,GAAG,CAAC,EAAE,CAAC;YACxC,MAAM,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QAChC,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAC1C,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YAClB,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACrB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC;YAC1E,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;QACD,YAAY,IAAI,CAAC,CAAC;QAElB,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAC9B,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAC9B,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;gBAClB,SAAS;YACX,CAAC;YACD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACd,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;QAED,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC;QAC/B,IAAI,CAAC,UAAU,IAAI,UAAU,KAAK,MAAM,EAAE,CAAC;YACzC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC;QACzD,CAAC;QAED,IAAI,QAAQ,KAAK,SAAS,IAAI,YAAY,IAAI,QAAQ,EAAE,CAAC;YACvD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC;QAC1D,CAAC;QAED,MAAM,GAAG,UAAU,CAAC;IACtB,CAAC;AACH,CAAC"} \ No newline at end of file diff --git a/vendor/package/dist/lib/query-ids.json b/vendor/package/dist/lib/query-ids.json new file mode 100644 index 0000000..6033f31 --- /dev/null +++ b/vendor/package/dist/lib/query-ids.json @@ -0,0 +1,20 @@ +{ + "CreateTweet": "nmdAQXJDxw6-0KKF2on7eA", + "CreateRetweet": "LFho5rIi4xcKO90p9jwG7A", + "CreateFriendship": "8h9JVdV8dlSyqyRDJEPCsA", + "DestroyFriendship": "ppXWuagMNXgvzx6WoXBW0Q", + "FavoriteTweet": "lI07N6Otwv1PhnEgXILM7A", + "DeleteBookmark": "Wlmlj2-xzyS1GN3a6cj-mQ", + "TweetDetail": "_NvJCnIjOW__EP5-RF197A", + "SearchTimeline": "6AAys3t42mosm_yTI_QENg", + "Bookmarks": "RV1g3b8n_SGOHwkqKYSCFw", + "BookmarkFolderTimeline": "KJIQpsvxrTfRIlbaRIySHQ", + "Following": "mWYeougg_ocJS2Vr1Vt28w", + "Followers": "SFYY3WsgwjlXSLlfnEUE4A", + "Likes": "ETJflBunfqNa1uE1mBPCaw", + "ExploreSidebar": "lpSN4M6qpimkF4nRFPE3nQ", + "ExplorePage": "kheAINB_4pzRDqkzG3K-ng", + "GenericTimelineById": "uGSr7alSjR9v6QJAIaqSKQ", + "TrendHistory": "Sj4T-jSB9pr0Mxtsc1UKZQ", + "AboutAccountQuery": "zs_jFPFT78rBpXv9Z3U2YQ" +} diff --git a/vendor/package/dist/lib/runtime-features.d.ts b/vendor/package/dist/lib/runtime-features.d.ts new file mode 100644 index 0000000..3fa1ab3 --- /dev/null +++ b/vendor/package/dist/lib/runtime-features.d.ts @@ -0,0 +1,19 @@ +export type FeatureOverrides = { + global?: Record; + sets?: Record>; +}; +type NormalizedFeatureOverrides = { + global: Record; + sets: Record>; +}; +export type FeatureOverridesSnapshot = { + cachePath: string; + overrides: FeatureOverrides; +}; +export declare function loadFeatureOverrides(): NormalizedFeatureOverrides; +export declare function getFeatureOverridesSnapshot(): FeatureOverridesSnapshot; +export declare function applyFeatureOverrides(setName: string, base: Record): Record; +export declare function refreshFeatureOverridesCache(): Promise; +export declare function clearFeatureOverridesCache(): void; +export {}; +//# sourceMappingURL=runtime-features.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/runtime-features.d.ts.map b/vendor/package/dist/lib/runtime-features.d.ts.map new file mode 100644 index 0000000..f7e0d14 --- /dev/null +++ b/vendor/package/dist/lib/runtime-features.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"runtime-features.d.ts","sourceRoot":"","sources":["../../src/lib/runtime-features.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAChD,CAAC;AAEF,KAAK,0BAA0B,GAAG;IAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC/C,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,gBAAgB,CAAC;CAC7B,CAAC;AAwGF,wBAAgB,oBAAoB,IAAI,0BAA0B,CAgBjE;AAED,wBAAgB,2BAA2B,IAAI,wBAAwB,CAMtE;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAmB7G;AAED,wBAAsB,4BAA4B,IAAI,OAAO,CAAC,wBAAwB,CAAC,CAQtF;AAED,wBAAgB,0BAA0B,IAAI,IAAI,CAEjD"} \ No newline at end of file diff --git a/vendor/package/dist/lib/runtime-features.js b/vendor/package/dist/lib/runtime-features.js new file mode 100644 index 0000000..6910e9d --- /dev/null +++ b/vendor/package/dist/lib/runtime-features.js @@ -0,0 +1,151 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { mkdir, writeFile } from 'node:fs/promises'; +import { homedir } from 'node:os'; +import path from 'node:path'; +// biome-ignore lint/correctness/useImportExtensions: JSON module import doesn't use .js extension. +import defaultOverrides from './features.json' with { type: 'json' }; +const DEFAULT_CACHE_FILENAME = 'features.json'; +let cachedOverrides = null; +function normalizeFeatureMap(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return {}; + } + const result = {}; + for (const [key, entry] of Object.entries(value)) { + if (typeof entry === 'boolean') { + result[key] = entry; + } + } + return result; +} +function normalizeOverrides(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return { global: {}, sets: {} }; + } + const record = value; + const global = normalizeFeatureMap(record.global); + const sets = {}; + const rawSets = record.sets && typeof record.sets === 'object' && !Array.isArray(record.sets) + ? record.sets + : {}; + for (const [setName, setValue] of Object.entries(rawSets)) { + const normalized = normalizeFeatureMap(setValue); + if (Object.keys(normalized).length > 0) { + sets[setName] = normalized; + } + } + return { global, sets }; +} +function mergeOverrides(base, next) { + const sets = { ...base.sets }; + for (const [setName, overrides] of Object.entries(next.sets)) { + const existing = sets[setName]; + sets[setName] = existing ? { ...existing, ...overrides } : { ...overrides }; + } + return { + global: { ...base.global, ...next.global }, + sets, + }; +} +function toFeatureOverrides(overrides) { + const result = {}; + if (Object.keys(overrides.global).length > 0) { + result.global = overrides.global; + } + const setEntries = Object.entries(overrides.sets).filter(([, value]) => Object.keys(value).length > 0); + if (setEntries.length > 0) { + result.sets = Object.fromEntries(setEntries); + } + return result; +} +function resolveFeaturesCachePath() { + const override = process.env.BIRD_FEATURES_CACHE ?? process.env.BIRD_FEATURES_PATH; + if (override && override.trim().length > 0) { + return path.resolve(override.trim()); + } + return path.join(homedir(), '.config', 'bird', DEFAULT_CACHE_FILENAME); +} +function readOverridesFromFile(cachePath) { + if (!existsSync(cachePath)) { + return null; + } + try { + const raw = readFileSync(cachePath, 'utf8'); + return normalizeOverrides(JSON.parse(raw)); + } + catch { + return null; + } +} +function readOverridesFromEnv() { + const raw = process.env.BIRD_FEATURES_JSON; + if (!raw || raw.trim().length === 0) { + return null; + } + try { + return normalizeOverrides(JSON.parse(raw)); + } + catch { + return null; + } +} +function writeOverridesToDisk(cachePath, overrides) { + const payload = toFeatureOverrides(overrides); + return mkdir(path.dirname(cachePath), { recursive: true }).then(() => writeFile(cachePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8')); +} +export function loadFeatureOverrides() { + if (cachedOverrides) { + return cachedOverrides; + } + const base = normalizeOverrides(defaultOverrides); + const fromFile = readOverridesFromFile(resolveFeaturesCachePath()); + const fromEnv = readOverridesFromEnv(); + let merged = base; + if (fromFile) { + merged = mergeOverrides(merged, fromFile); + } + if (fromEnv) { + merged = mergeOverrides(merged, fromEnv); + } + cachedOverrides = merged; + return merged; +} +export function getFeatureOverridesSnapshot() { + const overrides = toFeatureOverrides(loadFeatureOverrides()); + return { + cachePath: resolveFeaturesCachePath(), + overrides, + }; +} +export function applyFeatureOverrides(setName, base) { + const overrides = loadFeatureOverrides(); + const globalOverrides = overrides.global; + const setOverrides = overrides.sets[setName]; + if (Object.keys(globalOverrides).length === 0 && (!setOverrides || Object.keys(setOverrides).length === 0)) { + return base; + } + if (setOverrides) { + return { + ...base, + ...globalOverrides, + ...setOverrides, + }; + } + return { + ...base, + ...globalOverrides, + }; +} +export async function refreshFeatureOverridesCache() { + const cachePath = resolveFeaturesCachePath(); + const base = normalizeOverrides(defaultOverrides); + const fromFile = readOverridesFromFile(cachePath); + const merged = mergeOverrides(base, fromFile ?? { global: {}, sets: {} }); + await writeOverridesToDisk(cachePath, merged); + cachedOverrides = null; + return { cachePath, overrides: toFeatureOverrides(merged) }; +} +export function clearFeatureOverridesCache() { + cachedOverrides = null; +} +//# sourceMappingURL=runtime-features.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/runtime-features.js.map b/vendor/package/dist/lib/runtime-features.js.map new file mode 100644 index 0000000..b6d558f --- /dev/null +++ b/vendor/package/dist/lib/runtime-features.js.map @@ -0,0 +1 @@ +{"version":3,"file":"runtime-features.js","sourceRoot":"","sources":["../../src/lib/runtime-features.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/lib/runtime-query-ids.d.ts b/vendor/package/dist/lib/runtime-query-ids.d.ts new file mode 100644 index 0000000..a3e0756 --- /dev/null +++ b/vendor/package/dist/lib/runtime-query-ids.d.ts @@ -0,0 +1,33 @@ +export type RuntimeQueryIdSnapshot = { + fetchedAt: string; + ttlMs: number; + ids: Record; + discovery: { + pages: string[]; + bundles: string[]; + }; +}; +export type RuntimeQueryIdSnapshotInfo = { + snapshot: RuntimeQueryIdSnapshot; + cachePath: string; + ageMs: number; + isFresh: boolean; +}; +export type RuntimeQueryIdsOptions = { + cachePath?: string; + ttlMs?: number; + fetchImpl?: typeof fetch; +}; +export type RuntimeQueryIdStore = { + cachePath: string; + ttlMs: number; + getSnapshotInfo: () => Promise; + getQueryId: (operationName: string) => Promise; + refresh: (operationNames: string[], opts?: { + force?: boolean; + }) => Promise; + clearMemory: () => void; +}; +export declare function createRuntimeQueryIdStore(options?: RuntimeQueryIdsOptions): RuntimeQueryIdStore; +export declare const runtimeQueryIds: RuntimeQueryIdStore; +//# sourceMappingURL=runtime-query-ids.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/runtime-query-ids.d.ts.map b/vendor/package/dist/lib/runtime-query-ids.d.ts.map new file mode 100644 index 0000000..af0f299 --- /dev/null +++ b/vendor/package/dist/lib/runtime-query-ids.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"runtime-query-ids.d.ts","sourceRoot":"","sources":["../../src/lib/runtime-query-ids.ts"],"names":[],"mappings":"AA+CA,MAAM,MAAM,sBAAsB,GAAG;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,SAAS,EAAE;QACT,KAAK,EAAE,MAAM,EAAE,CAAC;QAChB,OAAO,EAAE,MAAM,EAAE,CAAC;KACnB,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,QAAQ,EAAE,sBAAsB,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,OAAO,CAAC,0BAA0B,GAAG,IAAI,CAAC,CAAC;IAClE,UAAU,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC9D,OAAO,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,0BAA0B,GAAG,IAAI,CAAC,CAAC;IAC9G,WAAW,EAAE,MAAM,IAAI,CAAC;CACzB,CAAC;AA6JF,wBAAgB,yBAAyB,CAAC,OAAO,GAAE,sBAA2B,GAAG,mBAAmB,CAwGnG;AAED,eAAO,MAAM,eAAe,qBAA8B,CAAC"} \ No newline at end of file diff --git a/vendor/package/dist/lib/runtime-query-ids.js b/vendor/package/dist/lib/runtime-query-ids.js new file mode 100644 index 0000000..c09fe62 --- /dev/null +++ b/vendor/package/dist/lib/runtime-query-ids.js @@ -0,0 +1,264 @@ +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { homedir } from 'node:os'; +import path from 'node:path'; +const DEFAULT_CACHE_FILENAME = 'query-ids-cache.json'; +const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000; +const DISCOVERY_PAGES = [ + 'https://x.com/?lang=en', + 'https://x.com/explore', + 'https://x.com/notifications', + 'https://x.com/settings/profile', +]; +const BUNDLE_URL_REGEX = /https:\/\/abs\.twimg\.com\/responsive-web\/client-web(?:-legacy)?\/[A-Za-z0-9.-]+\.js/g; +const QUERY_ID_REGEX = /^[a-zA-Z0-9_-]+$/; +const OPERATION_PATTERNS = [ + { + regex: /e\.exports=\{queryId\s*:\s*["']([^"']+)["']\s*,\s*operationName\s*:\s*["']([^"']+)["']/gs, + operationGroup: 2, + queryIdGroup: 1, + }, + { + regex: /e\.exports=\{operationName\s*:\s*["']([^"']+)["']\s*,\s*queryId\s*:\s*["']([^"']+)["']/gs, + operationGroup: 1, + queryIdGroup: 2, + }, + { + regex: /operationName\s*[:=]\s*["']([^"']+)["'](.{0,4000}?)queryId\s*[:=]\s*["']([^"']+)["']/gs, + operationGroup: 1, + queryIdGroup: 3, + }, + { + regex: /queryId\s*[:=]\s*["']([^"']+)["'](.{0,4000}?)operationName\s*[:=]\s*["']([^"']+)["']/gs, + operationGroup: 3, + queryIdGroup: 1, + }, +]; +const HEADERS = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36', + Accept: 'text/html,application/json;q=0.9,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.9', +}; +async function fetchText(fetchImpl, url) { + const response = await fetchImpl(url, { headers: HEADERS }); + if (!response.ok) { + const body = await response.text().catch(() => ''); + throw new Error(`HTTP ${response.status} for ${url}: ${body.slice(0, 120)}`); + } + return response.text(); +} +function resolveDefaultCachePath() { + const override = process.env.BIRD_QUERY_IDS_CACHE; + if (override && override.trim().length > 0) { + return path.resolve(override.trim()); + } + return path.join(homedir(), '.config', 'bird', DEFAULT_CACHE_FILENAME); +} +function parseSnapshot(raw) { + if (!raw || typeof raw !== 'object') { + return null; + } + const record = raw; + const fetchedAt = typeof record.fetchedAt === 'string' ? record.fetchedAt : null; + const ttlMs = typeof record.ttlMs === 'number' && Number.isFinite(record.ttlMs) ? record.ttlMs : null; + const ids = record.ids && typeof record.ids === 'object' ? record.ids : null; + const discovery = record.discovery && typeof record.discovery === 'object' ? record.discovery : null; + if (!fetchedAt || !ttlMs || !ids || !discovery) { + return null; + } + const pages = Array.isArray(discovery.pages) ? discovery.pages : null; + const bundles = Array.isArray(discovery.bundles) ? discovery.bundles : null; + if (!pages || !bundles) { + return null; + } + const normalizedIds = {}; + for (const [key, value] of Object.entries(ids)) { + if (typeof value === 'string' && value.trim().length > 0) { + normalizedIds[key] = value.trim(); + } + } + return { + fetchedAt, + ttlMs, + ids: normalizedIds, + discovery: { + pages: pages.filter((p) => typeof p === 'string'), + bundles: bundles.filter((b) => typeof b === 'string'), + }, + }; +} +async function readSnapshotFromDisk(cachePath) { + try { + const raw = await readFile(cachePath, 'utf8'); + return parseSnapshot(JSON.parse(raw)); + } + catch { + return null; + } +} +async function writeSnapshotToDisk(cachePath, snapshot) { + await mkdir(path.dirname(cachePath), { recursive: true }); + await writeFile(cachePath, `${JSON.stringify(snapshot, null, 2)}\n`, 'utf8'); +} +async function discoverBundles(fetchImpl) { + const bundles = new Set(); + for (const page of DISCOVERY_PAGES) { + try { + const html = await fetchText(fetchImpl, page); + for (const match of html.matchAll(BUNDLE_URL_REGEX)) { + bundles.add(match[0]); + } + } + catch { + // ignore discovery page failures; other pages often work + } + } + const discovered = [...bundles]; + if (discovered.length === 0) { + throw new Error('No client bundles discovered; x.com layout may have changed.'); + } + return discovered; +} +function extractOperations(bundleContents, bundleLabel, targets, discovered) { + for (const pattern of OPERATION_PATTERNS) { + pattern.regex.lastIndex = 0; + while (true) { + const match = pattern.regex.exec(bundleContents); + if (match === null) { + break; + } + const operationName = match[pattern.operationGroup]; + const queryId = match[pattern.queryIdGroup]; + if (!operationName || !queryId) { + continue; + } + if (!targets.has(operationName)) { + continue; + } + if (!QUERY_ID_REGEX.test(queryId)) { + continue; + } + if (discovered.has(operationName)) { + continue; + } + discovered.set(operationName, { queryId, bundle: bundleLabel }); + if (discovered.size === targets.size) { + return; + } + } + } +} +async function fetchAndExtract(fetchImpl, bundleUrls, targets) { + const discovered = new Map(); + const CONCURRENCY = 6; + for (let i = 0; i < bundleUrls.length; i += CONCURRENCY) { + const chunk = bundleUrls.slice(i, i + CONCURRENCY); + await Promise.all(chunk.map(async (url) => { + if (discovered.size === targets.size) { + return; + } + const label = url.split('/').at(-1) ?? url; + try { + const js = await fetchText(fetchImpl, url); + extractOperations(js, label, targets, discovered); + } + catch { + // ignore failed bundles + } + })); + if (discovered.size === targets.size) { + break; + } + } + return discovered; +} +export function createRuntimeQueryIdStore(options = {}) { + const fetchImpl = options.fetchImpl ?? fetch; + const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS; + const cachePath = options.cachePath ? path.resolve(options.cachePath) : resolveDefaultCachePath(); + let memorySnapshot = null; + let loadOnce = null; + let refreshInFlight = null; + const loadSnapshot = async () => { + if (memorySnapshot) { + return memorySnapshot; + } + if (!loadOnce) { + loadOnce = (async () => { + const fromDisk = await readSnapshotFromDisk(cachePath); + memorySnapshot = fromDisk; + return fromDisk; + })(); + } + return loadOnce; + }; + const getSnapshotInfo = async () => { + const snapshot = await loadSnapshot(); + if (!snapshot) { + return null; + } + const fetchedAtMs = new Date(snapshot.fetchedAt).getTime(); + const ageMs = Number.isFinite(fetchedAtMs) ? Math.max(0, Date.now() - fetchedAtMs) : Number.POSITIVE_INFINITY; + const effectiveTtl = Number.isFinite(snapshot.ttlMs) ? snapshot.ttlMs : ttlMs; + const isFresh = ageMs <= effectiveTtl; + return { snapshot, cachePath, ageMs, isFresh }; + }; + const getQueryId = async (operationName) => { + const info = await getSnapshotInfo(); + if (!info) { + return null; + } + return info.snapshot.ids[operationName] ?? null; + }; + const refresh = async (operationNames, opts = {}) => { + if (refreshInFlight) { + return refreshInFlight; + } + refreshInFlight = (async () => { + const current = await getSnapshotInfo(); + if (!opts.force && current?.isFresh) { + return current; + } + const targets = new Set(operationNames); + const bundleUrls = await discoverBundles(fetchImpl); + const discovered = await fetchAndExtract(fetchImpl, bundleUrls, targets); + if (discovered.size === 0) { + return current ?? null; + } + const ids = {}; + for (const name of operationNames) { + const entry = discovered.get(name); + if (entry?.queryId) { + ids[name] = entry.queryId; + } + } + const snapshot = { + fetchedAt: new Date().toISOString(), + ttlMs, + ids, + discovery: { + pages: [...DISCOVERY_PAGES], + bundles: bundleUrls.map((url) => url.split('/').at(-1) ?? url), + }, + }; + await writeSnapshotToDisk(cachePath, snapshot); + memorySnapshot = snapshot; + return getSnapshotInfo(); + })().finally(() => { + refreshInFlight = null; + }); + return refreshInFlight; + }; + return { + cachePath, + ttlMs, + getSnapshotInfo, + getQueryId, + refresh, + clearMemory() { + memorySnapshot = null; + loadOnce = null; + }, + }; +} +export const runtimeQueryIds = createRuntimeQueryIdStore(); +//# sourceMappingURL=runtime-query-ids.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/runtime-query-ids.js.map b/vendor/package/dist/lib/runtime-query-ids.js.map new file mode 100644 index 0000000..8e9978d --- /dev/null +++ b/vendor/package/dist/lib/runtime-query-ids.js.map @@ -0,0 +1 @@ +{"version":3,"file":"runtime-query-ids.js","sourceRoot":"","sources":["../../src/lib/runtime-query-ids.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/lib/thread-filters.d.ts b/vendor/package/dist/lib/thread-filters.d.ts new file mode 100644 index 0000000..a85ff1b --- /dev/null +++ b/vendor/package/dist/lib/thread-filters.d.ts @@ -0,0 +1,8 @@ +import type { TweetData, TweetWithMeta } from './twitter-client-types.js'; +export declare function filterAuthorChain(tweets: TweetData[], bookmarkedTweet: TweetData): TweetData[]; +export declare function filterAuthorOnly(tweets: TweetData[], bookmarkedTweet: TweetData): TweetData[]; +export declare function filterFullChain(tweets: TweetData[], bookmarkedTweet: TweetData, options?: { + includeAncestorBranches?: boolean; +}): TweetData[]; +export declare function addThreadMetadata(tweet: TweetData, allConversationTweets: TweetData[]): TweetWithMeta; +//# sourceMappingURL=thread-filters.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/thread-filters.d.ts.map b/vendor/package/dist/lib/thread-filters.d.ts.map new file mode 100644 index 0000000..fc1fafa --- /dev/null +++ b/vendor/package/dist/lib/thread-filters.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"thread-filters.d.ts","sourceRoot":"","sources":["../../src/lib/thread-filters.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAS1E,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,eAAe,EAAE,SAAS,GAAG,SAAS,EAAE,CAqC9F;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,eAAe,EAAE,SAAS,GAAG,SAAS,EAAE,CAG7F;AAED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,SAAS,EAAE,EACnB,eAAe,EAAE,SAAS,EAC1B,OAAO,GAAE;IAAE,uBAAuB,CAAC,EAAE,OAAO,CAAA;CAAO,GAClD,SAAS,EAAE,CA4Db;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,EAAE,qBAAqB,EAAE,SAAS,EAAE,GAAG,aAAa,CA0BrG"} \ No newline at end of file diff --git a/vendor/package/dist/lib/thread-filters.js b/vendor/package/dist/lib/thread-filters.js new file mode 100644 index 0000000..8700ab2 --- /dev/null +++ b/vendor/package/dist/lib/thread-filters.js @@ -0,0 +1,124 @@ +const sortByCreatedAt = (tweets) => tweets.slice().sort((a, b) => { + const aTime = a.createdAt ? Date.parse(a.createdAt) : 0; + const bTime = b.createdAt ? Date.parse(b.createdAt) : 0; + return aTime - bTime; +}); +export function filterAuthorChain(tweets, bookmarkedTweet) { + const author = bookmarkedTweet.author.username; + const byId = new Map(tweets.map((tweet) => [tweet.id, tweet])); + const chainIds = new Set(); + let current = bookmarkedTweet; + while (current && current.author.username === author) { + chainIds.add(current.id); + const parentId = current.inReplyToStatusId; + if (!parentId) { + break; + } + const parent = byId.get(parentId); + if (!parent || parent.author.username !== author) { + break; + } + current = parent; + } + let changed = true; + while (changed) { + changed = false; + for (const tweet of tweets) { + if (tweet.author.username !== author) { + continue; + } + if (chainIds.has(tweet.id)) { + continue; + } + if (tweet.inReplyToStatusId && chainIds.has(tweet.inReplyToStatusId)) { + chainIds.add(tweet.id); + changed = true; + } + } + } + return sortByCreatedAt(tweets.filter((tweet) => chainIds.has(tweet.id))); +} +export function filterAuthorOnly(tweets, bookmarkedTweet) { + const author = bookmarkedTweet.author.username; + return tweets.filter((tweet) => tweet.author.username === author); +} +export function filterFullChain(tweets, bookmarkedTweet, options = {}) { + const byId = new Map(tweets.map((tweet) => [tweet.id, tweet])); + const repliesByParent = new Map(); + for (const tweet of tweets) { + if (!tweet.inReplyToStatusId) { + continue; + } + const list = repliesByParent.get(tweet.inReplyToStatusId) ?? []; + list.push(tweet); + repliesByParent.set(tweet.inReplyToStatusId, list); + } + const chainIds = new Set(); + const ancestorIds = []; + chainIds.add(bookmarkedTweet.id); + let current = bookmarkedTweet; + while (current?.inReplyToStatusId) { + const parent = byId.get(current.inReplyToStatusId); + if (!parent) { + break; + } + if (!chainIds.has(parent.id)) { + chainIds.add(parent.id); + ancestorIds.push(parent.id); + } + current = parent; + } + const addDescendants = (startIds) => { + const queue = [...startIds]; + while (queue.length > 0) { + const currentId = queue.shift(); + if (!currentId) { + continue; + } + if (!chainIds.has(currentId)) { + chainIds.add(currentId); + } + const replies = repliesByParent.get(currentId) ?? []; + for (const reply of replies) { + if (chainIds.has(reply.id)) { + continue; + } + chainIds.add(reply.id); + queue.push(reply.id); + } + } + }; + addDescendants([bookmarkedTweet.id]); + if (options.includeAncestorBranches) { + for (const ancestorId of ancestorIds) { + addDescendants([ancestorId]); + } + } + return sortByCreatedAt(tweets.filter((tweet) => chainIds.has(tweet.id))); +} +export function addThreadMetadata(tweet, allConversationTweets) { + const author = tweet.author.username; + const hasSelfReplies = allConversationTweets.some((candidate) => candidate.inReplyToStatusId === tweet.id && candidate.author.username === author); + const isRoot = !tweet.inReplyToStatusId; + let threadPosition; + if (isRoot && !hasSelfReplies) { + threadPosition = 'standalone'; + } + else if (isRoot && hasSelfReplies) { + threadPosition = 'root'; + } + else if (!isRoot && hasSelfReplies) { + threadPosition = 'middle'; + } + else { + threadPosition = 'end'; + } + return { + ...tweet, + isThread: hasSelfReplies || !isRoot, + threadPosition, + hasSelfReplies, + threadRootId: tweet.conversationId ?? null, + }; +} +//# sourceMappingURL=thread-filters.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/thread-filters.js.map b/vendor/package/dist/lib/thread-filters.js.map new file mode 100644 index 0000000..26ae817 --- /dev/null +++ b/vendor/package/dist/lib/thread-filters.js.map @@ -0,0 +1 @@ +{"version":3,"file":"thread-filters.js","sourceRoot":"","sources":["../../src/lib/thread-filters.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-base.d.ts b/vendor/package/dist/lib/twitter-client-base.d.ts new file mode 100644 index 0000000..258e424 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-base.d.ts @@ -0,0 +1,38 @@ +import { type OperationName } from './twitter-client-constants.js'; +import type { CurrentUserResult, TwitterClientOptions } from './twitter-client-types.js'; +export type Constructor = new (...args: any[]) => T; +export type AbstractConstructor = abstract new (...args: any[]) => T; +export type Mixin, TAdded> = abstract new (...args: ConstructorParameters) => TwitterClientBase & TAdded; +export declare abstract class TwitterClientBase { + protected authToken: string; + protected ct0: string; + protected cookieHeader: string; + protected userAgent: string; + protected timeoutMs?: number; + protected quoteDepth: number; + protected clientUuid: string; + protected clientDeviceId: string; + protected clientUserId?: string; + constructor(options: TwitterClientOptions); + protected abstract getCurrentUser(): Promise; + protected sleep(ms: number): Promise; + protected getQueryId(operationName: OperationName): Promise; + protected refreshQueryIds(): Promise; + protected withRefreshedQueryIdsOn404(attempt: () => Promise): Promise<{ + result: T; + refreshed: boolean; + }>; + protected getTweetDetailQueryIds(): Promise; + protected getSearchTimelineQueryIds(): Promise; + protected fetchWithTimeout(url: string, init: RequestInit): Promise; + protected getHeaders(): Record; + protected createTransactionId(): string; + protected getBaseHeaders(): Record; + protected getJsonHeaders(): Record; + protected getUploadHeaders(): Record; + protected ensureClientUserId(): Promise; +} +//# sourceMappingURL=twitter-client-base.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-base.d.ts.map b/vendor/package/dist/lib/twitter-client-base.d.ts.map new file mode 100644 index 0000000..eacd113 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-base.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-base.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-base.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,aAAa,EAAyC,MAAM,+BAA+B,CAAC;AAC1G,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAIzF,MAAM,MAAM,WAAW,CAAC,CAAC,GAAG,MAAM,IAAI,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;AAEhE,MAAM,MAAM,mBAAmB,CAAC,CAAC,GAAG,MAAM,IAAI,QAAQ,MAAM,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;AACjF,MAAM,MAAM,KAAK,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EAAE,MAAM,IAAI,QAAQ,MACxF,GAAG,IAAI,EAAE,qBAAqB,CAAC,KAAK,CAAC,KAClC,iBAAiB,GAAG,MAAM,CAAC;AAEhC,8BAAsB,iBAAiB;IACrC,SAAS,CAAC,SAAS,EAAE,MAAM,CAAC;IAC5B,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,YAAY,EAAE,MAAM,CAAC;IAC/B,SAAS,CAAC,SAAS,EAAE,MAAM,CAAC;IAC5B,SAAS,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,UAAU,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,UAAU,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,cAAc,EAAE,MAAM,CAAC;IACjC,SAAS,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;gBAEpB,OAAO,EAAE,oBAAoB;IAgBzC,SAAS,CAAC,QAAQ,CAAC,cAAc,IAAI,OAAO,CAAC,iBAAiB,CAAC;cAE/C,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;cAIhC,UAAU,CAAC,aAAa,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC;cAKzD,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;cAWhC,0BAA0B,CAAC,CAAC,SAAS;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,EACzF,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC;QAAE,MAAM,EAAE,CAAC,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC;cAU7B,sBAAsB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;cAK3C,yBAAyB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;cAK9C,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC;IAcnF,SAAS,CAAC,UAAU,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAI9C,SAAS,CAAC,mBAAmB,IAAI,MAAM;IAIvC,SAAS,CAAC,cAAc,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IA0BlD,SAAS,CAAC,cAAc,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAOlD,SAAS,CAAC,gBAAgB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;cAKpC,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;CAYpD"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-base.js b/vendor/package/dist/lib/twitter-client-base.js new file mode 100644 index 0000000..8b7e969 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-base.js @@ -0,0 +1,129 @@ +import { randomBytes, randomUUID } from 'node:crypto'; +import { runtimeQueryIds } from './runtime-query-ids.js'; +import { QUERY_IDS, TARGET_QUERY_ID_OPERATIONS } from './twitter-client-constants.js'; +import { normalizeQuoteDepth } from './twitter-client-utils.js'; +export class TwitterClientBase { + authToken; + ct0; + cookieHeader; + userAgent; + timeoutMs; + quoteDepth; + clientUuid; + clientDeviceId; + clientUserId; + constructor(options) { + if (!options.cookies.authToken || !options.cookies.ct0) { + throw new Error('Both authToken and ct0 cookies are required'); + } + this.authToken = options.cookies.authToken; + this.ct0 = options.cookies.ct0; + this.cookieHeader = options.cookies.cookieHeader || `auth_token=${this.authToken}; ct0=${this.ct0}`; + this.userAgent = + options.userAgent || + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'; + this.timeoutMs = options.timeoutMs; + this.quoteDepth = normalizeQuoteDepth(options.quoteDepth); + this.clientUuid = randomUUID(); + this.clientDeviceId = randomUUID(); + } + async sleep(ms) { + await new Promise((resolve) => setTimeout(resolve, ms)); + } + async getQueryId(operationName) { + const cached = await runtimeQueryIds.getQueryId(operationName); + return cached ?? QUERY_IDS[operationName]; + } + async refreshQueryIds() { + if (process.env.NODE_ENV === 'test') { + return; + } + try { + await runtimeQueryIds.refresh(TARGET_QUERY_ID_OPERATIONS, { force: true }); + } + catch { + // ignore refresh failures; callers will fall back to baked-in IDs + } + } + async withRefreshedQueryIdsOn404(attempt) { + const firstAttempt = await attempt(); + if (firstAttempt.success || !firstAttempt.had404) { + return { result: firstAttempt, refreshed: false }; + } + await this.refreshQueryIds(); + const secondAttempt = await attempt(); + return { result: secondAttempt, refreshed: true }; + } + async getTweetDetailQueryIds() { + const primary = await this.getQueryId('TweetDetail'); + return Array.from(new Set([primary, '97JF30KziU00483E_8elBA', 'aFvUsJm2c-oDkJV75blV6g'])); + } + async getSearchTimelineQueryIds() { + const primary = await this.getQueryId('SearchTimeline'); + return Array.from(new Set([primary, 'M1jEez78PEfVfbQLvlWMvQ', '5h0kNbk3ii97rmfY6CdgAA', 'Tp1sewRU1AsZpBWhqCZicQ'])); + } + async fetchWithTimeout(url, init) { + if (!this.timeoutMs || this.timeoutMs <= 0) { + return fetch(url, init); + } + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs); + try { + return await fetch(url, { ...init, signal: controller.signal }); + } + finally { + clearTimeout(timeoutId); + } + } + getHeaders() { + return this.getJsonHeaders(); + } + createTransactionId() { + return randomBytes(16).toString('hex'); + } + getBaseHeaders() { + const headers = { + accept: '*/*', + 'accept-language': 'en-US,en;q=0.9', + authorization: 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA', + 'x-csrf-token': this.ct0, + 'x-twitter-auth-type': 'OAuth2Session', + 'x-twitter-active-user': 'yes', + 'x-twitter-client-language': 'en', + 'x-client-uuid': this.clientUuid, + 'x-twitter-client-deviceid': this.clientDeviceId, + 'x-client-transaction-id': this.createTransactionId(), + cookie: this.cookieHeader, + 'user-agent': this.userAgent, + origin: 'https://x.com', + referer: 'https://x.com/', + }; + if (this.clientUserId) { + headers['x-twitter-client-user-id'] = this.clientUserId; + } + return headers; + } + getJsonHeaders() { + return { + ...this.getBaseHeaders(), + 'content-type': 'application/json', + }; + } + getUploadHeaders() { + // Note: do not set content-type; URLSearchParams/FormData need to set it (incl boundary) themselves. + return this.getBaseHeaders(); + } + async ensureClientUserId() { + if (process.env.NODE_ENV === 'test') { + return; + } + if (this.clientUserId) { + return; + } + const result = await this.getCurrentUser(); + if (result.success && result.user?.id) { + this.clientUserId = result.user.id; + } + } +} +//# sourceMappingURL=twitter-client-base.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-base.js.map b/vendor/package/dist/lib/twitter-client-base.js.map new file mode 100644 index 0000000..6687bed --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-base.js.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-base.js","sourceRoot":"","sources":["../../src/lib/twitter-client-base.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-bookmarks.d.ts b/vendor/package/dist/lib/twitter-client-bookmarks.d.ts new file mode 100644 index 0000000..8a375fd --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-bookmarks.d.ts @@ -0,0 +1,7 @@ +import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js'; +import type { BookmarkMutationResult } from './twitter-client-types.js'; +export interface TwitterClientBookmarkMethods { + unbookmark(tweetId: string): Promise; +} +export declare function withBookmarks>(Base: TBase): Mixin; +//# sourceMappingURL=twitter-client-bookmarks.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-bookmarks.d.ts.map b/vendor/package/dist/lib/twitter-client-bookmarks.d.ts.map new file mode 100644 index 0000000..b6f2f20 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-bookmarks.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-bookmarks.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-bookmarks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE9F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAExE,MAAM,WAAW,4BAA4B;IAC3C,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;CAC9D;AAED,wBAAgB,aAAa,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EAChF,IAAI,EAAE,KAAK,GACV,KAAK,CAAC,KAAK,EAAE,4BAA4B,CAAC,CAqE5C"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-bookmarks.js b/vendor/package/dist/lib/twitter-client-bookmarks.js new file mode 100644 index 0000000..6f78fa1 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-bookmarks.js @@ -0,0 +1,61 @@ +import { TWITTER_API_BASE, TWITTER_GRAPHQL_POST_URL } from './twitter-client-constants.js'; +export function withBookmarks(Base) { + class TwitterClientBookmarks extends Base { + // biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement. + constructor(...args) { + super(...args); + } + async unbookmark(tweetId) { + // TODO: verify if DeleteBookmark requires client user ID or additional payload fields; add ensureClientUserId() if needed (needs live API test). + const variables = { tweet_id: tweetId }; + let queryId = await this.getQueryId('DeleteBookmark'); + let urlWithOperation = `${TWITTER_API_BASE}/${queryId}/DeleteBookmark`; + const buildBody = () => JSON.stringify({ variables, queryId }); + const buildHeaders = () => ({ ...this.getHeaders(), referer: `https://x.com/i/status/${tweetId}` }); + let body = buildBody(); + const parseResponse = async (response) => { + if (!response.ok) { + const text = await response.text(); + return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}` }; + } + const data = (await response.json()); + if (data.errors && data.errors.length > 0) { + return { success: false, error: data.errors.map((e) => e.message).join(', ') }; + } + return { success: true }; + }; + try { + let response = await this.fetchWithTimeout(urlWithOperation, { + method: 'POST', + headers: buildHeaders(), + body, + }); + if (response.status === 404) { + await this.refreshQueryIds(); + queryId = await this.getQueryId('DeleteBookmark'); + urlWithOperation = `${TWITTER_API_BASE}/${queryId}/DeleteBookmark`; + body = buildBody(); + response = await this.fetchWithTimeout(urlWithOperation, { + method: 'POST', + headers: buildHeaders(), + body, + }); + if (response.status === 404) { + const retry = await this.fetchWithTimeout(TWITTER_GRAPHQL_POST_URL, { + method: 'POST', + headers: buildHeaders(), + body, + }); + return parseResponse(retry); + } + } + return parseResponse(response); + } + catch (error) { + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + } + } + return TwitterClientBookmarks; +} +//# sourceMappingURL=twitter-client-bookmarks.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-bookmarks.js.map b/vendor/package/dist/lib/twitter-client-bookmarks.js.map new file mode 100644 index 0000000..9246701 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-bookmarks.js.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-bookmarks.js","sourceRoot":"","sources":["../../src/lib/twitter-client-bookmarks.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-constants.d.ts b/vendor/package/dist/lib/twitter-client-constants.d.ts new file mode 100644 index 0000000..a519723 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-constants.d.ts @@ -0,0 +1,43 @@ +export declare const TWITTER_API_BASE = "https://x.com/i/api/graphql"; +export declare const TWITTER_GRAPHQL_POST_URL = "https://x.com/i/api/graphql"; +export declare const TWITTER_UPLOAD_URL = "https://upload.twitter.com/i/media/upload.json"; +export declare const TWITTER_MEDIA_METADATA_URL = "https://x.com/i/api/1.1/media/metadata/create.json"; +export declare const TWITTER_STATUS_UPDATE_URL = "https://x.com/i/api/1.1/statuses/update.json"; +export declare const SETTINGS_SCREEN_NAME_REGEX: RegExp; +export declare const SETTINGS_USER_ID_REGEX: RegExp; +export declare const SETTINGS_NAME_REGEX: RegExp; +export declare const FALLBACK_QUERY_IDS: { + readonly CreateTweet: "TAJw1rBsjAtdNgTdlo2oeg"; + readonly CreateRetweet: "ojPdsZsimiJrUGLR1sjUtA"; + readonly DeleteRetweet: "iQtK4dl5hBmXewYZuEOKVw"; + readonly CreateFriendship: "8h9JVdV8dlSyqyRDJEPCsA"; + readonly DestroyFriendship: "ppXWuagMNXgvzx6WoXBW0Q"; + readonly FavoriteTweet: "lI07N6Otwv1PhnEgXILM7A"; + readonly UnfavoriteTweet: "ZYKSe-w7KEslx3JhSIk5LA"; + readonly CreateBookmark: "aoDbu3RHznuiSkQ9aNM67Q"; + readonly DeleteBookmark: "Wlmlj2-xzyS1GN3a6cj-mQ"; + readonly TweetDetail: "97JF30KziU00483E_8elBA"; + readonly SearchTimeline: "M1jEez78PEfVfbQLvlWMvQ"; + readonly UserArticlesTweets: "8zBy9h4L90aDL02RsBcCFg"; + readonly UserTweets: "Wms1GvIiHXAPBaCr9KblaA"; + readonly Bookmarks: "RV1g3b8n_SGOHwkqKYSCFw"; + readonly Following: "BEkNpEt5pNETESoqMsTEGA"; + readonly Followers: "kuFUYP9eV1FPoEy4N-pi7w"; + readonly Likes: "JR2gceKucIKcVNB_9JkhsA"; + readonly BookmarkFolderTimeline: "KJIQpsvxrTfRIlbaRIySHQ"; + readonly ListOwnerships: "wQcOSjSQ8NtgxIwvYl1lMg"; + readonly ListMemberships: "BlEXXdARdSeL_0KyKHHvvg"; + readonly ListLatestTweetsTimeline: "2TemLyqrMpTeAmysdbnVqw"; + readonly ListByRestId: "wXzyA5vM_aVkBL9G8Vp3kw"; + readonly HomeTimeline: "edseUwk9sP5Phz__9TIRnA"; + readonly HomeLatestTimeline: "iOEZpOdfekFsxSlPQCQtPg"; + readonly ExploreSidebar: "lpSN4M6qpimkF4nRFPE3nQ"; + readonly ExplorePage: "kheAINB_4pzRDqkzG3K-ng"; + readonly GenericTimelineById: "uGSr7alSjR9v6QJAIaqSKQ"; + readonly TrendHistory: "Sj4T-jSB9pr0Mxtsc1UKZQ"; + readonly AboutAccountQuery: "zs_jFPFT78rBpXv9Z3U2YQ"; +}; +export type OperationName = keyof typeof FALLBACK_QUERY_IDS; +export declare const QUERY_IDS: Record; +export declare const TARGET_QUERY_ID_OPERATIONS: Array; +//# sourceMappingURL=twitter-client-constants.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-constants.d.ts.map b/vendor/package/dist/lib/twitter-client-constants.d.ts.map new file mode 100644 index 0000000..b749584 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-constants.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-constants.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-constants.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,gBAAgB,gCAAgC,CAAC;AAC9D,eAAO,MAAM,wBAAwB,gCAAgC,CAAC;AACtE,eAAO,MAAM,kBAAkB,mDAAmD,CAAC;AACnF,eAAO,MAAM,0BAA0B,uDAAuD,CAAC;AAC/F,eAAO,MAAM,yBAAyB,iDAAiD,CAAC;AACxF,eAAO,MAAM,0BAA0B,QAA4B,CAAC;AACpE,eAAO,MAAM,sBAAsB,QAA4B,CAAC;AAChE,eAAO,MAAM,mBAAmB,QAAsC,CAAC;AAKvE,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BrB,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,kBAAkB,CAAC;AAE5D,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CAGnD,CAAC;AAEF,eAAO,MAAM,0BAA0B,EAAsC,KAAK,CAAC,aAAa,CAAC,CAAC"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-constants.js b/vendor/package/dist/lib/twitter-client-constants.js new file mode 100644 index 0000000..a9fcde3 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-constants.js @@ -0,0 +1,50 @@ +// biome-ignore lint/correctness/useImportExtensions: JSON module import doesn't use .js extension. +import queryIds from './query-ids.json' with { type: 'json' }; +export const TWITTER_API_BASE = 'https://x.com/i/api/graphql'; +export const TWITTER_GRAPHQL_POST_URL = 'https://x.com/i/api/graphql'; +export const TWITTER_UPLOAD_URL = 'https://upload.twitter.com/i/media/upload.json'; +export const TWITTER_MEDIA_METADATA_URL = 'https://x.com/i/api/1.1/media/metadata/create.json'; +export const TWITTER_STATUS_UPDATE_URL = 'https://x.com/i/api/1.1/statuses/update.json'; +export const SETTINGS_SCREEN_NAME_REGEX = /"screen_name":"([^"]+)"/; +export const SETTINGS_USER_ID_REGEX = /"user_id"\s*:\s*"(\d+)"/; +export const SETTINGS_NAME_REGEX = /"name":"([^"\\]*(?:\\.[^"\\]*)*)"/; +// Query IDs rotate frequently; the values in query-ids.json are refreshed by +// scripts/update-query-ids.ts. The fallback values keep the client usable if +// the file is missing or incomplete. +export const FALLBACK_QUERY_IDS = { + CreateTweet: 'TAJw1rBsjAtdNgTdlo2oeg', + CreateRetweet: 'ojPdsZsimiJrUGLR1sjUtA', + DeleteRetweet: 'iQtK4dl5hBmXewYZuEOKVw', + CreateFriendship: '8h9JVdV8dlSyqyRDJEPCsA', + DestroyFriendship: 'ppXWuagMNXgvzx6WoXBW0Q', + FavoriteTweet: 'lI07N6Otwv1PhnEgXILM7A', + UnfavoriteTweet: 'ZYKSe-w7KEslx3JhSIk5LA', + CreateBookmark: 'aoDbu3RHznuiSkQ9aNM67Q', + DeleteBookmark: 'Wlmlj2-xzyS1GN3a6cj-mQ', + TweetDetail: '97JF30KziU00483E_8elBA', + SearchTimeline: 'M1jEez78PEfVfbQLvlWMvQ', + UserArticlesTweets: '8zBy9h4L90aDL02RsBcCFg', + UserTweets: 'Wms1GvIiHXAPBaCr9KblaA', + Bookmarks: 'RV1g3b8n_SGOHwkqKYSCFw', + Following: 'BEkNpEt5pNETESoqMsTEGA', + Followers: 'kuFUYP9eV1FPoEy4N-pi7w', + Likes: 'JR2gceKucIKcVNB_9JkhsA', + BookmarkFolderTimeline: 'KJIQpsvxrTfRIlbaRIySHQ', + ListOwnerships: 'wQcOSjSQ8NtgxIwvYl1lMg', + ListMemberships: 'BlEXXdARdSeL_0KyKHHvvg', + ListLatestTweetsTimeline: '2TemLyqrMpTeAmysdbnVqw', + ListByRestId: 'wXzyA5vM_aVkBL9G8Vp3kw', + HomeTimeline: 'edseUwk9sP5Phz__9TIRnA', + HomeLatestTimeline: 'iOEZpOdfekFsxSlPQCQtPg', + ExploreSidebar: 'lpSN4M6qpimkF4nRFPE3nQ', + ExplorePage: 'kheAINB_4pzRDqkzG3K-ng', + GenericTimelineById: 'uGSr7alSjR9v6QJAIaqSKQ', + TrendHistory: 'Sj4T-jSB9pr0Mxtsc1UKZQ', + AboutAccountQuery: 'zs_jFPFT78rBpXv9Z3U2YQ', +}; +export const QUERY_IDS = { + ...FALLBACK_QUERY_IDS, + ...queryIds, +}; +export const TARGET_QUERY_ID_OPERATIONS = Object.keys(FALLBACK_QUERY_IDS); +//# sourceMappingURL=twitter-client-constants.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-constants.js.map b/vendor/package/dist/lib/twitter-client-constants.js.map new file mode 100644 index 0000000..63461d9 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-constants.js.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-constants.js","sourceRoot":"","sources":["../../src/lib/twitter-client-constants.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,OAAO,QAAQ,MAAM,kBAAkB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAC;AAE9D,MAAM,CAAC,MAAM,gBAAgB,GAAG,6BAA6B,CAAC;AAC9D,MAAM,CAAC,MAAM,wBAAwB,GAAG,6BAA6B,CAAC;AACtE,MAAM,CAAC,MAAM,kBAAkB,GAAG,gDAAgD,CAAC;AACnF,MAAM,CAAC,MAAM,0BAA0B,GAAG,oDAAoD,CAAC;AAC/F,MAAM,CAAC,MAAM,yBAAyB,GAAG,8CAA8C,CAAC;AACxF,MAAM,CAAC,MAAM,0BAA0B,GAAG,yBAAyB,CAAC;AACpE,MAAM,CAAC,MAAM,sBAAsB,GAAG,yBAAyB,CAAC;AAChE,MAAM,CAAC,MAAM,mBAAmB,GAAG,mCAAmC,CAAC;AAEvE,6EAA6E;AAC7E,6EAA6E;AAC7E,qCAAqC;AACrC,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,WAAW,EAAE,wBAAwB;IACrC,aAAa,EAAE,wBAAwB;IACvC,aAAa,EAAE,wBAAwB;IACvC,gBAAgB,EAAE,wBAAwB;IAC1C,iBAAiB,EAAE,wBAAwB;IAC3C,aAAa,EAAE,wBAAwB;IACvC,eAAe,EAAE,wBAAwB;IACzC,cAAc,EAAE,wBAAwB;IACxC,cAAc,EAAE,wBAAwB;IACxC,WAAW,EAAE,wBAAwB;IACrC,cAAc,EAAE,wBAAwB;IACxC,kBAAkB,EAAE,wBAAwB;IAC5C,UAAU,EAAE,wBAAwB;IACpC,SAAS,EAAE,wBAAwB;IACnC,SAAS,EAAE,wBAAwB;IACnC,SAAS,EAAE,wBAAwB;IACnC,KAAK,EAAE,wBAAwB;IAC/B,sBAAsB,EAAE,wBAAwB;IAChD,cAAc,EAAE,wBAAwB;IACxC,eAAe,EAAE,wBAAwB;IACzC,wBAAwB,EAAE,wBAAwB;IAClD,YAAY,EAAE,wBAAwB;IACtC,YAAY,EAAE,wBAAwB;IACtC,kBAAkB,EAAE,wBAAwB;IAC5C,cAAc,EAAE,wBAAwB;IACxC,WAAW,EAAE,wBAAwB;IACrC,mBAAmB,EAAE,wBAAwB;IAC7C,YAAY,EAAE,wBAAwB;IACtC,iBAAiB,EAAE,wBAAwB;CACnC,CAAC;AAIX,MAAM,CAAC,MAAM,SAAS,GAAkC;IACtD,GAAG,kBAAkB;IACrB,GAAI,QAAmD;CACxD,CAAC;AAEF,MAAM,CAAC,MAAM,0BAA0B,GAAG,MAAM,CAAC,IAAI,CAAC,kBAAkB,CAAyB,CAAC"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-engagement.d.ts b/vendor/package/dist/lib/twitter-client-engagement.d.ts new file mode 100644 index 0000000..0874480 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-engagement.d.ts @@ -0,0 +1,16 @@ +import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js'; +import type { BookmarkMutationResult } from './twitter-client-types.js'; +export interface TwitterClientEngagementMethods { + /** Like a tweet. */ + like(tweetId: string): Promise; + /** Remove a like from a tweet. */ + unlike(tweetId: string): Promise; + /** Retweet a tweet. */ + retweet(tweetId: string): Promise; + /** Remove a retweet. */ + unretweet(tweetId: string): Promise; + /** Bookmark a tweet. */ + bookmark(tweetId: string): Promise; +} +export declare function withEngagement>(Base: TBase): Mixin; +//# sourceMappingURL=twitter-client-engagement.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-engagement.d.ts.map b/vendor/package/dist/lib/twitter-client-engagement.d.ts.map new file mode 100644 index 0000000..24ba547 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-engagement.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-engagement.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-engagement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE9F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAExE,MAAM,WAAW,8BAA8B;IAC7C,oBAAoB;IACpB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACvD,kCAAkC;IAClC,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACzD,uBAAuB;IACvB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC1D,wBAAwB;IACxB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC5D,wBAAwB;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;CAC5D;AAED,wBAAgB,cAAc,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EACjF,IAAI,EAAE,KAAK,GACV,KAAK,CAAC,KAAK,EAAE,8BAA8B,CAAC,CAkG9C"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-engagement.js b/vendor/package/dist/lib/twitter-client-engagement.js new file mode 100644 index 0000000..32adf0e --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-engagement.js @@ -0,0 +1,81 @@ +import { TWITTER_API_BASE, TWITTER_GRAPHQL_POST_URL } from './twitter-client-constants.js'; +export function withEngagement(Base) { + class TwitterClientEngagement extends Base { + // biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement. + constructor(...args) { + super(...args); + } + async performEngagementMutation(operationName, tweetId) { + await this.ensureClientUserId(); + const variables = operationName === 'DeleteRetweet' ? { tweet_id: tweetId, source_tweet_id: tweetId } : { tweet_id: tweetId }; + let queryId = await this.getQueryId(operationName); + let urlWithOperation = `${TWITTER_API_BASE}/${queryId}/${operationName}`; + const buildBody = () => JSON.stringify({ variables, queryId }); + const buildHeaders = () => ({ ...this.getHeaders(), referer: `https://x.com/i/status/${tweetId}` }); + let body = buildBody(); + const parseResponse = async (response) => { + if (!response.ok) { + const text = await response.text(); + return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}` }; + } + const data = (await response.json()); + if (data.errors && data.errors.length > 0) { + return { success: false, error: data.errors.map((e) => e.message).join(', ') }; + } + return { success: true }; + }; + try { + let response = await this.fetchWithTimeout(urlWithOperation, { + method: 'POST', + headers: buildHeaders(), + body, + }); + if (response.status === 404) { + await this.refreshQueryIds(); + queryId = await this.getQueryId(operationName); + urlWithOperation = `${TWITTER_API_BASE}/${queryId}/${operationName}`; + body = buildBody(); + response = await this.fetchWithTimeout(urlWithOperation, { + method: 'POST', + headers: buildHeaders(), + body, + }); + if (response.status === 404) { + const retry = await this.fetchWithTimeout(TWITTER_GRAPHQL_POST_URL, { + method: 'POST', + headers: buildHeaders(), + body, + }); + return parseResponse(retry); + } + } + return parseResponse(response); + } + catch (error) { + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + } + /** Like a tweet. */ + async like(tweetId) { + return this.performEngagementMutation('FavoriteTweet', tweetId); + } + /** Remove a like from a tweet. */ + async unlike(tweetId) { + return this.performEngagementMutation('UnfavoriteTweet', tweetId); + } + /** Retweet a tweet. */ + async retweet(tweetId) { + return this.performEngagementMutation('CreateRetweet', tweetId); + } + /** Remove a retweet. */ + async unretweet(tweetId) { + return this.performEngagementMutation('DeleteRetweet', tweetId); + } + /** Bookmark a tweet. */ + async bookmark(tweetId) { + return this.performEngagementMutation('CreateBookmark', tweetId); + } + } + return TwitterClientEngagement; +} +//# sourceMappingURL=twitter-client-engagement.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-engagement.js.map b/vendor/package/dist/lib/twitter-client-engagement.js.map new file mode 100644 index 0000000..edda09a --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-engagement.js.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-engagement.js","sourceRoot":"","sources":["../../src/lib/twitter-client-engagement.ts"],"names":[],"mappings":"AACA,OAAO,EAAsB,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAgB/G,MAAM,UAAU,cAAc,CAC5B,IAAW;IAEX,MAAe,uBAAwB,SAAQ,IAAI;QACjD,qHAAqH;QACrH,YAAY,GAAG,IAAW;YACxB,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;QACjB,CAAC;QAEO,KAAK,CAAC,yBAAyB,CACrC,aAA4B,EAC5B,OAAe;YAEf,MAAM,IAAI,CAAC,kBAAkB,EAAE,CAAC;YAChC,MAAM,SAAS,GACb,aAAa,KAAK,eAAe,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;YAC9G,IAAI,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;YACnD,IAAI,gBAAgB,GAAG,GAAG,gBAAgB,IAAI,OAAO,IAAI,aAAa,EAAE,CAAC;YAEzE,MAAM,SAAS,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC;YAC/D,MAAM,YAAY,GAAG,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,UAAU,EAAE,EAAE,OAAO,EAAE,0BAA0B,OAAO,EAAE,EAAE,CAAC,CAAC;YACpG,IAAI,IAAI,GAAG,SAAS,EAAE,CAAC;YAEvB,MAAM,aAAa,GAAG,KAAK,EAAE,QAAkB,EAAmC,EAAE;gBAClF,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;oBACjB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;oBACnC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,QAAQ,CAAC,MAAM,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC;gBACrF,CAAC;gBAED,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA4C,CAAC;gBAChF,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAC1C,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBACjF,CAAC;gBAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;YAC3B,CAAC,CAAC;YAEF,IAAI,CAAC;gBACH,IAAI,QAAQ,GAAG,MAAM,IAAI,CAAC,gBAAgB,CAAC,gBAAgB,EAAE;oBAC3D,MAAM,EAAE,MAAM;oBACd,OAAO,EAAE,YAAY,EAAE;oBACvB,IAAI;iBACL,CAAC,CAAC;gBAEH,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;oBAC5B,MAAM,IAAI,CAAC,eAAe,EAAE,CAAC;oBAC7B,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;oBAC/C,gBAAgB,GAAG,GAAG,gBAAgB,IAAI,OAAO,IAAI,aAAa,EAAE,CAAC;oBACrE,IAAI,GAAG,SAAS,EAAE,CAAC;oBAEnB,QAAQ,GAAG,MAAM,IAAI,CAAC,gBAAgB,CAAC,gBAAgB,EAAE;wBACvD,MAAM,EAAE,MAAM;wBACd,OAAO,EAAE,YAAY,EAAE;wBACvB,IAAI;qBACL,CAAC,CAAC;oBAEH,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;wBAC5B,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,gBAAgB,CAAC,wBAAwB,EAAE;4BAClE,MAAM,EAAE,MAAM;4BACd,OAAO,EAAE,YAAY,EAAE;4BACvB,IAAI;yBACL,CAAC,CAAC;wBAEH,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC;oBAC9B,CAAC;gBACH,CAAC;gBAED,OAAO,aAAa,CAAC,QAAQ,CAAC,CAAC;YACjC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3F,CAAC;QACH,CAAC;QAED,oBAAoB;QACpB,KAAK,CAAC,IAAI,CAAC,OAAe;YACxB,OAAO,IAAI,CAAC,yBAAyB,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;QAClE,CAAC;QAED,kCAAkC;QAClC,KAAK,CAAC,MAAM,CAAC,OAAe;YAC1B,OAAO,IAAI,CAAC,yBAAyB,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAC;QACpE,CAAC;QAED,uBAAuB;QACvB,KAAK,CAAC,OAAO,CAAC,OAAe;YAC3B,OAAO,IAAI,CAAC,yBAAyB,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;QAClE,CAAC;QAED,wBAAwB;QACxB,KAAK,CAAC,SAAS,CAAC,OAAe;YAC7B,OAAO,IAAI,CAAC,yBAAyB,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;QAClE,CAAC;QAED,wBAAwB;QACxB,KAAK,CAAC,QAAQ,CAAC,OAAe;YAC5B,OAAO,IAAI,CAAC,yBAAyB,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;QACnE,CAAC;KACF;IAED,OAAO,uBAAuB,CAAC;AACjC,CAAC"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-features.d.ts b/vendor/package/dist/lib/twitter-client-features.d.ts new file mode 100644 index 0000000..79374e7 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-features.d.ts @@ -0,0 +1,14 @@ +export declare function buildArticleFeatures(): Record; +export declare function buildTweetDetailFeatures(): Record; +export declare function buildArticleFieldToggles(): Record; +export declare function buildSearchFeatures(): Record; +export declare function buildTweetCreateFeatures(): Record; +export declare function buildTimelineFeatures(): Record; +export declare function buildBookmarksFeatures(): Record; +export declare function buildLikesFeatures(): Record; +export declare function buildListsFeatures(): Record; +export declare function buildHomeTimelineFeatures(): Record; +export declare function buildUserTweetsFeatures(): Record; +export declare function buildFollowingFeatures(): Record; +export declare function buildExploreFeatures(): Record; +//# sourceMappingURL=twitter-client-features.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-features.d.ts.map b/vendor/package/dist/lib/twitter-client-features.d.ts.map new file mode 100644 index 0000000..5f10d86 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-features.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-features.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-features.ts"],"names":[],"mappings":"AAEA,wBAAgB,oBAAoB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAwC9D;AAED,wBAAgB,wBAAwB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAelE;AAED,wBAAgB,wBAAwB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASlE;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAyC7D;AAED,wBAAgB,wBAAwB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAuClE;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAY/D;AAED,wBAAgB,sBAAsB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAKhE;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAE5D;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA6C5D;AAED,wBAAgB,yBAAyB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAInE;AAED,wBAAgB,uBAAuB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAuCjE;AAED,wBAAgB,sBAAsB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAuChE;AAED,wBAAgB,oBAAoB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA0C9D"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-features.js b/vendor/package/dist/lib/twitter-client-features.js new file mode 100644 index 0000000..6083861 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-features.js @@ -0,0 +1,347 @@ +import { applyFeatureOverrides } from './runtime-features.js'; +export function buildArticleFeatures() { + return applyFeatureOverrides('article', { + rweb_video_screen_enabled: true, + profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: true, + rweb_tipjar_consumption_enabled: true, + verified_phone_label_enabled: false, + creator_subscriptions_tweet_preview_api_enabled: true, + responsive_web_graphql_timeline_navigation_enabled: true, + responsive_web_graphql_exclude_directive_enabled: true, + responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + premium_content_api_read_enabled: false, + communities_web_enable_tweet_community_results_fetch: true, + c9s_tweet_anatomy_moderator_badge_enabled: true, + responsive_web_grok_analyze_button_fetch_trends_enabled: false, + responsive_web_grok_analyze_post_followups_enabled: false, + responsive_web_grok_annotations_enabled: false, + responsive_web_jetfuel_frame: true, + post_ctas_fetch_enabled: true, + responsive_web_grok_share_attachment_enabled: true, + articles_preview_enabled: true, + responsive_web_edit_tweet_api_enabled: true, + graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, + view_counts_everywhere_api_enabled: true, + longform_notetweets_consumption_enabled: true, + responsive_web_twitter_article_tweet_consumption_enabled: true, + tweet_awards_web_tipping_enabled: false, + responsive_web_grok_show_grok_translated_post: false, + responsive_web_grok_analysis_button_from_backend: true, + creator_subscriptions_quote_tweet_preview_enabled: false, + freedom_of_speech_not_reach_fetch_enabled: true, + standardized_nudges_misinfo: true, + tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, + longform_notetweets_rich_text_read_enabled: true, + longform_notetweets_inline_media_enabled: true, + responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, + responsive_web_enhance_cards_enabled: false, + }); +} +export function buildTweetDetailFeatures() { + return applyFeatureOverrides('tweetDetail', { + ...buildArticleFeatures(), + responsive_web_graphql_exclude_directive_enabled: true, + communities_web_enable_tweet_community_results_fetch: true, + responsive_web_twitter_article_plain_text_enabled: true, + responsive_web_twitter_article_seed_tweet_detail_enabled: true, + responsive_web_twitter_article_seed_tweet_summary_enabled: true, + longform_notetweets_rich_text_read_enabled: true, + longform_notetweets_inline_media_enabled: true, + responsive_web_edit_tweet_api_enabled: true, + tweet_awards_web_tipping_enabled: false, + creator_subscriptions_quote_tweet_preview_enabled: false, + verified_phone_label_enabled: false, + }); +} +export function buildArticleFieldToggles() { + return { + withPayments: false, + withAuxiliaryUserLabels: false, + withArticleRichContentState: true, + withArticlePlainText: true, + withGrokAnalyze: false, + withDisallowedReplyControls: false, + }; +} +export function buildSearchFeatures() { + return applyFeatureOverrides('search', { + rweb_video_screen_enabled: true, + profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: true, + rweb_tipjar_consumption_enabled: true, + verified_phone_label_enabled: false, + creator_subscriptions_tweet_preview_api_enabled: true, + responsive_web_graphql_timeline_navigation_enabled: true, + responsive_web_graphql_exclude_directive_enabled: true, + responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + premium_content_api_read_enabled: false, + communities_web_enable_tweet_community_results_fetch: true, + c9s_tweet_anatomy_moderator_badge_enabled: true, + responsive_web_grok_analyze_button_fetch_trends_enabled: false, + responsive_web_grok_analyze_post_followups_enabled: false, + responsive_web_grok_annotations_enabled: false, + responsive_web_jetfuel_frame: true, + post_ctas_fetch_enabled: true, + responsive_web_grok_share_attachment_enabled: true, + responsive_web_edit_tweet_api_enabled: true, + graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, + view_counts_everywhere_api_enabled: true, + longform_notetweets_consumption_enabled: true, + responsive_web_twitter_article_tweet_consumption_enabled: true, + tweet_awards_web_tipping_enabled: false, + responsive_web_grok_show_grok_translated_post: false, + responsive_web_grok_analysis_button_from_backend: true, + creator_subscriptions_quote_tweet_preview_enabled: false, + freedom_of_speech_not_reach_fetch_enabled: true, + standardized_nudges_misinfo: true, + tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, + rweb_video_timestamps_enabled: true, + longform_notetweets_rich_text_read_enabled: true, + longform_notetweets_inline_media_enabled: true, + responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, + articles_preview_enabled: true, + responsive_web_enhance_cards_enabled: false, + }); +} +export function buildTweetCreateFeatures() { + return applyFeatureOverrides('tweetCreate', { + rweb_video_screen_enabled: true, + creator_subscriptions_tweet_preview_api_enabled: true, + premium_content_api_read_enabled: false, + communities_web_enable_tweet_community_results_fetch: true, + c9s_tweet_anatomy_moderator_badge_enabled: true, + responsive_web_grok_analyze_button_fetch_trends_enabled: false, + responsive_web_grok_analyze_post_followups_enabled: false, + responsive_web_grok_annotations_enabled: false, + responsive_web_jetfuel_frame: true, + post_ctas_fetch_enabled: true, + responsive_web_grok_share_attachment_enabled: true, + responsive_web_edit_tweet_api_enabled: true, + graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, + view_counts_everywhere_api_enabled: true, + longform_notetweets_consumption_enabled: true, + responsive_web_twitter_article_tweet_consumption_enabled: true, + tweet_awards_web_tipping_enabled: false, + responsive_web_grok_show_grok_translated_post: false, + responsive_web_grok_analysis_button_from_backend: true, + creator_subscriptions_quote_tweet_preview_enabled: false, + longform_notetweets_rich_text_read_enabled: true, + longform_notetweets_inline_media_enabled: true, + profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, + rweb_tipjar_consumption_enabled: true, + verified_phone_label_enabled: false, + articles_preview_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, + responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + freedom_of_speech_not_reach_fetch_enabled: true, + standardized_nudges_misinfo: true, + tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, + responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_graphql_timeline_navigation_enabled: true, + responsive_web_enhance_cards_enabled: false, + }); +} +export function buildTimelineFeatures() { + return applyFeatureOverrides('timeline', { + ...buildSearchFeatures(), + blue_business_profile_image_shape_enabled: true, + responsive_web_text_conversations_enabled: false, + tweetypie_unmention_optimization_enabled: true, + vibe_api_enabled: true, + responsive_web_twitter_blue_verified_badge_is_enabled: true, + interactive_text_enabled: true, + longform_notetweets_richtext_consumption_enabled: true, + responsive_web_media_download_video_enabled: false, + }); +} +export function buildBookmarksFeatures() { + return applyFeatureOverrides('bookmarks', { + ...buildTimelineFeatures(), + graphql_timeline_v2_bookmark_timeline: true, + }); +} +export function buildLikesFeatures() { + return applyFeatureOverrides('likes', buildTimelineFeatures()); +} +export function buildListsFeatures() { + return applyFeatureOverrides('lists', { + rweb_video_screen_enabled: true, + profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: true, + rweb_tipjar_consumption_enabled: true, + verified_phone_label_enabled: false, + creator_subscriptions_tweet_preview_api_enabled: true, + responsive_web_graphql_timeline_navigation_enabled: true, + responsive_web_graphql_exclude_directive_enabled: true, + responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + premium_content_api_read_enabled: false, + communities_web_enable_tweet_community_results_fetch: true, + c9s_tweet_anatomy_moderator_badge_enabled: true, + responsive_web_grok_analyze_button_fetch_trends_enabled: false, + responsive_web_grok_analyze_post_followups_enabled: false, + responsive_web_grok_annotations_enabled: false, + responsive_web_jetfuel_frame: true, + post_ctas_fetch_enabled: true, + responsive_web_grok_share_attachment_enabled: true, + articles_preview_enabled: true, + responsive_web_edit_tweet_api_enabled: true, + graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, + view_counts_everywhere_api_enabled: true, + longform_notetweets_consumption_enabled: true, + responsive_web_twitter_article_tweet_consumption_enabled: true, + tweet_awards_web_tipping_enabled: false, + responsive_web_grok_show_grok_translated_post: false, + responsive_web_grok_analysis_button_from_backend: true, + creator_subscriptions_quote_tweet_preview_enabled: false, + freedom_of_speech_not_reach_fetch_enabled: true, + standardized_nudges_misinfo: true, + tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, + longform_notetweets_rich_text_read_enabled: true, + longform_notetweets_inline_media_enabled: true, + responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, + responsive_web_enhance_cards_enabled: false, + blue_business_profile_image_shape_enabled: false, + responsive_web_text_conversations_enabled: false, + tweetypie_unmention_optimization_enabled: true, + vibe_api_enabled: false, + interactive_text_enabled: false, + }); +} +export function buildHomeTimelineFeatures() { + return applyFeatureOverrides('homeTimeline', { + ...buildTimelineFeatures(), + }); +} +export function buildUserTweetsFeatures() { + return applyFeatureOverrides('userTweets', { + rweb_video_screen_enabled: false, + profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, + rweb_tipjar_consumption_enabled: true, + verified_phone_label_enabled: false, + creator_subscriptions_tweet_preview_api_enabled: true, + responsive_web_graphql_timeline_navigation_enabled: true, + responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + premium_content_api_read_enabled: false, + communities_web_enable_tweet_community_results_fetch: true, + c9s_tweet_anatomy_moderator_badge_enabled: true, + responsive_web_grok_analyze_button_fetch_trends_enabled: false, + responsive_web_grok_analyze_post_followups_enabled: true, + responsive_web_jetfuel_frame: true, + post_ctas_fetch_enabled: true, + responsive_web_grok_share_attachment_enabled: true, + responsive_web_grok_annotations_enabled: false, + articles_preview_enabled: true, + responsive_web_edit_tweet_api_enabled: true, + graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, + view_counts_everywhere_api_enabled: true, + longform_notetweets_consumption_enabled: true, + responsive_web_twitter_article_tweet_consumption_enabled: true, + tweet_awards_web_tipping_enabled: false, + responsive_web_grok_show_grok_translated_post: true, + responsive_web_grok_analysis_button_from_backend: true, + creator_subscriptions_quote_tweet_preview_enabled: false, + freedom_of_speech_not_reach_fetch_enabled: true, + standardized_nudges_misinfo: true, + tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, + longform_notetweets_rich_text_read_enabled: true, + longform_notetweets_inline_media_enabled: true, + responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, + responsive_web_enhance_cards_enabled: false, + }); +} +export function buildFollowingFeatures() { + return applyFeatureOverrides('following', { + rweb_video_screen_enabled: true, + profile_label_improvements_pcf_label_in_post_enabled: false, + responsive_web_profile_redirect_enabled: true, + rweb_tipjar_consumption_enabled: true, + verified_phone_label_enabled: false, + creator_subscriptions_tweet_preview_api_enabled: true, + responsive_web_graphql_timeline_navigation_enabled: true, + responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + premium_content_api_read_enabled: true, + communities_web_enable_tweet_community_results_fetch: true, + c9s_tweet_anatomy_moderator_badge_enabled: true, + responsive_web_grok_analyze_button_fetch_trends_enabled: false, + responsive_web_grok_analyze_post_followups_enabled: false, + responsive_web_grok_annotations_enabled: false, + responsive_web_jetfuel_frame: false, + post_ctas_fetch_enabled: true, + responsive_web_grok_share_attachment_enabled: false, + articles_preview_enabled: true, + responsive_web_edit_tweet_api_enabled: true, + graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, + view_counts_everywhere_api_enabled: true, + longform_notetweets_consumption_enabled: true, + responsive_web_twitter_article_tweet_consumption_enabled: true, + tweet_awards_web_tipping_enabled: true, + responsive_web_grok_show_grok_translated_post: false, + responsive_web_grok_analysis_button_from_backend: false, + creator_subscriptions_quote_tweet_preview_enabled: false, + freedom_of_speech_not_reach_fetch_enabled: true, + standardized_nudges_misinfo: true, + tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, + longform_notetweets_rich_text_read_enabled: true, + longform_notetweets_inline_media_enabled: true, + responsive_web_grok_image_annotation_enabled: false, + responsive_web_grok_imagine_annotation_enabled: false, + responsive_web_grok_community_note_auto_translation_is_enabled: false, + responsive_web_enhance_cards_enabled: false, + }); +} +export function buildExploreFeatures() { + return applyFeatureOverrides('explore', { + rweb_video_screen_enabled: true, + profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: true, + rweb_tipjar_consumption_enabled: true, + verified_phone_label_enabled: false, + creator_subscriptions_tweet_preview_api_enabled: true, + responsive_web_graphql_timeline_navigation_enabled: true, + responsive_web_graphql_exclude_directive_enabled: true, + responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + premium_content_api_read_enabled: false, + communities_web_enable_tweet_community_results_fetch: true, + c9s_tweet_anatomy_moderator_badge_enabled: true, + responsive_web_grok_analyze_button_fetch_trends_enabled: true, + responsive_web_grok_analyze_post_followups_enabled: true, + responsive_web_grok_annotations_enabled: true, + responsive_web_jetfuel_frame: true, + responsive_web_grok_share_attachment_enabled: true, + articles_preview_enabled: true, + responsive_web_edit_tweet_api_enabled: true, + graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, + view_counts_everywhere_api_enabled: true, + longform_notetweets_consumption_enabled: true, + responsive_web_twitter_article_tweet_consumption_enabled: true, + tweet_awards_web_tipping_enabled: false, + responsive_web_grok_show_grok_translated_post: true, + responsive_web_grok_analysis_button_from_backend: true, + creator_subscriptions_quote_tweet_preview_enabled: false, + freedom_of_speech_not_reach_fetch_enabled: true, + standardized_nudges_misinfo: true, + tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, + longform_notetweets_rich_text_read_enabled: true, + longform_notetweets_inline_media_enabled: true, + responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: true, + responsive_web_enhance_cards_enabled: false, + // Additional features required for ExploreSidebar + post_ctas_fetch_enabled: true, + rweb_video_timestamps_enabled: true, + }); +} +//# sourceMappingURL=twitter-client-features.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-features.js.map b/vendor/package/dist/lib/twitter-client-features.js.map new file mode 100644 index 0000000..7b4f0f0 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-features.js.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-features.js","sourceRoot":"","sources":["../../src/lib/twitter-client-features.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-follow.d.ts b/vendor/package/dist/lib/twitter-client-follow.d.ts new file mode 100644 index 0000000..cc4daed --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-follow.d.ts @@ -0,0 +1,8 @@ +import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js'; +import type { FollowMutationResult } from './twitter-client-types.js'; +export interface TwitterClientFollowMethods { + follow(userId: string): Promise; + unfollow(userId: string): Promise; +} +export declare function withFollow>(Base: TBase): Mixin; +//# sourceMappingURL=twitter-client-follow.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-follow.d.ts.map b/vendor/package/dist/lib/twitter-client-follow.d.ts.map new file mode 100644 index 0000000..3c69c13 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-follow.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-follow.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-follow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE9F,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAEtE,MAAM,WAAW,0BAA0B;IACzC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACtD,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;CACzD;AAED,wBAAgB,UAAU,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EAC7E,IAAI,EAAE,KAAK,GACV,KAAK,CAAC,KAAK,EAAE,0BAA0B,CAAC,CA8N1C"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-follow.js b/vendor/package/dist/lib/twitter-client-follow.js new file mode 100644 index 0000000..a2e1196 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-follow.js @@ -0,0 +1,178 @@ +import { TWITTER_API_BASE } from './twitter-client-constants.js'; +export function withFollow(Base) { + class TwitterClientFollow extends Base { + // biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement. + constructor(...args) { + super(...args); + } + /** + * Follow a user by their user ID + */ + async follow(userId) { + await this.ensureClientUserId(); + // Try REST API first (more reliable for follow/unfollow) + const restResult = await this.followViaRest(userId, 'create'); + if (restResult.success) { + return restResult; + } + // Fallback to GraphQL + return this.followViaGraphQL(userId, true); + } + /** + * Unfollow a user by their user ID + */ + async unfollow(userId) { + await this.ensureClientUserId(); + // Try REST API first (more reliable for follow/unfollow) + const restResult = await this.followViaRest(userId, 'destroy'); + if (restResult.success) { + return restResult; + } + // Fallback to GraphQL + return this.followViaGraphQL(userId, false); + } + async followViaRest(userId, action) { + const urls = [ + `https://x.com/i/api/1.1/friendships/${action}.json`, + `https://api.twitter.com/1.1/friendships/${action}.json`, + ]; + const params = new URLSearchParams({ + user_id: userId, + skip_status: 'true', + }); + let lastError; + for (const url of urls) { + try { + const response = await this.fetchWithTimeout(url, { + method: 'POST', + headers: { + ...this.getBaseHeaders(), + 'content-type': 'application/x-www-form-urlencoded', + }, + body: params.toString(), + }); + if (!response.ok) { + const text = await response.text(); + // Parse error response + try { + const errorData = JSON.parse(text); + if (errorData.errors && errorData.errors.length > 0) { + const error = errorData.errors[0]; + // Code 160: Already following / already not following + if (error.code === 160) { + return { success: true }; + } + // Code 162: Blocked from following + if (error.code === 162) { + return { success: false, error: 'You have been blocked from following this account' }; + } + // Code 108: Cannot find specified user + if (error.code === 108) { + return { success: false, error: 'User not found' }; + } + lastError = `${error.message} (code ${error.code})`; + continue; + } + } + catch { + // Not JSON, continue with generic error + } + lastError = `HTTP ${response.status}: ${text.slice(0, 200)}`; + continue; + } + const data = (await response.json()); + if (data.errors && data.errors.length > 0) { + lastError = data.errors.map((e) => e.message).join(', '); + continue; + } + // Success - user data returned + if (data.id_str || data.screen_name) { + return { + success: true, + userId: data.id_str, + username: data.screen_name, + }; + } + return { success: true }; + } + catch (error) { + lastError = error instanceof Error ? error.message : String(error); + } + } + return { success: false, error: lastError ?? `Unknown error during ${action}` }; + } + async followViaGraphQL(userId, follow) { + const operationName = follow ? 'CreateFriendship' : 'DestroyFriendship'; + const variables = { + user_id: userId, + }; + const tryOnce = async () => { + let lastError; + let had404 = false; + const queryIds = await this.getFollowQueryIds(follow); + for (const queryId of queryIds) { + const url = `${TWITTER_API_BASE}/${queryId}/${operationName}`; + try { + const response = await this.fetchWithTimeout(url, { + method: 'POST', + headers: this.getHeaders(), + body: JSON.stringify({ variables, queryId }), + }); + if (response.status === 404) { + had404 = true; + lastError = 'HTTP 404'; + continue; + } + if (!response.ok) { + const text = await response.text(); + lastError = `HTTP ${response.status}: ${text.slice(0, 200)}`; + continue; + } + const data = (await response.json()); + if (data.errors && data.errors.length > 0) { + lastError = data.errors.map((e) => e.message).join(', '); + continue; + } + const result = data.data?.user?.result; + return { + success: true, + userId: result?.rest_id, + username: result?.legacy?.screen_name, + had404, + }; + } + catch (error) { + lastError = error instanceof Error ? error.message : String(error); + } + } + return { + success: false, + error: lastError ?? `Unknown error during ${operationName}`, + had404, + }; + }; + const firstAttempt = await tryOnce(); + if (firstAttempt.success) { + return { success: true, userId: firstAttempt.userId, username: firstAttempt.username }; + } + if (firstAttempt.had404) { + await this.refreshQueryIds(); + const secondAttempt = await tryOnce(); + if (secondAttempt.success) { + return { success: true, userId: secondAttempt.userId, username: secondAttempt.username }; + } + return { success: false, error: secondAttempt.error }; + } + return { success: false, error: firstAttempt.error }; + } + async getFollowQueryIds(follow) { + const primary = await this.getQueryId(follow ? 'CreateFriendship' : 'DestroyFriendship'); + const fallbacks = follow + ? ['8h9JVdV8dlSyqyRDJEPCsA', 'OPwKc1HXnBT_bWXfAlo-9g'] + : ['ppXWuagMNXgvzx6WoXBW0Q', '8h9JVdV8dlSyqyRDJEPCsA']; + return Array.from(new Set([primary, ...fallbacks])); + } + } + return TwitterClientFollow; +} +//# sourceMappingURL=twitter-client-follow.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-follow.js.map b/vendor/package/dist/lib/twitter-client-follow.js.map new file mode 100644 index 0000000..462ad86 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-follow.js.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-follow.js","sourceRoot":"","sources":["../../src/lib/twitter-client-follow.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-home.d.ts b/vendor/package/dist/lib/twitter-client-home.d.ts new file mode 100644 index 0000000..ad3baf5 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-home.d.ts @@ -0,0 +1,13 @@ +import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js'; +import type { SearchResult } from './twitter-client-types.js'; +/** Options for home timeline fetch methods */ +export interface HomeTimelineFetchOptions { + /** Include raw GraphQL response in `_raw` field */ + includeRaw?: boolean; +} +export interface TwitterClientHomeMethods { + getHomeTimeline(count?: number, options?: HomeTimelineFetchOptions): Promise; + getHomeLatestTimeline(count?: number, options?: HomeTimelineFetchOptions): Promise; +} +export declare function withHome>(Base: TBase): Mixin; +//# sourceMappingURL=twitter-client-home.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-home.d.ts.map b/vendor/package/dist/lib/twitter-client-home.d.ts.map new file mode 100644 index 0000000..176c006 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-home.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-home.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-home.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAG9F,OAAO,KAAK,EAAsB,YAAY,EAAa,MAAM,2BAA2B,CAAC;AAS7F,8CAA8C;AAC9C,MAAM,WAAW,wBAAwB;IACvC,mDAAmD;IACnD,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,wBAAwB;IACvC,eAAe,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,wBAAwB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC3F,qBAAqB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,wBAAwB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CAClG;AAED,wBAAgB,QAAQ,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EAC3E,IAAI,EAAE,KAAK,GACV,KAAK,CAAC,KAAK,EAAE,wBAAwB,CAAC,CA+KxC"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-home.js b/vendor/package/dist/lib/twitter-client-home.js new file mode 100644 index 0000000..69d71cd --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-home.js @@ -0,0 +1,138 @@ +import { TWITTER_API_BASE } from './twitter-client-constants.js'; +import { buildHomeTimelineFeatures } from './twitter-client-features.js'; +import { extractCursorFromInstructions, parseTweetsFromInstructions } from './twitter-client-utils.js'; +const QUERY_UNSPECIFIED_REGEX = /query:\s*unspecified/i; +function isQueryIdMismatch(errors) { + return errors.some((error) => QUERY_UNSPECIFIED_REGEX.test(error.message ?? '')); +} +export function withHome(Base) { + class TwitterClientHome extends Base { + // biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement. + constructor(...args) { + super(...args); + } + async getHomeTimelineQueryIds() { + const primary = await this.getQueryId('HomeTimeline'); + return Array.from(new Set([primary, 'edseUwk9sP5Phz__9TIRnA'])); + } + async getHomeLatestTimelineQueryIds() { + const primary = await this.getQueryId('HomeLatestTimeline'); + return Array.from(new Set([primary, 'iOEZpOdfekFsxSlPQCQtPg'])); + } + /** + * Get the authenticated user's "For You" home timeline + */ + async getHomeTimeline(count = 20, options = {}) { + return this.fetchHomeTimeline('HomeTimeline', count, options); + } + /** + * Get the authenticated user's "Following" (latest/chronological) home timeline + */ + async getHomeLatestTimeline(count = 20, options = {}) { + return this.fetchHomeTimeline('HomeLatestTimeline', count, options); + } + async fetchHomeTimeline(operation, count, options) { + const { includeRaw = false } = options; + const features = buildHomeTimelineFeatures(); + const pageSize = 20; + const seen = new Set(); + const tweets = []; + let cursor; + const fetchPage = async (pageCount, pageCursor) => { + let lastError; + let had404 = false; + const queryIds = operation === 'HomeTimeline' + ? await this.getHomeTimelineQueryIds() + : await this.getHomeLatestTimelineQueryIds(); + for (const queryId of queryIds) { + const variables = { + count: pageCount, + includePromotedContent: true, + latestControlAvailable: true, + requestContext: 'launch', + withCommunity: true, + ...(pageCursor ? { cursor: pageCursor } : {}), + }; + const params = new URLSearchParams({ + variables: JSON.stringify(variables), + features: JSON.stringify(features), + }); + const url = `${TWITTER_API_BASE}/${queryId}/${operation}?${params.toString()}`; + try { + const response = await this.fetchWithTimeout(url, { + method: 'GET', + headers: this.getHeaders(), + }); + if (response.status === 404) { + had404 = true; + lastError = `HTTP ${response.status}`; + continue; + } + if (!response.ok) { + const text = await response.text(); + return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}`, had404 }; + } + const data = (await response.json()); + if (data.errors && data.errors.length > 0) { + const errorMessage = data.errors.map((e) => e.message).join(', '); + return { + success: false, + error: errorMessage, + had404: had404 || isQueryIdMismatch(data.errors), + }; + } + const instructions = data.data?.home?.home_timeline_urt?.instructions; + const pageTweets = parseTweetsFromInstructions(instructions, { quoteDepth: this.quoteDepth, includeRaw }); + const nextCursor = extractCursorFromInstructions(instructions); + return { success: true, tweets: pageTweets, cursor: nextCursor, had404 }; + } + catch (error) { + lastError = error instanceof Error ? error.message : String(error); + } + } + return { success: false, error: lastError ?? 'Unknown error fetching home timeline', had404 }; + }; + const fetchWithRefresh = async (pageCount, pageCursor) => { + const firstAttempt = await fetchPage(pageCount, pageCursor); + if (firstAttempt.success) { + return firstAttempt; + } + if (firstAttempt.had404) { + await this.refreshQueryIds(); + const secondAttempt = await fetchPage(pageCount, pageCursor); + if (secondAttempt.success) { + return secondAttempt; + } + return { success: false, error: secondAttempt.error }; + } + return { success: false, error: firstAttempt.error }; + }; + while (tweets.length < count) { + const pageCount = Math.min(pageSize, count - tweets.length); + const page = await fetchWithRefresh(pageCount, cursor); + if (!page.success) { + return { success: false, error: page.error }; + } + let added = 0; + for (const tweet of page.tweets) { + if (seen.has(tweet.id)) { + continue; + } + seen.add(tweet.id); + tweets.push(tweet); + added += 1; + if (tweets.length >= count) { + break; + } + } + if (!page.cursor || page.cursor === cursor || page.tweets.length === 0 || added === 0) { + break; + } + cursor = page.cursor; + } + return { success: true, tweets }; + } + } + return TwitterClientHome; +} +//# sourceMappingURL=twitter-client-home.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-home.js.map b/vendor/package/dist/lib/twitter-client-home.js.map new file mode 100644 index 0000000..9d52b74 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-home.js.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-home.js","sourceRoot":"","sources":["../../src/lib/twitter-client-home.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-lists.d.ts b/vendor/package/dist/lib/twitter-client-lists.d.ts new file mode 100644 index 0000000..2ec192b --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-lists.d.ts @@ -0,0 +1,11 @@ +import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js'; +import type { TimelineFetchOptions, TimelinePaginationOptions } from './twitter-client-timelines.js'; +import type { ListsResult, SearchResult } from './twitter-client-types.js'; +export interface TwitterClientListMethods { + getOwnedLists(count?: number): Promise; + getListMemberships(count?: number): Promise; + getListTimeline(listId: string, count?: number, options?: TimelineFetchOptions): Promise; + getAllListTimeline(listId: string, options?: TimelinePaginationOptions): Promise; +} +export declare function withLists>(Base: TBase): Mixin; +//# sourceMappingURL=twitter-client-lists.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-lists.d.ts.map b/vendor/package/dist/lib/twitter-client-lists.d.ts.map new file mode 100644 index 0000000..9aa2c31 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-lists.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-lists.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-lists.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAG9F,OAAO,KAAK,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AACrG,OAAO,KAAK,EAAsB,WAAW,EAAE,YAAY,EAA0B,MAAM,2BAA2B,CAAC;AAGvH,MAAM,WAAW,wBAAwB;IACvC,aAAa,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IACpD,kBAAkB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IACzD,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACvG,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,yBAAyB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CAChG;AAiFD,wBAAgB,SAAS,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EAC5E,IAAI,EAAE,KAAK,GACV,KAAK,CAAC,KAAK,EAAE,wBAAwB,CAAC,CAyYxC"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-lists.js b/vendor/package/dist/lib/twitter-client-lists.js new file mode 100644 index 0000000..d31934c --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-lists.js @@ -0,0 +1,321 @@ +// ABOUTME: Mixin for Twitter Lists GraphQL operations. +// ABOUTME: Provides methods to fetch user's owned lists, memberships, and list timelines. +import { TWITTER_API_BASE } from './twitter-client-constants.js'; +import { buildListsFeatures } from './twitter-client-features.js'; +import { extractCursorFromInstructions, parseTweetsFromInstructions } from './twitter-client-utils.js'; +function parseList(listResult) { + if (!listResult.id_str || !listResult.name) { + return null; + } + const owner = listResult.user_results?.result; + return { + id: listResult.id_str, + name: listResult.name, + description: listResult.description, + memberCount: listResult.member_count, + subscriberCount: listResult.subscriber_count, + isPrivate: listResult.mode?.toLowerCase() === 'private', + createdAt: listResult.created_at, + owner: owner + ? { + id: owner.rest_id ?? '', + username: owner.legacy?.screen_name ?? '', + name: owner.legacy?.name ?? '', + } + : undefined, + }; +} +function parseListsFromInstructions(instructions) { + const lists = []; + if (!instructions) { + return lists; + } + for (const instruction of instructions) { + if (!instruction.entries) { + continue; + } + for (const entry of instruction.entries) { + const listResult = entry.content?.itemContent?.list; + if (listResult) { + const parsed = parseList(listResult); + if (parsed) { + lists.push(parsed); + } + } + } + } + return lists; +} +export function withLists(Base) { + class TwitterClientLists extends Base { + // biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement. + constructor(...args) { + super(...args); + } + async getListOwnershipsQueryIds() { + const primary = await this.getQueryId('ListOwnerships'); + return Array.from(new Set([primary, 'wQcOSjSQ8NtgxIwvYl1lMg'])); + } + async getListMembershipsQueryIds() { + const primary = await this.getQueryId('ListMemberships'); + return Array.from(new Set([primary, 'BlEXXdARdSeL_0KyKHHvvg'])); + } + async getListTimelineQueryIds() { + const primary = await this.getQueryId('ListLatestTweetsTimeline'); + return Array.from(new Set([primary, '2TemLyqrMpTeAmysdbnVqw'])); + } + /** + * Get lists owned by the authenticated user + */ + async getOwnedLists(count = 100) { + const userResult = await this.getCurrentUser(); + if (!userResult.success || !userResult.user) { + return { success: false, error: userResult.error ?? 'Could not determine current user' }; + } + const variables = { + userId: userResult.user.id, + count, + isListMembershipShown: true, + isListMemberTargetUserId: userResult.user.id, + }; + const features = buildListsFeatures(); + const params = new URLSearchParams({ + variables: JSON.stringify(variables), + features: JSON.stringify(features), + }); + const tryOnce = async () => { + let lastError; + let had404 = false; + const queryIds = await this.getListOwnershipsQueryIds(); + for (const queryId of queryIds) { + const url = `${TWITTER_API_BASE}/${queryId}/ListOwnerships?${params.toString()}`; + try { + const response = await this.fetchWithTimeout(url, { + method: 'GET', + headers: this.getHeaders(), + }); + if (response.status === 404) { + had404 = true; + lastError = `HTTP ${response.status}`; + continue; + } + if (!response.ok) { + const text = await response.text(); + return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}`, had404 }; + } + const data = (await response.json()); + if (data.errors && data.errors.length > 0) { + return { success: false, error: data.errors.map((e) => e.message).join(', '), had404 }; + } + const instructions = data.data?.user?.result?.timeline?.timeline?.instructions; + const lists = parseListsFromInstructions(instructions); + return { success: true, lists, had404 }; + } + catch (error) { + lastError = error instanceof Error ? error.message : String(error); + } + } + return { success: false, error: lastError ?? 'Unknown error fetching owned lists', had404 }; + }; + const firstAttempt = await tryOnce(); + if (firstAttempt.success) { + return { success: true, lists: firstAttempt.lists }; + } + if (firstAttempt.had404) { + await this.refreshQueryIds(); + const secondAttempt = await tryOnce(); + if (secondAttempt.success) { + return { success: true, lists: secondAttempt.lists }; + } + return { success: false, error: secondAttempt.error }; + } + return { success: false, error: firstAttempt.error }; + } + /** + * Get lists the authenticated user is a member of + */ + async getListMemberships(count = 100) { + const userResult = await this.getCurrentUser(); + if (!userResult.success || !userResult.user) { + return { success: false, error: userResult.error ?? 'Could not determine current user' }; + } + const variables = { + userId: userResult.user.id, + count, + isListMembershipShown: true, + isListMemberTargetUserId: userResult.user.id, + }; + const features = buildListsFeatures(); + const params = new URLSearchParams({ + variables: JSON.stringify(variables), + features: JSON.stringify(features), + }); + const tryOnce = async () => { + let lastError; + let had404 = false; + const queryIds = await this.getListMembershipsQueryIds(); + for (const queryId of queryIds) { + const url = `${TWITTER_API_BASE}/${queryId}/ListMemberships?${params.toString()}`; + try { + const response = await this.fetchWithTimeout(url, { + method: 'GET', + headers: this.getHeaders(), + }); + if (response.status === 404) { + had404 = true; + lastError = `HTTP ${response.status}`; + continue; + } + if (!response.ok) { + const text = await response.text(); + return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}`, had404 }; + } + const data = (await response.json()); + if (data.errors && data.errors.length > 0) { + return { success: false, error: data.errors.map((e) => e.message).join(', '), had404 }; + } + const instructions = data.data?.user?.result?.timeline?.timeline?.instructions; + const lists = parseListsFromInstructions(instructions); + return { success: true, lists, had404 }; + } + catch (error) { + lastError = error instanceof Error ? error.message : String(error); + } + } + return { success: false, error: lastError ?? 'Unknown error fetching list memberships', had404 }; + }; + const firstAttempt = await tryOnce(); + if (firstAttempt.success) { + return { success: true, lists: firstAttempt.lists }; + } + if (firstAttempt.had404) { + await this.refreshQueryIds(); + const secondAttempt = await tryOnce(); + if (secondAttempt.success) { + return { success: true, lists: secondAttempt.lists }; + } + return { success: false, error: secondAttempt.error }; + } + return { success: false, error: firstAttempt.error }; + } + /** + * Get tweets from a list timeline + */ + async getListTimeline(listId, count = 20, options = {}) { + return this.getListTimelinePaged(listId, count, options); + } + /** + * Get all tweets from a list timeline (paginated) + */ + async getAllListTimeline(listId, options) { + return this.getListTimelinePaged(listId, Number.POSITIVE_INFINITY, options); + } + /** + * Internal paginated list timeline fetcher + */ + async getListTimelinePaged(listId, limit, options = {}) { + const features = buildListsFeatures(); + const pageSize = 20; + const seen = new Set(); + const tweets = []; + let cursor = options.cursor; + let nextCursor; + let pagesFetched = 0; + const { includeRaw = false, maxPages } = options; + const fetchPage = async (pageCount, pageCursor) => { + let lastError; + let had404 = false; + const queryIds = await this.getListTimelineQueryIds(); + const variables = { + listId, + count: pageCount, + ...(pageCursor ? { cursor: pageCursor } : {}), + }; + const params = new URLSearchParams({ + variables: JSON.stringify(variables), + features: JSON.stringify(features), + }); + for (const queryId of queryIds) { + const url = `${TWITTER_API_BASE}/${queryId}/ListLatestTweetsTimeline?${params.toString()}`; + try { + const response = await this.fetchWithTimeout(url, { + method: 'GET', + headers: this.getHeaders(), + }); + if (response.status === 404) { + had404 = true; + lastError = `HTTP ${response.status}`; + continue; + } + if (!response.ok) { + const text = await response.text(); + return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}`, had404 }; + } + const data = (await response.json()); + if (data.errors && data.errors.length > 0) { + return { success: false, error: data.errors.map((e) => e.message).join(', '), had404 }; + } + const instructions = data.data?.list?.tweets_timeline?.timeline?.instructions; + const pageTweets = parseTweetsFromInstructions(instructions, { quoteDepth: this.quoteDepth, includeRaw }); + const nextCursor = extractCursorFromInstructions(instructions); + return { success: true, tweets: pageTweets, cursor: nextCursor, had404 }; + } + catch (error) { + lastError = error instanceof Error ? error.message : String(error); + } + } + return { success: false, error: lastError ?? 'Unknown error fetching list timeline', had404 }; + }; + const fetchWithRefresh = async (pageCount, pageCursor) => { + const firstAttempt = await fetchPage(pageCount, pageCursor); + if (firstAttempt.success) { + return firstAttempt; + } + if (firstAttempt.had404) { + await this.refreshQueryIds(); + const secondAttempt = await fetchPage(pageCount, pageCursor); + if (secondAttempt.success) { + return secondAttempt; + } + return { success: false, error: secondAttempt.error }; + } + return { success: false, error: firstAttempt.error }; + }; + const unlimited = limit === Number.POSITIVE_INFINITY; + while (unlimited || tweets.length < limit) { + const pageCount = unlimited ? pageSize : Math.min(pageSize, limit - tweets.length); + const page = await fetchWithRefresh(pageCount, cursor); + if (!page.success) { + return { success: false, error: page.error }; + } + pagesFetched += 1; + let added = 0; + for (const tweet of page.tweets) { + if (seen.has(tweet.id)) { + continue; + } + seen.add(tweet.id); + tweets.push(tweet); + added += 1; + if (!unlimited && tweets.length >= limit) { + break; + } + } + const pageCursor = page.cursor; + if (!pageCursor || pageCursor === cursor || page.tweets.length === 0 || added === 0) { + nextCursor = undefined; + break; + } + if (maxPages && pagesFetched >= maxPages) { + nextCursor = pageCursor; + break; + } + cursor = pageCursor; + nextCursor = pageCursor; + } + return { success: true, tweets, nextCursor }; + } + } + return TwitterClientLists; +} +//# sourceMappingURL=twitter-client-lists.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-lists.js.map b/vendor/package/dist/lib/twitter-client-lists.js.map new file mode 100644 index 0000000..fd4b5c3 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-lists.js.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-lists.js","sourceRoot":"","sources":["../../src/lib/twitter-client-lists.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-media.d.ts b/vendor/package/dist/lib/twitter-client-media.d.ts new file mode 100644 index 0000000..3e46fb6 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-media.d.ts @@ -0,0 +1,11 @@ +import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js'; +import type { UploadMediaResult } from './twitter-client-types.js'; +export interface TwitterClientMediaMethods { + uploadMedia(input: { + data: Uint8Array; + mimeType: string; + alt?: string; + }): Promise; +} +export declare function withMedia>(Base: TBase): Mixin; +//# sourceMappingURL=twitter-client-media.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-media.d.ts.map b/vendor/package/dist/lib/twitter-client-media.d.ts.map new file mode 100644 index 0000000..e4fa97e --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-media.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-media.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-media.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE9F,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAEnE,MAAM,WAAW,yBAAyB;IACxC,WAAW,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,UAAU,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;CACtG;AAED,wBAAgB,SAAS,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EAC5E,IAAI,EAAE,KAAK,GACV,KAAK,CAAC,KAAK,EAAE,yBAAyB,CAAC,CAoKzC"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-media.js b/vendor/package/dist/lib/twitter-client-media.js new file mode 100644 index 0000000..16a8439 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-media.js @@ -0,0 +1,136 @@ +import { TWITTER_MEDIA_METADATA_URL, TWITTER_UPLOAD_URL } from './twitter-client-constants.js'; +export function withMedia(Base) { + class TwitterClientMedia extends Base { + // biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement. + constructor(...args) { + super(...args); + } + mediaCategoryForMime(mimeType) { + if (mimeType.startsWith('image/')) { + if (mimeType === 'image/gif') { + return 'tweet_gif'; + } + return 'tweet_image'; + } + if (mimeType.startsWith('video/')) { + return 'tweet_video'; + } + return null; + } + async uploadMedia(input) { + const category = this.mediaCategoryForMime(input.mimeType); + if (!category) { + return { success: false, error: `Unsupported media type: ${input.mimeType}` }; + } + try { + const initParams = new URLSearchParams({ + command: 'INIT', + total_bytes: String(input.data.byteLength), + media_type: input.mimeType, + media_category: category, + }); + const initResp = await this.fetchWithTimeout(TWITTER_UPLOAD_URL, { + method: 'POST', + headers: this.getUploadHeaders(), + body: initParams, + }); + if (!initResp.ok) { + const text = await initResp.text(); + return { success: false, error: `HTTP ${initResp.status}: ${text.slice(0, 200)}` }; + } + const initBody = (await initResp.json()); + const mediaId = typeof initBody.media_id_string === 'string' + ? initBody.media_id_string + : initBody.media_id !== undefined + ? String(initBody.media_id) + : undefined; + if (!mediaId) { + return { success: false, error: 'Media upload INIT did not return media_id' }; + } + const chunkSize = 5 * 1024 * 1024; + let segmentIndex = 0; + for (let offset = 0; offset < input.data.byteLength; offset += chunkSize) { + const chunk = input.data.slice(offset, Math.min(input.data.byteLength, offset + chunkSize)); + const form = new FormData(); + form.set('command', 'APPEND'); + form.set('media_id', mediaId); + form.set('segment_index', String(segmentIndex)); + form.set('media', new Blob([chunk], { type: input.mimeType }), 'media'); + const appendResp = await this.fetchWithTimeout(TWITTER_UPLOAD_URL, { + method: 'POST', + headers: this.getUploadHeaders(), + body: form, + }); + if (!appendResp.ok) { + const text = await appendResp.text(); + return { success: false, error: `HTTP ${appendResp.status}: ${text.slice(0, 200)}` }; + } + segmentIndex += 1; + } + const finalizeParams = new URLSearchParams({ command: 'FINALIZE', media_id: mediaId }); + const finalizeResp = await this.fetchWithTimeout(TWITTER_UPLOAD_URL, { + method: 'POST', + headers: this.getUploadHeaders(), + body: finalizeParams, + }); + if (!finalizeResp.ok) { + const text = await finalizeResp.text(); + return { success: false, error: `HTTP ${finalizeResp.status}: ${text.slice(0, 200)}` }; + } + const finalizeBody = (await finalizeResp.json()); + const info = finalizeBody.processing_info; + if (info?.state && info.state !== 'succeeded') { + let attempts = 0; + while (attempts < 20) { + if (info.state === 'failed') { + const msg = info.error?.message || info.error?.name || 'Media processing failed'; + return { success: false, error: msg }; + } + const delaySecs = Number.isFinite(info.check_after_secs) ? Math.max(1, info.check_after_secs) : 2; + await this.sleep(delaySecs * 1000); + const statusUrl = `${TWITTER_UPLOAD_URL}?${new URLSearchParams({ + command: 'STATUS', + media_id: mediaId, + }).toString()}`; + const statusResp = await this.fetchWithTimeout(statusUrl, { + method: 'GET', + headers: this.getUploadHeaders(), + }); + if (!statusResp.ok) { + const text = await statusResp.text(); + return { success: false, error: `HTTP ${statusResp.status}: ${text.slice(0, 200)}` }; + } + const statusBody = (await statusResp.json()); + if (!statusBody.processing_info) { + break; + } + info.state = statusBody.processing_info.state; + info.check_after_secs = statusBody.processing_info.check_after_secs; + info.error = statusBody.processing_info.error; + if (info.state === 'succeeded') { + break; + } + attempts += 1; + } + } + if (input.alt && input.mimeType.startsWith('image/')) { + const metaResp = await this.fetchWithTimeout(TWITTER_MEDIA_METADATA_URL, { + method: 'POST', + headers: this.getJsonHeaders(), + body: JSON.stringify({ media_id: mediaId, alt_text: { text: input.alt } }), + }); + if (!metaResp.ok) { + const text = await metaResp.text(); + return { success: false, error: `HTTP ${metaResp.status}: ${text.slice(0, 200)}` }; + } + } + return { success: true, mediaId }; + } + catch (error) { + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + } + } + return TwitterClientMedia; +} +//# sourceMappingURL=twitter-client-media.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-media.js.map b/vendor/package/dist/lib/twitter-client-media.js.map new file mode 100644 index 0000000..4d2af4f --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-media.js.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-media.js","sourceRoot":"","sources":["../../src/lib/twitter-client-media.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-news.d.ts b/vendor/package/dist/lib/twitter-client-news.d.ts new file mode 100644 index 0000000..a58e93b --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-news.d.ts @@ -0,0 +1,47 @@ +import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js'; +import type { TweetData } from './twitter-client-types.js'; +declare const TIMELINE_IDS: { + readonly forYou: "VGltZWxpbmU6DAC2CwABAAAAB2Zvcl95b3UAAA=="; + readonly trending: "VGltZWxpbmU6DAC2CwABAAAACHRyZW5kaW5nAAA="; + readonly news: "VGltZWxpbmU6DAC2CwABAAAABG5ld3MAAA=="; + readonly sports: "VGltZWxpbmU6DAC2CwABAAAABnNwb3J0cwAA"; + readonly entertainment: "VGltZWxpbmU6DAC2CwABAAAADWVudGVydGFpbm1lbnQAAA=="; +}; +export type ExploreTab = keyof typeof TIMELINE_IDS; +/** Options for news fetch methods */ +export interface NewsFetchOptions { + /** Include raw GraphQL response in `_raw` field */ + includeRaw?: boolean; + /** Also fetch related tweets for each news item */ + withTweets?: boolean; + /** Number of tweets to fetch per news item (default: 5) */ + tweetsPerItem?: number; + /** Filter to show only AI-curated news items */ + aiOnly?: boolean; + /** Fetch from specific tabs only (default: all tabs) */ + tabs?: ExploreTab[]; +} +export interface NewsItem { + id: string; + headline: string; + category?: string; + timeAgo?: string; + postCount?: number; + description?: string; + url?: string; + tweets?: TweetData[]; + _raw?: any; +} +export type NewsResult = { + success: true; + items: NewsItem[]; +} | { + success: false; + error: string; +}; +export interface TwitterClientNewsMethods { + getNews(count?: number, options?: NewsFetchOptions): Promise; +} +export declare function withNews>(Base: TBase): Mixin; +export {}; +//# sourceMappingURL=twitter-client-news.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-news.d.ts.map b/vendor/package/dist/lib/twitter-client-news.d.ts.map new file mode 100644 index 0000000..4da3eb6 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-news.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-news.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-news.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAG9F,OAAO,KAAK,EAAgB,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAMzE,QAAA,MAAM,YAAY;;;;;;CAMR,CAAC;AAEX,MAAM,MAAM,UAAU,GAAG,MAAM,OAAO,YAAY,CAAC;AAEnD,qCAAqC;AACrC,MAAM,WAAW,gBAAgB;IAC/B,mDAAmD;IACnD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,mDAAmD;IACnD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,2DAA2D;IAC3D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gDAAgD;IAChD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,wDAAwD;IACxD,IAAI,CAAC,EAAE,UAAU,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;IAErB,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ;AAED,MAAM,MAAM,UAAU,GAClB;IACE,OAAO,EAAE,IAAI,CAAC;IACd,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB,GACD;IACE,OAAO,EAAE,KAAK,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEN,MAAM,WAAW,wBAAwB;IACvC,OAAO,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CAC1E;AAED,wBAAgB,QAAQ,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EAC3E,IAAI,EAAE,KAAK,GACV,KAAK,CAAC,KAAK,EAAE,wBAAwB,CAAC,CA+WxC"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-news.js b/vendor/package/dist/lib/twitter-client-news.js new file mode 100644 index 0000000..a261d39 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-news.js @@ -0,0 +1,283 @@ +import { TWITTER_API_BASE } from './twitter-client-constants.js'; +import { buildExploreFeatures } from './twitter-client-features.js'; +const POST_COUNT_REGEX = /[\d.]+[KMB]?\s*posts?/i; +const POST_COUNT_MATCH_REGEX = /([\d.]+)([KMB]?)\s*posts?/i; +// Timeline IDs for different Explore tabs +const TIMELINE_IDS = { + forYou: 'VGltZWxpbmU6DAC2CwABAAAAB2Zvcl95b3UAAA==', + trending: 'VGltZWxpbmU6DAC2CwABAAAACHRyZW5kaW5nAAA=', + news: 'VGltZWxpbmU6DAC2CwABAAAABG5ld3MAAA==', + sports: 'VGltZWxpbmU6DAC2CwABAAAABnNwb3J0cwAA', + entertainment: 'VGltZWxpbmU6DAC2CwABAAAADWVudGVydGFpbm1lbnQAAA==', +}; +export function withNews(Base) { + class TwitterClientNews extends Base { + // biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement. + constructor(...args) { + super(...args); + } + /** + * Fetch news and trending topics from Twitter's Explore page tabs + */ + async getNews(count = 10, options = {}) { + const { includeRaw = false, withTweets = false, tweetsPerItem = 5, aiOnly = false, tabs = ['forYou', 'news', 'sports', 'entertainment'], } = options; + const debug = process.env.BIRD_DEBUG === '1'; + if (debug) { + console.error(`[getNews] Fetching from tabs: ${tabs.join(', ')}`); + } + const allItems = []; + const seenHeadlines = new Set(); + // Fetch from each tab + for (const tab of tabs) { + const timelineId = TIMELINE_IDS[tab]; + if (!timelineId) { + continue; + } + try { + const tabItems = await this.fetchTimelineTab(tab, timelineId, count, aiOnly, includeRaw); + // Deduplicate across tabs + for (const item of tabItems) { + if (!seenHeadlines.has(item.headline)) { + seenHeadlines.add(item.headline); + allItems.push(item); + } + } + if (debug) { + console.error(`[getNews] Tab ${tab}: found ${tabItems.length} items, total unique: ${allItems.length}`); + } + // Stop early if we have enough + if (allItems.length >= count) { + break; + } + } + catch (error) { + if (debug) { + console.error(`[getNews] Error fetching tab ${tab}:`, error); + } + // Continue with other tabs + } + } + if (allItems.length === 0) { + return { success: false, error: 'No news items found' }; + } + // Limit to requested count + const items = allItems.slice(0, count); + if (withTweets) { + await this.enrichWithTweets(items, tweetsPerItem, includeRaw); + } + return { success: true, items }; + } + /** + * Fetch a specific timeline tab using GenericTimelineById + */ + async fetchTimelineTab(tabName, timelineId, maxCount, aiOnly, includeRaw) { + const queryId = await this.getQueryId('GenericTimelineById'); + const features = buildExploreFeatures(); + const variables = { + timelineId: timelineId, + count: maxCount * 2, // Fetch more to account for filtering + includePromotedContent: false, + }; + const params = new URLSearchParams({ + variables: JSON.stringify(variables), + features: JSON.stringify(features), + }); + const url = `${TWITTER_API_BASE}/${queryId}/GenericTimelineById?${params.toString()}`; + const response = await this.fetchWithTimeout(url, { + method: 'GET', + headers: this.getHeaders(), + }); + if (!response.ok) { + const text = await response.text(); + throw new Error(`HTTP ${response.status}: ${text.slice(0, 200)}`); + } + const data = (await response.json()); + // Debug: save response if BIRD_DEBUG_JSON is set + if (process.env.BIRD_DEBUG_JSON) { + const fs = await import('node:fs/promises'); + const debugPath = process.env.BIRD_DEBUG_JSON.replace('.json', `-${tabName}.json`); + await fs.writeFile(debugPath, JSON.stringify(data, null, 2)).catch(() => { }); + } + if (data.errors && data.errors.length > 0) { + throw new Error(data.errors.map((e) => e.message).join('; ')); + } + // Parse timeline response + return this.parseTimelineTabItems(data, tabName, maxCount, aiOnly, includeRaw); + } + /** + * Parse items from a GenericTimelineById response + */ + parseTimelineTabItems( + // biome-ignore lint/suspicious/noExplicitAny: API response structure is complex + data, source, maxCount, aiOnly, includeRaw) { + const items = []; + const seenHeadlines = new Set(); + // Navigate to timeline instructions + const timeline = data?.data?.timeline?.timeline; + if (!timeline) { + return []; + } + const instructions = timeline.instructions || []; + for (const instruction of instructions) { + const entries = instruction.entries ?? (instruction.entry ? [instruction.entry] : []); + if (!entries || entries.length === 0) { + continue; + } + for (const entry of entries) { + if (items.length >= maxCount) { + break; + } + const content = entry.content; + if (!content) { + continue; + } + // Handle TimelineTimelineItem (single trend item) + if (content.itemContent) { + const newsItem = this.parseNewsItemFromContent(content.itemContent, entry.entryId, source, seenHeadlines, aiOnly, includeRaw); + if (newsItem) { + items.push(newsItem); + } + } + // Handle TimelineTimelineModule (multiple items) + const itemsArray = content?.items || []; + for (const data of itemsArray) { + if (items.length >= maxCount) { + break; + } + // Structure can be data.itemContent OR data.item.itemContent + const itemContent = data?.itemContent || data?.item?.itemContent; + if (!itemContent) { + continue; + } + const newsItem = this.parseNewsItemFromContent(itemContent, entry.entryId, source, seenHeadlines, aiOnly, includeRaw); + if (newsItem) { + items.push(newsItem); + } + } + } + } + return items; + } + parseNewsItemFromContent( + // biome-ignore lint/suspicious/noExplicitAny: API response structure is complex + itemContent, entryId, source, seenHeadlines, aiOnly, includeRaw) { + const headline = itemContent.name || itemContent.title; + if (!headline) { + return null; + } + const trendMetadata = itemContent?.trend_metadata; + const trendUrl = itemContent.trend_url?.url || trendMetadata?.url?.url; + // Detect AI news by characteristics: + // 1. Full sentence headlines (contains spaces and is longer) + // 2. Has social_context with "News" category + // 3. Or explicitly marked as is_ai_trend + const socialContext = itemContent?.social_context?.text || ''; + const hasNewsCategory = socialContext.includes('News') || socialContext.includes('hours ago'); + const isFullSentence = headline.split(' ').length >= 5; // AI news are full sentences + const isExplicitlyAiTrend = itemContent.is_ai_trend === true; + const isAiNews = isExplicitlyAiTrend || (isFullSentence && hasNewsCategory); + // Filter AI trends if aiOnly is enabled + if (aiOnly && !isAiNews) { + return null; + } + if (seenHeadlines.has(headline)) { + return null; + } + seenHeadlines.add(headline); + let postCount; + let timeAgo; + let category = 'Trending'; + // Parse social context for metadata + const socialCtx = itemContent?.social_context; + if (socialCtx?.text) { + const socialContextText = socialCtx.text; + const parts = socialContextText.split('·').map((s) => s.trim()); + for (const part of parts) { + if (part.includes('ago')) { + timeAgo = part; + } + else if (part.match(POST_COUNT_REGEX)) { + const match = part.match(POST_COUNT_MATCH_REGEX); + if (match) { + let num = Number.parseFloat(match[1]); + const suffix = match[2]?.toUpperCase(); + if (suffix === 'K') { + num *= 1000; + } + else if (suffix === 'M') { + num *= 1_000_000; + } + else if (suffix === 'B') { + num *= 1_000_000_000; + } + postCount = Math.round(num); + } + } + else { + category = part; + } + } + } + // Parse trend metadata + if (trendMetadata?.meta_description) { + const metaDesc = trendMetadata.meta_description; + const postMatch = metaDesc.match(POST_COUNT_MATCH_REGEX); + if (postMatch) { + let num = Number.parseFloat(postMatch[1]); + const suffix = postMatch[2]?.toUpperCase(); + if (suffix === 'K') { + num *= 1000; + } + else if (suffix === 'M') { + num *= 1_000_000; + } + else if (suffix === 'B') { + num *= 1_000_000_000; + } + postCount = Math.round(num); + } + } + if (trendMetadata?.domain_context && (category === 'Trending' || category === 'News')) { + category = trendMetadata.domain_context; + } + const item = { + id: trendUrl ?? (entryId ? `${entryId}-${headline}` : `${source}-${headline}`), + headline, + category: isAiNews ? `AI · ${category}` : category, + timeAgo, + postCount, + description: itemContent.description, + url: trendUrl, + }; + if (includeRaw) { + item._raw = itemContent; + } + return item; + } + async enrichWithTweets(items, tweetsPerItem, includeRaw) { + const debug = process.env.BIRD_DEBUG === '1'; + for (const item of items) { + try { + const searchQuery = item.headline; + if (!searchQuery) { + continue; + } + // Use the search method if available (requires search mixin) + if ('search' in this && typeof this.search === 'function') { + const result = (await this.search(searchQuery, tweetsPerItem, { includeRaw })); + if (result.success && result.tweets) { + item.tweets = result.tweets; + } + } + } + catch { + if (debug) { + console.error('[getNews] Failed to enrich item with tweets:', item.headline); + } + } + } + } + } + return TwitterClientNews; +} +//# sourceMappingURL=twitter-client-news.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-news.js.map b/vendor/package/dist/lib/twitter-client-news.js.map new file mode 100644 index 0000000..cd24e72 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-news.js.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-news.js","sourceRoot":"","sources":["../../src/lib/twitter-client-news.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-posting.d.ts b/vendor/package/dist/lib/twitter-client-posting.d.ts new file mode 100644 index 0000000..2260866 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-posting.d.ts @@ -0,0 +1,8 @@ +import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js'; +import type { TweetResult } from './twitter-client-types.js'; +export interface TwitterClientPostingMethods { + tweet(text: string, mediaIds?: string[]): Promise; + reply(text: string, replyToTweetId: string, mediaIds?: string[]): Promise; +} +export declare function withPosting>(Base: TBase): Mixin; +//# sourceMappingURL=twitter-client-posting.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-posting.d.ts.map b/vendor/package/dist/lib/twitter-client-posting.d.ts.map new file mode 100644 index 0000000..9fac1c2 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-posting.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-posting.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-posting.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAG9F,OAAO,KAAK,EAAuB,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAElF,MAAM,WAAW,2BAA2B;IAC1C,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC/D,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CACxF;AAED,wBAAgB,WAAW,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EAC9E,IAAI,EAAE,KAAK,GACV,KAAK,CAAC,KAAK,EAAE,2BAA2B,CAAC,CAkR3C"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-posting.js b/vendor/package/dist/lib/twitter-client-posting.js new file mode 100644 index 0000000..bc6012e --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-posting.js @@ -0,0 +1,218 @@ +import { TWITTER_API_BASE, TWITTER_GRAPHQL_POST_URL, TWITTER_STATUS_UPDATE_URL } from './twitter-client-constants.js'; +import { buildTweetCreateFeatures } from './twitter-client-features.js'; +export function withPosting(Base) { + class TwitterClientPosting extends Base { + // biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement. + constructor(...args) { + super(...args); + } + /** + * Post a new tweet + */ + async tweet(text, mediaIds) { + const variables = { + tweet_text: text, + dark_request: false, + media: { + media_entities: (mediaIds ?? []).map((id) => ({ media_id: id, tagged_users: [] })), + possibly_sensitive: false, + }, + semantic_annotation_ids: [], + }; + const features = buildTweetCreateFeatures(); + return this.createTweet(variables, features); + } + /** + * Reply to an existing tweet + */ + async reply(text, replyToTweetId, mediaIds) { + const variables = { + tweet_text: text, + reply: { + in_reply_to_tweet_id: replyToTweetId, + exclude_reply_user_ids: [], + }, + dark_request: false, + media: { + media_entities: (mediaIds ?? []).map((id) => ({ media_id: id, tagged_users: [] })), + possibly_sensitive: false, + }, + semantic_annotation_ids: [], + }; + const features = buildTweetCreateFeatures(); + return this.createTweet(variables, features); + } + async createTweet(variables, features) { + await this.ensureClientUserId(); + let queryId = await this.getQueryId('CreateTweet'); + let urlWithOperation = `${TWITTER_API_BASE}/${queryId}/CreateTweet`; + const buildBody = () => JSON.stringify({ variables, features, queryId }); + let body = buildBody(); + try { + const headers = { ...this.getHeaders(), referer: 'https://x.com/compose/post' }; + let response = await this.fetchWithTimeout(urlWithOperation, { + method: 'POST', + headers, + body, + }); + // Twitter increasingly prefers POST to /i/api/graphql with queryId in the payload. + // If the operation URL 404s, retry the generic endpoint. + if (response.status === 404) { + await this.refreshQueryIds(); + queryId = await this.getQueryId('CreateTweet'); + urlWithOperation = `${TWITTER_API_BASE}/${queryId}/CreateTweet`; + body = buildBody(); + response = await this.fetchWithTimeout(urlWithOperation, { + method: 'POST', + headers: { ...this.getHeaders(), referer: 'https://x.com/compose/post' }, + body, + }); + if (response.status === 404) { + const retry = await this.fetchWithTimeout(TWITTER_GRAPHQL_POST_URL, { + method: 'POST', + headers: { ...this.getHeaders(), referer: 'https://x.com/compose/post' }, + body, + }); + if (!retry.ok) { + const text = await retry.text(); + return { success: false, error: `HTTP ${retry.status}: ${text.slice(0, 200)}` }; + } + const data = (await retry.json()); + if (data.errors && data.errors.length > 0) { + const fallback = await this.tryStatusUpdateFallback(data.errors, variables); + if (fallback) { + return fallback; + } + return { success: false, error: this.formatErrors(data.errors) }; + } + const tweetId = data.data?.create_tweet?.tweet_results?.result?.rest_id; + if (tweetId) { + return { success: true, tweetId }; + } + return { success: false, error: 'Tweet created but no ID returned' }; + } + } + if (!response.ok) { + const text = await response.text(); + return { + success: false, + error: `HTTP ${response.status}: ${text.slice(0, 200)}`, + }; + } + const data = (await response.json()); + if (data.errors && data.errors.length > 0) { + const fallback = await this.tryStatusUpdateFallback(data.errors, variables); + if (fallback) { + return fallback; + } + return { + success: false, + error: this.formatErrors(data.errors), + }; + } + const tweetId = data.data?.create_tweet?.tweet_results?.result?.rest_id; + if (tweetId) { + return { + success: true, + tweetId, + }; + } + return { + success: false, + error: 'Tweet created but no ID returned', + }; + } + catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } + formatErrors(errors) { + return errors + .map((error) => (typeof error.code === 'number' ? `${error.message} (${error.code})` : error.message)) + .join(', '); + } + statusUpdateInputFromCreateTweetVariables(variables) { + const text = typeof variables.tweet_text === 'string' ? variables.tweet_text : null; + if (!text) { + return null; + } + const reply = variables.reply; + const inReplyToTweetId = reply && + typeof reply === 'object' && + typeof reply.in_reply_to_tweet_id === 'string' + ? reply.in_reply_to_tweet_id + : undefined; + const media = variables.media; + const mediaEntities = media && typeof media === 'object' ? media.media_entities : undefined; + const mediaIds = Array.isArray(mediaEntities) + ? mediaEntities + .map((entity) => entity && typeof entity === 'object' && 'media_id' in entity + ? entity.media_id + : undefined) + .filter((value) => typeof value === 'string' || typeof value === 'number') + .map((value) => String(value)) + : undefined; + return { text, inReplyToTweetId, mediaIds: mediaIds && mediaIds.length > 0 ? mediaIds : undefined }; + } + async postStatusUpdate(input) { + const params = new URLSearchParams(); + params.set('status', input.text); + if (input.inReplyToTweetId) { + params.set('in_reply_to_status_id', input.inReplyToTweetId); + params.set('auto_populate_reply_metadata', 'true'); + } + if (input.mediaIds && input.mediaIds.length > 0) { + params.set('media_ids', input.mediaIds.join(',')); + } + try { + const response = await this.fetchWithTimeout(TWITTER_STATUS_UPDATE_URL, { + method: 'POST', + headers: { + ...this.getBaseHeaders(), + 'content-type': 'application/x-www-form-urlencoded', + referer: 'https://x.com/compose/post', + }, + body: params.toString(), + }); + if (!response.ok) { + const text = await response.text(); + return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}` }; + } + const data = (await response.json()); + if (data.errors && data.errors.length > 0) { + return { success: false, error: this.formatErrors(data.errors) }; + } + const tweetId = typeof data.id_str === 'string' ? data.id_str : data.id !== undefined ? String(data.id) : undefined; + if (tweetId) { + return { success: true, tweetId }; + } + return { success: false, error: 'Tweet created but no ID returned' }; + } + catch (error) { + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + } + async tryStatusUpdateFallback(errors, variables) { + if (!errors.some((error) => error.code === 226)) { + return null; + } + const input = this.statusUpdateInputFromCreateTweetVariables(variables); + if (!input) { + return null; + } + const fallback = await this.postStatusUpdate(input); + if (fallback.success) { + return fallback; + } + return { + success: false, + error: `${this.formatErrors(errors)} | fallback: ${fallback.error ?? 'Unknown error'}`, + }; + } + } + return TwitterClientPosting; +} +//# sourceMappingURL=twitter-client-posting.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-posting.js.map b/vendor/package/dist/lib/twitter-client-posting.js.map new file mode 100644 index 0000000..96ff677 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-posting.js.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-posting.js","sourceRoot":"","sources":["../../src/lib/twitter-client-posting.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-search.d.ts b/vendor/package/dist/lib/twitter-client-search.d.ts new file mode 100644 index 0000000..c6eddd3 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-search.d.ts @@ -0,0 +1,19 @@ +import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js'; +import type { SearchResult } from './twitter-client-types.js'; +/** Options for search methods */ +export interface SearchFetchOptions { + /** Include raw GraphQL response in `_raw` field */ + includeRaw?: boolean; +} +/** Options for paged search methods */ +export interface SearchPaginationOptions extends SearchFetchOptions { + maxPages?: number; + /** Starting cursor for pagination (resume from previous fetch) */ + cursor?: string; +} +export interface TwitterClientSearchMethods { + search(query: string, count?: number, options?: SearchFetchOptions): Promise; + getAllSearchResults(query: string, options?: SearchPaginationOptions): Promise; +} +export declare function withSearch>(Base: TBase): Mixin; +//# sourceMappingURL=twitter-client-search.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-search.d.ts.map b/vendor/package/dist/lib/twitter-client-search.d.ts.map new file mode 100644 index 0000000..63f9216 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-search.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-search.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAG9F,OAAO,KAAK,EAAE,YAAY,EAAa,MAAM,2BAA2B,CAAC;AAKzE,iCAAiC;AACjC,MAAM,WAAW,kBAAkB;IACjC,mDAAmD;IACnD,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,uCAAuC;AACvC,MAAM,WAAW,uBAAwB,SAAQ,kBAAkB;IACjE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,0BAA0B;IACzC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC3F,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CAC9F;AAuBD,wBAAgB,UAAU,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EAC7E,IAAI,EAAE,KAAK,GACV,KAAK,CAAC,KAAK,EAAE,0BAA0B,CAAC,CA0M1C"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-search.js b/vendor/package/dist/lib/twitter-client-search.js new file mode 100644 index 0000000..64d1fdb --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-search.js @@ -0,0 +1,157 @@ +import { TWITTER_API_BASE } from './twitter-client-constants.js'; +import { buildSearchFeatures } from './twitter-client-features.js'; +import { extractCursorFromInstructions, parseTweetsFromInstructions } from './twitter-client-utils.js'; +const RAW_QUERY_MISSING_REGEX = /must be defined/i; +function isQueryIdMismatch(payload) { + try { + const parsed = JSON.parse(payload); + return (parsed.errors?.some((error) => { + if (error?.extensions?.code === 'GRAPHQL_VALIDATION_FAILED') { + return true; + } + if (error?.path?.includes('rawQuery') && RAW_QUERY_MISSING_REGEX.test(error.message ?? '')) { + return true; + } + return false; + }) ?? false); + } + catch { + return false; + } +} +export function withSearch(Base) { + class TwitterClientSearch extends Base { + // biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement. + constructor(...args) { + super(...args); + } + /** + * Search for tweets matching a query + */ + async search(query, count = 20, options = {}) { + return this.searchPaged(query, count, options); + } + /** + * Get all search results (paged) + */ + async getAllSearchResults(query, options) { + return this.searchPaged(query, Number.POSITIVE_INFINITY, options); + } + async searchPaged(query, limit, options = {}) { + const features = buildSearchFeatures(); + const pageSize = 20; + const seen = new Set(); + const tweets = []; + let cursor = options.cursor; + let nextCursor; + let pagesFetched = 0; + const { includeRaw = false, maxPages } = options; + const fetchPage = async (pageCount, pageCursor) => { + let lastError; + let had404 = false; + const queryIds = await this.getSearchTimelineQueryIds(); + for (const queryId of queryIds) { + const variables = { + rawQuery: query, + count: pageCount, + querySource: 'typed_query', + product: 'Latest', + ...(pageCursor ? { cursor: pageCursor } : {}), + }; + const params = new URLSearchParams({ + variables: JSON.stringify(variables), + }); + const url = `${TWITTER_API_BASE}/${queryId}/SearchTimeline?${params.toString()}`; + try { + const response = await this.fetchWithTimeout(url, { + method: 'POST', + headers: this.getHeaders(), + body: JSON.stringify({ features, queryId }), + }); + if (response.status === 404) { + had404 = true; + lastError = `HTTP ${response.status}`; + continue; + } + if (!response.ok) { + const text = await response.text(); + const shouldRefreshQueryIds = (response.status === 400 || response.status === 422) && isQueryIdMismatch(text); + return { + success: false, + error: `HTTP ${response.status}: ${text.slice(0, 200)}`, + had404: had404 || shouldRefreshQueryIds, + }; + } + const data = (await response.json()); + if (data.errors && data.errors.length > 0) { + const shouldRefreshQueryIds = data.errors.some((error) => error?.extensions?.code === 'GRAPHQL_VALIDATION_FAILED'); + return { + success: false, + error: data.errors.map((e) => e.message).join(', '), + had404: had404 || shouldRefreshQueryIds, + }; + } + const instructions = data.data?.search_by_raw_query?.search_timeline?.timeline?.instructions; + const pageTweets = parseTweetsFromInstructions(instructions, { quoteDepth: this.quoteDepth, includeRaw }); + const nextCursor = extractCursorFromInstructions(instructions); + return { success: true, tweets: pageTweets, cursor: nextCursor, had404 }; + } + catch (error) { + lastError = error instanceof Error ? error.message : String(error); + } + } + return { success: false, error: lastError ?? 'Unknown error fetching search results', had404 }; + }; + const fetchWithRefresh = async (pageCount, pageCursor) => { + const firstAttempt = await fetchPage(pageCount, pageCursor); + if (firstAttempt.success) { + return firstAttempt; + } + if (firstAttempt.had404) { + await this.refreshQueryIds(); + const secondAttempt = await fetchPage(pageCount, pageCursor); + if (secondAttempt.success) { + return secondAttempt; + } + return { success: false, error: secondAttempt.error }; + } + return { success: false, error: firstAttempt.error }; + }; + const unlimited = limit === Number.POSITIVE_INFINITY; + while (unlimited || tweets.length < limit) { + const pageCount = unlimited ? pageSize : Math.min(pageSize, limit - tweets.length); + const page = await fetchWithRefresh(pageCount, cursor); + if (!page.success) { + return { success: false, error: page.error }; + } + pagesFetched += 1; + let added = 0; + for (const tweet of page.tweets) { + if (seen.has(tweet.id)) { + continue; + } + seen.add(tweet.id); + tweets.push(tweet); + added += 1; + if (!unlimited && tweets.length >= limit) { + break; + } + } + const pageCursor = page.cursor; + if (!pageCursor || pageCursor === cursor || page.tweets.length === 0 || added === 0) { + nextCursor = undefined; + break; + } + if (maxPages && pagesFetched >= maxPages) { + nextCursor = pageCursor; + break; + } + cursor = pageCursor; + nextCursor = pageCursor; + } + return { success: true, tweets, nextCursor }; + } + } + return TwitterClientSearch; +} +//# sourceMappingURL=twitter-client-search.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-search.js.map b/vendor/package/dist/lib/twitter-client-search.js.map new file mode 100644 index 0000000..3deb1af --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-search.js.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-search.js","sourceRoot":"","sources":["../../src/lib/twitter-client-search.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-timelines.d.ts b/vendor/package/dist/lib/twitter-client-timelines.d.ts new file mode 100644 index 0000000..8aa5645 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-timelines.d.ts @@ -0,0 +1,23 @@ +import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js'; +import type { SearchResult } from './twitter-client-types.js'; +/** Options for timeline fetch methods */ +export interface TimelineFetchOptions { + /** Include raw GraphQL response in `_raw` field */ + includeRaw?: boolean; +} +/** Options for paged timeline fetch methods */ +export interface TimelinePaginationOptions extends TimelineFetchOptions { + maxPages?: number; + /** Starting cursor for pagination (resume from previous fetch) */ + cursor?: string; +} +export interface TwitterClientTimelineMethods { + getBookmarks(count?: number, options?: TimelineFetchOptions): Promise; + getAllBookmarks(options?: TimelinePaginationOptions): Promise; + getLikes(count?: number, options?: TimelineFetchOptions): Promise; + getAllLikes(options?: TimelinePaginationOptions): Promise; + getBookmarkFolderTimeline(folderId: string, count?: number, options?: TimelineFetchOptions): Promise; + getAllBookmarkFolderTimeline(folderId: string, options?: TimelinePaginationOptions): Promise; +} +export declare function withTimelines>(Base: TBase): Mixin; +//# sourceMappingURL=twitter-client-timelines.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-timelines.d.ts.map b/vendor/package/dist/lib/twitter-client-timelines.d.ts.map new file mode 100644 index 0000000..917bd41 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-timelines.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-timelines.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-timelines.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAG9F,OAAO,KAAK,EAAsB,YAAY,EAAa,MAAM,2BAA2B,CAAC;AAG7F,yCAAyC;AACzC,MAAM,WAAW,oBAAoB;IACnC,mDAAmD;IACnD,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,+CAA+C;AAC/C,MAAM,WAAW,yBAA0B,SAAQ,oBAAoB;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,4BAA4B;IAC3C,YAAY,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACpF,eAAe,CAAC,OAAO,CAAC,EAAE,yBAAyB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC5E,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAChF,WAAW,CAAC,OAAO,CAAC,EAAE,yBAAyB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACxE,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACnH,4BAA4B,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,yBAAyB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CAC5G;AAED,wBAAgB,aAAa,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EAChF,IAAI,EAAE,KAAK,GACV,KAAK,CAAC,KAAK,EAAE,4BAA4B,CAAC,CA4lB5C"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-timelines.js b/vendor/package/dist/lib/twitter-client-timelines.js new file mode 100644 index 0000000..46df8a7 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-timelines.js @@ -0,0 +1,471 @@ +import { TWITTER_API_BASE } from './twitter-client-constants.js'; +import { buildBookmarksFeatures, buildLikesFeatures } from './twitter-client-features.js'; +import { extractCursorFromInstructions, parseTweetsFromInstructions } from './twitter-client-utils.js'; +export function withTimelines(Base) { + class TwitterClientTimelines extends Base { + // biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement. + constructor(...args) { + super(...args); + } + logBookmarksDebug(message, data) { + if (process.env.BIRD_DEBUG_BOOKMARKS !== '1') { + return; + } + if (data) { + console.error(`[bird][debug][bookmarks] ${message}`, JSON.stringify(data)); + } + else { + console.error(`[bird][debug][bookmarks] ${message}`); + } + } + async getBookmarksQueryIds() { + const primary = await this.getQueryId('Bookmarks'); + return Array.from(new Set([primary, 'RV1g3b8n_SGOHwkqKYSCFw', 'tmd4ifV8RHltzn8ymGg1aw'])); + } + async getBookmarkFolderQueryIds() { + const primary = await this.getQueryId('BookmarkFolderTimeline'); + return Array.from(new Set([primary, 'KJIQpsvxrTfRIlbaRIySHQ'])); + } + async getLikesQueryIds() { + const primary = await this.getQueryId('Likes'); + return Array.from(new Set([primary, 'JR2gceKucIKcVNB_9JkhsA'])); + } + /** + * Get the authenticated user's bookmarks + */ + async getBookmarks(count = 20, options = {}) { + return this.getBookmarksPaged(count, options); + } + async getAllBookmarks(options) { + return this.getBookmarksPaged(Number.POSITIVE_INFINITY, options); + } + /** + * Get the authenticated user's liked tweets + */ + async getLikes(count = 20, options = {}) { + return this.getLikesPaged(count, options); + } + async getAllLikes(options) { + return this.getLikesPaged(Number.POSITIVE_INFINITY, options); + } + async getLikesPaged(limit, options = {}) { + const userResult = await this.getCurrentUser(); + if (!userResult.success || !userResult.user) { + return { success: false, error: userResult.error ?? 'Could not determine current user' }; + } + const userId = userResult.user.id; + const features = buildLikesFeatures(); + const pageSize = 20; + const seen = new Set(); + const tweets = []; + let cursor = options.cursor; + let nextCursor; + let pagesFetched = 0; + const { includeRaw = false, maxPages } = options; + const fetchPage = async (pageCount, pageCursor) => { + let lastError; + let had404 = false; + const queryIds = await this.getLikesQueryIds(); + for (const queryId of queryIds) { + const variables = { + userId, + count: pageCount, + includePromotedContent: false, + withClientEventToken: false, + withBirdwatchNotes: false, + withVoice: true, + ...(pageCursor ? { cursor: pageCursor } : {}), + }; + const params = new URLSearchParams({ + variables: JSON.stringify(variables), + features: JSON.stringify(features), + }); + const url = `${TWITTER_API_BASE}/${queryId}/Likes?${params.toString()}`; + try { + const response = await this.fetchWithTimeout(url, { + method: 'GET', + headers: this.getHeaders(), + }); + if (response.status === 404) { + had404 = true; + lastError = `HTTP ${response.status}`; + continue; + } + if (!response.ok) { + const text = await response.text(); + return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}`, had404 }; + } + const data = (await response.json()); + const instructions = data.data?.user?.result?.timeline?.timeline?.instructions; + if (data.errors && data.errors.length > 0) { + const message = data.errors.map((e) => e.message).join(', '); + if (!instructions) { + if (message.includes('Query: Unspecified')) { + lastError = message; + continue; + } + return { success: false, error: message, had404 }; + } + } + const pageTweets = parseTweetsFromInstructions(instructions, { quoteDepth: this.quoteDepth, includeRaw }); + const extractedCursor = extractCursorFromInstructions(instructions); + return { success: true, tweets: pageTweets, cursor: extractedCursor, had404 }; + } + catch (error) { + lastError = error instanceof Error ? error.message : String(error); + } + } + return { success: false, error: lastError ?? 'Unknown error fetching likes', had404 }; + }; + const fetchWithRefresh = async (pageCount, pageCursor) => { + const firstAttempt = await fetchPage(pageCount, pageCursor); + if (firstAttempt.success) { + return firstAttempt; + } + const shouldRefresh = firstAttempt.had404 || + (typeof firstAttempt.error === 'string' && firstAttempt.error.includes('Query: Unspecified')); + if (shouldRefresh) { + await this.refreshQueryIds(); + const secondAttempt = await fetchPage(pageCount, pageCursor); + if (secondAttempt.success) { + return secondAttempt; + } + return { success: false, error: secondAttempt.error }; + } + return { success: false, error: firstAttempt.error }; + }; + const unlimited = limit === Number.POSITIVE_INFINITY; + while (unlimited || tweets.length < limit) { + const pageCount = unlimited ? pageSize : Math.min(pageSize, limit - tweets.length); + const page = await fetchWithRefresh(pageCount, cursor); + if (!page.success) { + return { success: false, error: page.error }; + } + pagesFetched += 1; + let added = 0; + for (const tweet of page.tweets) { + if (seen.has(tweet.id)) { + continue; + } + seen.add(tweet.id); + tweets.push(tweet); + added += 1; + if (!unlimited && tweets.length >= limit) { + break; + } + } + const pageCursor = page.cursor; + if (!pageCursor || pageCursor === cursor || page.tweets.length === 0 || added === 0) { + nextCursor = undefined; + break; + } + if (maxPages && pagesFetched >= maxPages) { + nextCursor = pageCursor; + break; + } + cursor = pageCursor; + nextCursor = pageCursor; + } + return { success: true, tweets, nextCursor }; + } + /** + * Get the authenticated user's bookmark folder timeline + */ + async getBookmarkFolderTimeline(folderId, count = 20, options = {}) { + return this.getBookmarkFolderTimelinePaged(folderId, count, options); + } + async getAllBookmarkFolderTimeline(folderId, options) { + return this.getBookmarkFolderTimelinePaged(folderId, Number.POSITIVE_INFINITY, options); + } + async getBookmarksPaged(limit, options = {}) { + const features = buildBookmarksFeatures(); + const pageSize = 20; + const seen = new Set(); + const tweets = []; + let cursor = options.cursor; + let nextCursor; + let pagesFetched = 0; + const { includeRaw = false, maxPages } = options; + const fetchPage = async (pageCount, pageCursor) => { + let lastError; + let had404 = false; + const queryIds = await this.getBookmarksQueryIds(); + const variables = { + count: pageCount, + includePromotedContent: false, + withDownvotePerspective: false, + withReactionsMetadata: false, + withReactionsPerspective: false, + ...(pageCursor ? { cursor: pageCursor } : {}), + }; + const params = new URLSearchParams({ + variables: JSON.stringify(variables), + features: JSON.stringify(features), + }); + for (const queryId of queryIds) { + const url = `${TWITTER_API_BASE}/${queryId}/Bookmarks?${params.toString()}`; + try { + this.logBookmarksDebug('request bookmarks page', { + queryId, + pageCount, + hasCursor: Boolean(pageCursor), + }); + const response = await this.fetchWithRetry(url, { + method: 'GET', + headers: this.getHeaders(), + }); + if (response.status === 404) { + had404 = true; + lastError = `HTTP ${response.status}`; + this.logBookmarksDebug('bookmarks 404', { queryId }); + continue; + } + if (!response.ok) { + const text = await response.text(); + this.logBookmarksDebug('bookmarks non-200', { + queryId, + status: response.status, + body: text.slice(0, 200), + }); + return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}`, had404 }; + } + const data = (await response.json()); + const instructions = data.data?.bookmark_timeline_v2?.timeline?.instructions; + const pageTweets = parseTweetsFromInstructions(instructions, { quoteDepth: this.quoteDepth, includeRaw }); + const nextCursor = extractCursorFromInstructions(instructions); + if (data.errors && data.errors.length > 0) { + this.logBookmarksDebug('bookmarks graphql errors (non-fatal)', { queryId, errors: data.errors }); + if (!instructions) { + lastError = data.errors.map((e) => e.message).join(', '); + continue; + } + } + this.logBookmarksDebug('bookmarks page parsed', { + queryId, + tweets: pageTweets.length, + hasNextCursor: Boolean(nextCursor), + }); + return { success: true, tweets: pageTweets, cursor: nextCursor, had404 }; + } + catch (error) { + lastError = error instanceof Error ? error.message : String(error); + this.logBookmarksDebug('bookmarks request error', { queryId, error: lastError }); + } + } + return { success: false, error: lastError ?? 'Unknown error fetching bookmarks', had404 }; + }; + const fetchWithRefresh = async (pageCount, pageCursor) => { + const firstAttempt = await fetchPage(pageCount, pageCursor); + if (firstAttempt.success) { + return firstAttempt; + } + if (firstAttempt.had404) { + await this.refreshQueryIds(); + const secondAttempt = await fetchPage(pageCount, pageCursor); + if (secondAttempt.success) { + return secondAttempt; + } + return { success: false, error: secondAttempt.error }; + } + return { success: false, error: firstAttempt.error }; + }; + const unlimited = limit === Number.POSITIVE_INFINITY; + while (unlimited || tweets.length < limit) { + const pageCount = unlimited ? pageSize : Math.min(pageSize, limit - tweets.length); + const page = await fetchWithRefresh(pageCount, cursor); + if (!page.success) { + return { success: false, error: page.error }; + } + pagesFetched += 1; + let added = 0; + for (const tweet of page.tweets) { + if (seen.has(tweet.id)) { + continue; + } + seen.add(tweet.id); + tweets.push(tweet); + added += 1; + if (!unlimited && tweets.length >= limit) { + break; + } + } + const pageCursor = page.cursor; + if (!pageCursor || pageCursor === cursor || page.tweets.length === 0 || added === 0) { + nextCursor = undefined; + break; + } + if (maxPages && pagesFetched >= maxPages) { + nextCursor = pageCursor; + break; + } + cursor = pageCursor; + nextCursor = pageCursor; + } + return { success: true, tweets, nextCursor }; + } + async getBookmarkFolderTimelinePaged(folderId, limit, options = {}) { + const features = buildBookmarksFeatures(); + const pageSize = 20; + const seen = new Set(); + const tweets = []; + let cursor = options.cursor; + let nextCursor; + let pagesFetched = 0; + const { includeRaw = false, maxPages } = options; + const buildVariables = (pageCount, pageCursor, includeCount) => ({ + bookmark_collection_id: folderId, + includePromotedContent: true, + ...(includeCount ? { count: pageCount } : {}), + ...(pageCursor ? { cursor: pageCursor } : {}), + }); + const fetchPage = async (pageCount, pageCursor) => { + let lastError; + let had404 = false; + const queryIds = await this.getBookmarkFolderQueryIds(); + const tryOnce = async (variables) => { + const params = new URLSearchParams({ + variables: JSON.stringify(variables), + features: JSON.stringify(features), + }); + for (const queryId of queryIds) { + const url = `${TWITTER_API_BASE}/${queryId}/BookmarkFolderTimeline?${params.toString()}`; + try { + this.logBookmarksDebug('request bookmark folder page', { + queryId, + pageCount, + hasCursor: Boolean(pageCursor), + includeCount: Object.hasOwn(variables, 'count'), + }); + const response = await this.fetchWithRetry(url, { + method: 'GET', + headers: this.getHeaders(), + }); + if (response.status === 404) { + had404 = true; + lastError = `HTTP ${response.status}`; + this.logBookmarksDebug('bookmark folder 404', { queryId }); + continue; + } + if (!response.ok) { + const text = await response.text(); + this.logBookmarksDebug('bookmark folder non-200', { + queryId, + status: response.status, + body: text.slice(0, 200), + }); + return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}`, had404 }; + } + const data = (await response.json()); + const instructions = data.data?.bookmark_collection_timeline?.timeline?.instructions; + const pageTweets = parseTweetsFromInstructions(instructions, { quoteDepth: this.quoteDepth, includeRaw }); + const nextCursor = extractCursorFromInstructions(instructions); + if (data.errors && data.errors.length > 0) { + this.logBookmarksDebug('bookmark folder graphql errors (non-fatal)', { queryId, errors: data.errors }); + if (!instructions) { + lastError = data.errors.map((e) => e.message).join(', '); + continue; + } + } + this.logBookmarksDebug('bookmark folder page parsed', { + queryId, + tweets: pageTweets.length, + hasNextCursor: Boolean(nextCursor), + }); + return { success: true, tweets: pageTweets, cursor: nextCursor, had404 }; + } + catch (error) { + lastError = error instanceof Error ? error.message : String(error); + this.logBookmarksDebug('bookmark folder request error', { queryId, error: lastError }); + } + } + return { success: false, error: lastError ?? 'Unknown error fetching bookmark folder', had404 }; + }; + let attempt = await tryOnce(buildVariables(pageCount, pageCursor, true)); + if (!attempt.success && attempt.error?.includes('Variable "$count"')) { + attempt = await tryOnce(buildVariables(pageCount, pageCursor, false)); + } + if (!attempt.success && attempt.error?.includes('Variable "$cursor"') && pageCursor) { + return { + success: false, + error: 'Bookmark folder pagination rejected the cursor parameter', + had404: attempt.had404, + }; + } + return attempt; + }; + const fetchWithRefresh = async (pageCount, pageCursor) => { + const firstAttempt = await fetchPage(pageCount, pageCursor); + if (firstAttempt.success) { + return firstAttempt; + } + if (firstAttempt.had404) { + await this.refreshQueryIds(); + const secondAttempt = await fetchPage(pageCount, pageCursor); + if (secondAttempt.success) { + return secondAttempt; + } + return { success: false, error: secondAttempt.error }; + } + return { success: false, error: firstAttempt.error }; + }; + const unlimited = limit === Number.POSITIVE_INFINITY; + while (unlimited || tweets.length < limit) { + const pageCount = unlimited ? pageSize : Math.min(pageSize, limit - tweets.length); + const page = await fetchWithRefresh(pageCount, cursor); + if (!page.success) { + return { success: false, error: page.error }; + } + pagesFetched += 1; + let added = 0; + for (const tweet of page.tweets) { + if (seen.has(tweet.id)) { + continue; + } + seen.add(tweet.id); + tweets.push(tweet); + added += 1; + if (!unlimited && tweets.length >= limit) { + break; + } + } + const pageCursor = page.cursor; + if (!pageCursor || pageCursor === cursor || page.tweets.length === 0 || added === 0) { + nextCursor = undefined; + break; + } + if (maxPages && pagesFetched >= maxPages) { + nextCursor = pageCursor; + break; + } + cursor = pageCursor; + nextCursor = pageCursor; + } + return { success: true, tweets, nextCursor }; + } + async fetchWithRetry(url, init) { + const maxRetries = 2; + const baseDelayMs = 500; + const retryable = new Set([429, 500, 502, 503, 504]); + for (let attempt = 0; attempt <= maxRetries; attempt += 1) { + const response = await this.fetchWithTimeout(url, init); + if (!retryable.has(response.status) || attempt === maxRetries) { + return response; + } + this.logBookmarksDebug('retrying bookmarks request', { + status: response.status, + attempt, + }); + // Retry-After supports delta-seconds only; HTTP-date falls back to backoff. + const retryAfter = response.headers?.get?.('retry-after'); + const retryAfterMs = retryAfter ? Number.parseInt(retryAfter, 10) * 1000 : Number.NaN; + const backoffMs = Number.isFinite(retryAfterMs) + ? retryAfterMs + : baseDelayMs * 2 ** attempt + Math.floor(Math.random() * baseDelayMs); + await new Promise((resolve) => setTimeout(resolve, backoffMs)); + } + return this.fetchWithTimeout(url, init); + } + } + return TwitterClientTimelines; +} +//# sourceMappingURL=twitter-client-timelines.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-timelines.js.map b/vendor/package/dist/lib/twitter-client-timelines.js.map new file mode 100644 index 0000000..453e464 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-timelines.js.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-timelines.js","sourceRoot":"","sources":["../../src/lib/twitter-client-timelines.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-tweet-detail.d.ts b/vendor/package/dist/lib/twitter-client-tweet-detail.d.ts new file mode 100644 index 0000000..1876012 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-tweet-detail.d.ts @@ -0,0 +1,25 @@ +import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js'; +import type { GetTweetResult, SearchResult } from './twitter-client-types.js'; +/** Options for tweet fetching methods */ +export interface TweetFetchOptions { + /** Include raw GraphQL response in `_raw` field */ + includeRaw?: boolean; +} +/** Options for paginated tweet detail fetch */ +export interface TweetDetailPaginationOptions extends TweetFetchOptions { + /** Maximum number of pages to fetch (default: unlimited when using pagination) */ + maxPages?: number; + /** Starting cursor for pagination (resume from previous fetch) */ + cursor?: string; + /** Delay in milliseconds between page fetches (default: 1000) */ + pageDelayMs?: number; +} +export interface TwitterClientTweetDetailMethods { + getTweet(tweetId: string, options?: TweetFetchOptions): Promise; + getReplies(tweetId: string, options?: TweetFetchOptions): Promise; + getThread(tweetId: string, options?: TweetFetchOptions): Promise; + getRepliesPaged(tweetId: string, options?: TweetDetailPaginationOptions): Promise; + getThreadPaged(tweetId: string, options?: TweetDetailPaginationOptions): Promise; +} +export declare function withTweetDetails>(Base: TBase): Mixin; +//# sourceMappingURL=twitter-client-tweet-detail.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-tweet-detail.d.ts.map b/vendor/package/dist/lib/twitter-client-tweet-detail.d.ts.map new file mode 100644 index 0000000..a968fed --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-tweet-detail.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-tweet-detail.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-tweet-detail.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAG9F,OAAO,KAAK,EAAE,cAAc,EAAsB,YAAY,EAAa,MAAM,2BAA2B,CAAC;AAU7G,yCAAyC;AACzC,MAAM,WAAW,iBAAiB;IAChC,mDAAmD;IACnD,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,+CAA+C;AAC/C,MAAM,WAAW,4BAA6B,SAAQ,iBAAiB;IACrE,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IAChF,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAChF,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC/E,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,4BAA4B,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAChG,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,4BAA4B,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CAChG;AAED,wBAAgB,gBAAgB,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EACnF,IAAI,EAAE,KAAK,GACV,KAAK,CAAC,KAAK,EAAE,+BAA+B,CAAC,CA0a/C"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-tweet-detail.js b/vendor/package/dist/lib/twitter-client-tweet-detail.js new file mode 100644 index 0000000..e2f4160 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-tweet-detail.js @@ -0,0 +1,295 @@ +import { paginateCursor } from './paginate-cursor.js'; +import { TWITTER_API_BASE } from './twitter-client-constants.js'; +import { buildArticleFeatures, buildArticleFieldToggles, buildTweetDetailFeatures } from './twitter-client-features.js'; +import { extractArticleText, extractCursorFromInstructions, findTweetInInstructions, firstText, mapTweetResult, parseTweetsFromInstructions, } from './twitter-client-utils.js'; +export function withTweetDetails(Base) { + class TwitterClientTweetDetails extends Base { + // biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement. + constructor(...args) { + super(...args); + } + async fetchUserArticlePlainText(userId, tweetId) { + const variables = { + userId, + count: 20, + includePromotedContent: true, + withVoice: true, + withQuickPromoteEligibilityTweetFields: true, + withBirdwatchNotes: true, + withCommunity: true, + withSafetyModeUserFields: true, + withSuperFollowsUserFields: true, + withDownvotePerspective: false, + withReactionsMetadata: false, + withReactionsPerspective: false, + withSuperFollowsTweetFields: true, + withSuperFollowsReplyCount: false, + withClientEventToken: false, + }; + const params = new URLSearchParams({ + variables: JSON.stringify(variables), + features: JSON.stringify(buildArticleFeatures()), + fieldToggles: JSON.stringify(buildArticleFieldToggles()), + }); + const queryId = await this.getQueryId('UserArticlesTweets'); + const url = `${TWITTER_API_BASE}/${queryId}/UserArticlesTweets?${params.toString()}`; + try { + const response = await this.fetchWithTimeout(url, { method: 'GET', headers: this.getHeaders() }); + if (!response.ok) { + return {}; + } + const data = (await response.json()); + const instructions = data.data?.user?.result?.timeline?.timeline?.instructions ?? []; + for (const instruction of instructions) { + for (const entry of instruction.entries ?? []) { + const result = entry.content?.itemContent?.tweet_results?.result; + if (result?.rest_id !== tweetId) { + continue; + } + const articleResult = result.article?.article_results?.result; + const title = firstText(articleResult?.title, result.article?.title); + const plainText = firstText(articleResult?.plain_text, result.article?.plain_text); + return { title, plainText }; + } + } + } + catch { + return {}; + } + return {}; + } + async fetchTweetDetail(tweetId, cursor) { + const variables = { + focalTweetId: tweetId, + with_rux_injections: false, + rankingMode: 'Relevance', + includePromotedContent: true, + withCommunity: true, + withQuickPromoteEligibilityTweetFields: true, + withBirdwatchNotes: true, + withVoice: true, + ...(cursor ? { cursor } : {}), + }; + const features = { + ...buildTweetDetailFeatures(), + articles_preview_enabled: true, + articles_rest_api_enabled: true, + responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + creator_subscriptions_tweet_preview_api_enabled: true, + graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, + view_counts_everywhere_api_enabled: true, + longform_notetweets_consumption_enabled: true, + responsive_web_twitter_article_tweet_consumption_enabled: true, + freedom_of_speech_not_reach_fetch_enabled: true, + standardized_nudges_misinfo: true, + tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, + rweb_video_timestamps_enabled: true, + }; + const fieldToggles = { + ...buildArticleFieldToggles(), + withArticleRichContentState: true, + }; + const params = new URLSearchParams({ + variables: JSON.stringify(variables), + features: JSON.stringify(features), + fieldToggles: JSON.stringify(fieldToggles), + }); + try { + const parseResponse = async (response) => { + if (!response.ok) { + const text = await response.text(); + return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}` }; + } + const data = (await response.json()); + if (data.errors && data.errors.length > 0) { + // Twitter API sometimes returns partial errors (e.g., is_translatable failures) + // alongside valid tweet/thread data. Only fail if nothing useful is present. + const hasUsableData = Boolean(data.data?.tweetResult?.result || + data.data?.threaded_conversation_with_injections_v2?.instructions?.length); + if (!hasUsableData) { + return { success: false, error: data.errors.map((e) => e.message).join(', ') }; + } + } + return { success: true, data: data.data ?? {} }; + }; + let lastError; + let had404 = false; + const tryOnce = async () => { + const queryIds = await this.getTweetDetailQueryIds(); + for (const queryId of queryIds) { + const url = `${TWITTER_API_BASE}/${queryId}/TweetDetail?${params.toString()}`; + const response = await this.fetchWithTimeout(url, { + method: 'GET', + headers: this.getHeaders(), + }); + if (response.status !== 404) { + return await parseResponse(response); + } + had404 = true; + const postResponse = await this.fetchWithTimeout(`${TWITTER_API_BASE}/${queryId}/TweetDetail`, { + method: 'POST', + headers: this.getHeaders(), + body: JSON.stringify({ variables, features, queryId }), + }); + if (postResponse.status !== 404) { + return await parseResponse(postResponse); + } + lastError = 'HTTP 404'; + } + return { success: false, error: lastError ?? 'Unknown error fetching tweet detail' }; + }; + const firstAttempt = await tryOnce(); + if (firstAttempt.success) { + return firstAttempt; + } + if (had404) { + await this.refreshQueryIds(); + return await tryOnce(); + } + return firstAttempt; + } + catch (error) { + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + } + /** + * Get tweet details by ID + */ + async getTweet(tweetId, options = {}) { + const { includeRaw = false } = options; + const response = await this.fetchTweetDetail(tweetId); + if (!response.success) { + return response; + } + const tweetResult = response.data.tweetResult?.result ?? + findTweetInInstructions(response.data.threaded_conversation_with_injections_v2?.instructions, tweetId); + const mapped = mapTweetResult(tweetResult, { quoteDepth: this.quoteDepth, includeRaw }); + if (mapped) { + if (tweetResult?.article) { + const title = firstText(tweetResult.article.article_results?.result?.title, tweetResult.article.title); + const articleText = extractArticleText(tweetResult); + if (title && (!articleText || articleText.trim() === title.trim())) { + const userId = tweetResult.core?.user_results?.result?.rest_id; + if (userId) { + const fallback = await this.fetchUserArticlePlainText(userId, tweetId); + if (fallback.plainText) { + mapped.text = fallback.title ? `${fallback.title}\n\n${fallback.plainText}` : fallback.plainText; + } + } + } + } + return { success: true, tweet: mapped }; + } + return { success: false, error: 'Tweet not found in response' }; + } + /** + * Get replies to a tweet by ID + */ + async getReplies(tweetId, options = {}) { + const { includeRaw = false } = options; + const response = await this.fetchTweetDetail(tweetId); + if (!response.success) { + return response; + } + const instructions = response.data.threaded_conversation_with_injections_v2?.instructions; + const tweets = parseTweetsFromInstructions(instructions, { quoteDepth: this.quoteDepth, includeRaw }); + const replies = tweets.filter((tweet) => tweet.inReplyToStatusId === tweetId); + return { success: true, tweets: replies }; + } + /** + * Get full conversation thread for a tweet ID + */ + async getThread(tweetId, options = {}) { + const { includeRaw = false } = options; + const response = await this.fetchTweetDetail(tweetId); + if (!response.success) { + return response; + } + const instructions = response.data.threaded_conversation_with_injections_v2?.instructions; + const tweets = parseTweetsFromInstructions(instructions, { quoteDepth: this.quoteDepth, includeRaw }); + const target = tweets.find((t) => t.id === tweetId); + const rootId = target?.conversationId || tweetId; + const thread = tweets.filter((tweet) => tweet.conversationId === rootId); + thread.sort((a, b) => { + const aTime = a.createdAt ? Date.parse(a.createdAt) : 0; + const bTime = b.createdAt ? Date.parse(b.createdAt) : 0; + return aTime - bTime; + }); + return { success: true, tweets: thread }; + } + /** + * Get replies to a tweet with pagination support + */ + async getRepliesPaged(tweetId, options = {}) { + const { includeRaw = false, maxPages, pageDelayMs = 1000 } = options; + const result = await paginateCursor({ + cursor: options.cursor, + maxPages, + pageDelayMs, + sleep: async (ms) => this.sleep(ms), + getKey: (tweet) => tweet.id, + fetchPage: async (cursor) => { + const response = await this.fetchTweetDetail(tweetId, cursor); + if (!response.success) { + return response; + } + const instructions = response.data.threaded_conversation_with_injections_v2?.instructions; + const tweets = parseTweetsFromInstructions(instructions, { quoteDepth: this.quoteDepth, includeRaw }); + const replies = tweets.filter((tweet) => tweet.inReplyToStatusId === tweetId); + const pageCursor = extractCursorFromInstructions(instructions); + return { success: true, items: replies, cursor: pageCursor }; + }, + }); + if (result.success) { + return { success: true, tweets: result.items, nextCursor: result.nextCursor }; + } + if (result.items) { + return { success: false, tweets: result.items, nextCursor: result.nextCursor, error: result.error }; + } + return { success: false, error: result.error }; + } + /** + * Get full conversation thread with pagination support + */ + async getThreadPaged(tweetId, options = {}) { + const { includeRaw = false, maxPages, pageDelayMs = 1000 } = options; + let rootId; + const result = await paginateCursor({ + cursor: options.cursor, + maxPages, + pageDelayMs, + sleep: async (ms) => this.sleep(ms), + getKey: (tweet) => tweet.id, + fetchPage: async (cursor) => { + const response = await this.fetchTweetDetail(tweetId, cursor); + if (!response.success) { + return response; + } + const instructions = response.data.threaded_conversation_with_injections_v2?.instructions; + const tweets = parseTweetsFromInstructions(instructions, { quoteDepth: this.quoteDepth, includeRaw }); + if (!rootId) { + const target = tweets.find((t) => t.id === tweetId); + rootId = target?.conversationId || tweetId; + } + const threadTweets = tweets.filter((tweet) => tweet.conversationId === rootId); + const pageCursor = extractCursorFromInstructions(instructions); + return { success: true, items: threadTweets, cursor: pageCursor }; + }, + }); + const sortedTweets = (result.items ?? []).slice().sort((a, b) => { + const aTime = a.createdAt ? Date.parse(a.createdAt) : 0; + const bTime = b.createdAt ? Date.parse(b.createdAt) : 0; + return aTime - bTime; + }); + if (result.success) { + return { success: true, tweets: sortedTweets, nextCursor: result.nextCursor }; + } + if (result.items) { + return { success: false, tweets: sortedTweets, nextCursor: result.nextCursor, error: result.error }; + } + return { success: false, error: result.error }; + } + } + return TwitterClientTweetDetails; +} +//# sourceMappingURL=twitter-client-tweet-detail.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-tweet-detail.js.map b/vendor/package/dist/lib/twitter-client-tweet-detail.js.map new file mode 100644 index 0000000..4e100b3 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-tweet-detail.js.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-tweet-detail.js","sourceRoot":"","sources":["../../src/lib/twitter-client-tweet-detail.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-types.d.ts b/vendor/package/dist/lib/twitter-client-types.d.ts new file mode 100644 index 0000000..cfa7d10 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-types.d.ts @@ -0,0 +1,407 @@ +import type { TwitterCookies } from './cookies.js'; +export interface GraphqlMediaEntity { + id_str?: string; + media_url_https?: string; + type?: 'photo' | 'video' | 'animated_gif'; + url?: string; + expanded_url?: string; + sizes?: { + thumb?: { + w: number; + h: number; + resize: string; + }; + small?: { + w: number; + h: number; + resize: string; + }; + medium?: { + w: number; + h: number; + resize: string; + }; + large?: { + w: number; + h: number; + resize: string; + }; + }; + video_info?: { + duration_millis?: number; + variants?: Array<{ + bitrate?: number; + content_type?: string; + url?: string; + }>; + }; +} +export type GraphqlTweetResult = { + __typename?: string; + rest_id?: string; + legacy?: { + full_text?: string; + created_at?: string; + reply_count?: number; + retweet_count?: number; + favorite_count?: number; + conversation_id_str?: string; + in_reply_to_status_id_str?: string | null; + entities?: { + media?: GraphqlMediaEntity[]; + }; + extended_entities?: { + media?: GraphqlMediaEntity[]; + }; + }; + core?: { + user_results?: { + result?: { + rest_id?: string; + id?: string; + legacy?: { + screen_name?: string; + name?: string; + }; + core?: { + screen_name?: string; + name?: string; + }; + }; + }; + }; + note_tweet?: { + note_tweet_results?: { + result?: { + text?: string; + richtext?: { + text?: string; + }; + rich_text?: { + text?: string; + }; + content?: { + text?: string; + richtext?: { + text?: string; + }; + rich_text?: { + text?: string; + }; + }; + }; + }; + }; + article?: { + title?: string; + preview_text?: string; + article_results?: { + result?: { + title?: string; + preview_text?: string; + plain_text?: string; + text?: string; + richtext?: { + text?: string; + }; + rich_text?: { + text?: string; + }; + body?: { + text?: string; + richtext?: { + text?: string; + }; + rich_text?: { + text?: string; + }; + }; + content?: { + text?: string; + richtext?: { + text?: string; + }; + rich_text?: { + text?: string; + }; + items?: Array<{ + text?: string; + content?: { + text?: string; + richtext?: { + text?: string; + }; + rich_text?: { + text?: string; + }; + }; + }>; + }; + sections?: Array<{ + items?: Array<{ + text?: string; + content?: { + text?: string; + richtext?: { + text?: string; + }; + rich_text?: { + text?: string; + }; + }; + }>; + }>; + /** Draft.js content state for rich article content */ + content_state?: { + blocks: Array<{ + key: string; + type: string; + text: string; + data?: Record; + entityRanges?: Array<{ + key: number; + offset: number; + length: number; + }>; + inlineStyleRanges?: Array<{ + offset: number; + length: number; + style: string; + }>; + }>; + entityMap?: Array<{ + key: string; + value: { + type: string; + mutability: string; + data: Record; + }; + }> | Record; + }>; + }; + }; + }; + plain_text?: string; + text?: string; + richtext?: { + text?: string; + }; + rich_text?: { + text?: string; + }; + body?: { + text?: string; + richtext?: { + text?: string; + }; + rich_text?: { + text?: string; + }; + }; + content?: { + text?: string; + richtext?: { + text?: string; + }; + rich_text?: { + text?: string; + }; + items?: Array<{ + text?: string; + content?: { + text?: string; + richtext?: { + text?: string; + }; + rich_text?: { + text?: string; + }; + }; + }>; + }; + sections?: Array<{ + items?: Array<{ + text?: string; + content?: { + text?: string; + richtext?: { + text?: string; + }; + rich_text?: { + text?: string; + }; + }; + }>; + }>; + }; + tweet?: GraphqlTweetResult; + quoted_status_result?: { + result?: GraphqlTweetResult; + }; +}; +export type TweetResult = { + success: true; + tweetId: string; +} | { + success: false; + error: string; +}; +export type BookmarkMutationResult = { + success: true; +} | { + success: false; + error: string; +}; +export type FollowMutationResult = { + success: true; + userId?: string; + username?: string; +} | { + success: false; + error: string; +}; +export interface UploadMediaResult { + success: boolean; + mediaId?: string; + error?: string; +} +export interface TweetMedia { + type: 'photo' | 'video' | 'animated_gif'; + url: string; + previewUrl?: string; + width?: number; + height?: number; + videoUrl?: string; + durationMs?: number; +} +export interface TweetData { + id: string; + text: string; + author: { + username: string; + name: string; + }; + authorId?: string; + createdAt?: string; + replyCount?: number; + retweetCount?: number; + likeCount?: number; + conversationId?: string; + inReplyToStatusId?: string; + quotedTweet?: TweetData; + media?: TweetMedia[]; + article?: { + title: string; + previewText?: string; + }; + _raw?: GraphqlTweetResult; +} +export interface TweetWithMeta extends TweetData { + isThread: boolean; + threadPosition: 'root' | 'middle' | 'end' | 'standalone'; + hasSelfReplies: boolean; + threadRootId: string | null; +} +export interface GetTweetResult { + success: boolean; + tweet?: TweetData; + error?: string; +} +export type SearchResult = { + success: true; + tweets: TweetData[]; + /** Cursor for fetching the next page of results */ + nextCursor?: string; +} | { + success: false; + error: string; + tweets?: TweetData[]; + /** Cursor for fetching the next page of results */ + nextCursor?: string; +}; +export interface CurrentUserResult { + success: boolean; + user?: { + id: string; + username: string; + name: string; + }; + error?: string; +} +export interface TwitterUser { + id: string; + username: string; + name: string; + description?: string; + followersCount?: number; + followingCount?: number; + isBlueVerified?: boolean; + profileImageUrl?: string; + createdAt?: string; +} +export interface FollowingResult { + success: boolean; + users?: TwitterUser[]; + error?: string; + /** Cursor for fetching the next page of results */ + nextCursor?: string; +} +export interface AboutAccountProfile { + accountBasedIn?: string; + source?: string; + createdCountryAccurate?: boolean; + locationAccurate?: boolean; + learnMoreUrl?: string; +} +export interface AboutAccountResult { + success: boolean; + aboutProfile?: AboutAccountProfile; + error?: string; +} +export interface TwitterClientOptions { + cookies: TwitterCookies; + userAgent?: string; + timeoutMs?: number; + quoteDepth?: number; +} +export interface TwitterList { + id: string; + name: string; + description?: string; + memberCount?: number; + subscriberCount?: number; + isPrivate?: boolean; + createdAt?: string; + owner?: { + id: string; + username: string; + name: string; + }; +} +export interface ListsResult { + success: boolean; + lists?: TwitterList[]; + error?: string; +} +export interface CreateTweetResponse { + data?: { + create_tweet?: { + tweet_results?: { + result?: { + rest_id?: string; + legacy?: { + full_text?: string; + }; + }; + }; + }; + }; + errors?: Array<{ + message: string; + code?: number; + }>; +} +//# sourceMappingURL=twitter-client-types.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-types.d.ts.map b/vendor/package/dist/lib/twitter-client-types.d.ts.map new file mode 100644 index 0000000..05748c3 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-types.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-types.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-types.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-types.js b/vendor/package/dist/lib/twitter-client-types.js new file mode 100644 index 0000000..3277c71 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-types.js @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=twitter-client-types.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-types.js.map b/vendor/package/dist/lib/twitter-client-types.js.map new file mode 100644 index 0000000..cc4bbcc --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-types.js.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-types.js","sourceRoot":"","sources":["../../src/lib/twitter-client-types.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-user-lookup.d.ts b/vendor/package/dist/lib/twitter-client-user-lookup.d.ts new file mode 100644 index 0000000..0719e20 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-user-lookup.d.ts @@ -0,0 +1,16 @@ +import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js'; +import type { AboutAccountResult } from './twitter-client-types.js'; +/** Result of username to userId lookup */ +export interface UserLookupResult { + success: boolean; + userId?: string; + username?: string; + name?: string; + error?: string; +} +export interface TwitterClientUserLookupMethods { + getUserIdByUsername(username: string): Promise; + getUserAboutAccount(username: string): Promise; +} +export declare function withUserLookup>(Base: TBase): Mixin; +//# sourceMappingURL=twitter-client-user-lookup.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-user-lookup.d.ts.map b/vendor/package/dist/lib/twitter-client-user-lookup.d.ts.map new file mode 100644 index 0000000..b093894 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-user-lookup.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-user-lookup.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-user-lookup.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE9F,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAEpE,0CAA0C;AAC1C,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,8BAA8B;IAC7C,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACjE,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACpE;AAED,wBAAgB,cAAc,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EACjF,IAAI,EAAE,KAAK,GACV,KAAK,CAAC,KAAK,EAAE,8BAA8B,CAAC,CAyS9C"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-user-lookup.js b/vendor/package/dist/lib/twitter-client-user-lookup.js new file mode 100644 index 0000000..1701c19 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-user-lookup.js @@ -0,0 +1,224 @@ +import { normalizeHandle } from './normalize-handle.js'; +import { TWITTER_API_BASE } from './twitter-client-constants.js'; +export function withUserLookup(Base) { + class TwitterClientUserLookup extends Base { + // biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement. + constructor(...args) { + super(...args); + } + async getUserByScreenNameGraphQL(screenName) { + // UserByScreenName query IDs observed from web client + const queryIds = ['xc8f1g7BYqr6VTzTbvNlGw', 'qW5u-DAuXpMEG0zA1F7UGQ', 'sLVLhk0bGj3MVFEKTdax1w']; + const variables = { + screen_name: screenName, + withSafetyModeUserFields: true, + }; + const features = { + hidden_profile_subscriptions_enabled: true, + hidden_profile_likes_enabled: true, + rweb_tipjar_consumption_enabled: true, + responsive_web_graphql_exclude_directive_enabled: true, + verified_phone_label_enabled: false, + subscriptions_verification_info_is_identity_verified_enabled: true, + subscriptions_verification_info_verified_since_enabled: true, + highlights_tweets_tab_ui_enabled: true, + responsive_web_twitter_article_notes_tab_enabled: true, + subscriptions_feature_can_gift_premium: true, + creator_subscriptions_tweet_preview_api_enabled: true, + responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + responsive_web_graphql_timeline_navigation_enabled: true, + blue_business_profile_image_shape_enabled: true, + }; + const fieldToggles = { + withAuxiliaryUserLabels: false, + }; + const params = new URLSearchParams({ + variables: JSON.stringify(variables), + features: JSON.stringify(features), + fieldToggles: JSON.stringify(fieldToggles), + }); + let lastError; + for (const queryId of queryIds) { + const url = `${TWITTER_API_BASE}/${queryId}/UserByScreenName?${params.toString()}`; + try { + const response = await this.fetchWithTimeout(url, { + method: 'GET', + headers: this.getHeaders(), + }); + if (!response.ok) { + const text = await response.text(); + if (response.status === 404) { + // Try next query ID + lastError = `HTTP ${response.status}`; + continue; + } + lastError = `HTTP ${response.status}: ${text.slice(0, 200)}`; + continue; + } + const data = (await response.json()); + // Check for user not found + if (data.data?.user?.result?.__typename === 'UserUnavailable') { + return { success: false, error: `User @${screenName} not found or unavailable` }; + } + const userResult = data.data?.user?.result; + const userId = userResult?.rest_id; + const username = userResult?.legacy?.screen_name ?? userResult?.core?.screen_name; + const name = userResult?.legacy?.name ?? userResult?.core?.name; + if (userId && username) { + return { + success: true, + userId, + username, + name, + }; + } + if (data.errors && data.errors.length > 0) { + lastError = data.errors.map((e) => e.message).join(', '); + continue; + } + lastError = 'Could not parse user data from response'; + } + catch (error) { + lastError = error instanceof Error ? error.message : String(error); + } + } + return { success: false, error: lastError ?? 'Unknown error looking up user' }; + } + /** + * Look up a user's ID by their username/handle. + * Uses GraphQL UserByScreenName first, then falls back to REST on transient failures. + */ + async getUserIdByUsername(username) { + const cleanUsername = normalizeHandle(username); + if (!cleanUsername) { + return { success: false, error: `Invalid username: ${username}` }; + } + const graphqlResult = await this.getUserByScreenNameGraphQL(cleanUsername); + if (graphqlResult.success) { + return graphqlResult; + } + // If GraphQL definitively says user is unavailable, don't retry with REST + if (graphqlResult.error?.includes('not found or unavailable')) { + return graphqlResult; + } + // Fallback to REST API for transient GraphQL errors + const urls = [ + `https://x.com/i/api/1.1/users/show.json?screen_name=${encodeURIComponent(cleanUsername)}`, + `https://api.twitter.com/1.1/users/show.json?screen_name=${encodeURIComponent(cleanUsername)}`, + ]; + let lastError = graphqlResult.error; + for (const url of urls) { + try { + const response = await this.fetchWithTimeout(url, { + method: 'GET', + headers: this.getHeaders(), + }); + if (!response.ok) { + const text = await response.text(); + if (response.status === 404) { + return { success: false, error: `User @${cleanUsername} not found` }; + } + lastError = `HTTP ${response.status}: ${text.slice(0, 200)}`; + continue; + } + const data = (await response.json()); + const userId = data.id_str ?? (data.id ? String(data.id) : null); + if (!userId) { + lastError = 'Could not parse user ID from response'; + continue; + } + return { + success: true, + userId, + username: data.screen_name ?? cleanUsername, + name: data.name, + }; + } + catch (error) { + lastError = error instanceof Error ? error.message : String(error); + } + } + return { success: false, error: lastError ?? 'Unknown error looking up user' }; + } + async getAboutAccountQueryIds() { + const primary = await this.getQueryId('AboutAccountQuery'); + return Array.from(new Set([primary, 'zs_jFPFT78rBpXv9Z3U2YQ'])); + } + /** + * Get account origin and location information for a user. + * Returns data from Twitter's "About this account" feature. + */ + async getUserAboutAccount(username) { + const cleanUsername = normalizeHandle(username); + if (!cleanUsername) { + return { success: false, error: `Invalid username: ${username}` }; + } + const variables = { + screenName: cleanUsername, + }; + const params = new URLSearchParams({ + variables: JSON.stringify(variables), + }); + const tryOnce = async () => { + let lastError; + let had404 = false; + const queryIds = await this.getAboutAccountQueryIds(); + for (const queryId of queryIds) { + const url = `${TWITTER_API_BASE}/${queryId}/AboutAccountQuery?${params.toString()}`; + try { + const response = await this.fetchWithTimeout(url, { + method: 'GET', + headers: this.getHeaders(), + }); + if (!response.ok) { + const text = await response.text(); + if (response.status === 404) { + had404 = true; + lastError = `HTTP ${response.status}`; + continue; + } + lastError = `HTTP ${response.status}: ${text.slice(0, 200)}`; + continue; + } + const data = (await response.json()); + if (data.errors && data.errors.length > 0) { + lastError = data.errors.map((e) => e.message).join(', '); + continue; + } + const aboutProfile = data.data?.user_result_by_screen_name?.result?.about_profile; + if (!aboutProfile) { + lastError = 'Missing about_profile in response'; + continue; + } + return { + success: true, + aboutProfile: { + accountBasedIn: aboutProfile.account_based_in, + source: aboutProfile.source, + createdCountryAccurate: aboutProfile.created_country_accurate, + locationAccurate: aboutProfile.location_accurate, + learnMoreUrl: aboutProfile.learn_more_url, + }, + had404, + }; + } + catch (error) { + lastError = error instanceof Error ? error.message : String(error); + } + } + return { + success: false, + error: lastError ?? 'Unknown error fetching account details', + had404, + }; + }; + const { result } = await this.withRefreshedQueryIdsOn404(tryOnce); + if (result.success) { + return { success: true, aboutProfile: result.aboutProfile }; + } + return { success: false, error: result.error }; + } + } + return TwitterClientUserLookup; +} +//# sourceMappingURL=twitter-client-user-lookup.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-user-lookup.js.map b/vendor/package/dist/lib/twitter-client-user-lookup.js.map new file mode 100644 index 0000000..cd56c3b --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-user-lookup.js.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-user-lookup.js","sourceRoot":"","sources":["../../src/lib/twitter-client-user-lookup.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-user-tweets.d.ts b/vendor/package/dist/lib/twitter-client-user-tweets.d.ts new file mode 100644 index 0000000..a4feab2 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-user-tweets.d.ts @@ -0,0 +1,22 @@ +import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js'; +import type { SearchResult } from './twitter-client-types.js'; +/** Options for user tweets fetch methods */ +export interface UserTweetsFetchOptions { + /** Include raw GraphQL response in `_raw` field */ + includeRaw?: boolean; +} +/** Options for paginated user tweets fetch */ +export interface UserTweetsPaginationOptions extends UserTweetsFetchOptions { + /** Maximum number of pages to fetch (default: 1) */ + maxPages?: number; + /** Starting cursor for pagination (resume from previous fetch) */ + cursor?: string; + /** Delay in milliseconds between page fetches (default: 1000) */ + pageDelayMs?: number; +} +export interface TwitterClientUserTweetsMethods { + getUserTweets(userId: string, count?: number, options?: UserTweetsFetchOptions): Promise; + getUserTweetsPaged(userId: string, limit: number, options?: UserTweetsPaginationOptions): Promise; +} +export declare function withUserTweets>(Base: TBase): Mixin; +//# sourceMappingURL=twitter-client-user-tweets.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-user-tweets.d.ts.map b/vendor/package/dist/lib/twitter-client-user-tweets.d.ts.map new file mode 100644 index 0000000..f983540 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-user-tweets.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-user-tweets.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-user-tweets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAG9F,OAAO,KAAK,EAAsB,YAAY,EAAa,MAAM,2BAA2B,CAAC;AAG7F,4CAA4C;AAC5C,MAAM,WAAW,sBAAsB;IACrC,mDAAmD;IACnD,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,8CAA8C;AAC9C,MAAM,WAAW,2BAA4B,SAAQ,sBAAsB;IACzE,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,8BAA8B;IAC7C,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACvG,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,2BAA2B,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CACjH;AAED,wBAAgB,cAAc,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EACjF,IAAI,EAAE,KAAK,GACV,KAAK,CAAC,KAAK,EAAE,8BAA8B,CAAC,CAwO9C"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-user-tweets.js b/vendor/package/dist/lib/twitter-client-user-tweets.js new file mode 100644 index 0000000..5d10c8e --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-user-tweets.js @@ -0,0 +1,154 @@ +import { TWITTER_API_BASE } from './twitter-client-constants.js'; +import { buildUserTweetsFeatures } from './twitter-client-features.js'; +import { extractCursorFromInstructions, parseTweetsFromInstructions } from './twitter-client-utils.js'; +export function withUserTweets(Base) { + class TwitterClientUserTweets extends Base { + // biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement. + constructor(...args) { + super(...args); + } + async getUserTweetsQueryIds() { + const primary = await this.getQueryId('UserTweets'); + // Fallback query ID observed from web client + return Array.from(new Set([primary, 'Wms1GvIiHXAPBaCr9KblaA'])); + } + /** + * Get tweets from a user's profile timeline (single page). + */ + async getUserTweets(userId, count = 20, options = {}) { + return this.getUserTweetsPaged(userId, count, options); + } + /** + * Get tweets from a user's profile timeline with pagination support. + */ + async getUserTweetsPaged(userId, limit, options = {}) { + if (!Number.isFinite(limit) || limit <= 0) { + return { success: false, error: `Invalid limit: ${limit}` }; + } + const { includeRaw = false, maxPages, pageDelayMs = 1000 } = options; + const features = buildUserTweetsFeatures(); + const pageSize = 20; + const seen = new Set(); + const tweets = []; + let cursor = options.cursor; + let nextCursor; + let pagesFetched = 0; + const hardMaxPages = 10; + const computedMaxPages = Math.max(1, Math.ceil(limit / pageSize)); + const effectiveMaxPages = Math.min(hardMaxPages, maxPages ?? computedMaxPages); + const fetchPage = async (pageCount, pageCursor) => { + let lastError; + let had404 = false; + const queryIds = await this.getUserTweetsQueryIds(); + const variables = { + userId, + count: pageCount, + includePromotedContent: false, // Filter out ads + withQuickPromoteEligibilityTweetFields: true, + withVoice: true, + ...(pageCursor ? { cursor: pageCursor } : {}), + }; + const fieldToggles = { + withArticlePlainText: false, + }; + const params = new URLSearchParams({ + variables: JSON.stringify(variables), + features: JSON.stringify(features), + fieldToggles: JSON.stringify(fieldToggles), + }); + for (const queryId of queryIds) { + const url = `${TWITTER_API_BASE}/${queryId}/UserTweets?${params.toString()}`; + try { + const response = await this.fetchWithTimeout(url, { + method: 'GET', + headers: this.getHeaders(), + }); + if (response.status === 404) { + had404 = true; + lastError = `HTTP ${response.status}`; + continue; + } + if (!response.ok) { + const text = await response.text(); + return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}`, had404 }; + } + const data = (await response.json()); + if (data.errors && data.errors.length > 0) { + // Check for common errors + const errorMsg = data.errors.map((e) => e.message).join(', '); + if (errorMsg.includes('User has been suspended') || errorMsg.includes('User not found')) { + return { success: false, error: errorMsg, had404 }; + } + // Some errors are non-fatal if we got data + if (!data.data?.user?.result?.timeline?.timeline?.instructions) { + return { success: false, error: errorMsg, had404 }; + } + } + const instructions = data.data?.user?.result?.timeline?.timeline?.instructions; + const pageTweets = parseTweetsFromInstructions(instructions, { quoteDepth: this.quoteDepth, includeRaw }); + const pageCursorValue = extractCursorFromInstructions(instructions); + return { success: true, tweets: pageTweets, cursor: pageCursorValue, had404 }; + } + catch (error) { + lastError = error instanceof Error ? error.message : String(error); + } + } + return { success: false, error: lastError ?? 'Unknown error fetching user tweets', had404 }; + }; + const fetchWithRefresh = async (pageCount, pageCursor) => { + const firstAttempt = await fetchPage(pageCount, pageCursor); + if (firstAttempt.success) { + return firstAttempt; + } + if (firstAttempt.had404) { + await this.refreshQueryIds(); + const secondAttempt = await fetchPage(pageCount, pageCursor); + if (secondAttempt.success) { + return secondAttempt; + } + return { success: false, error: secondAttempt.error }; + } + return { success: false, error: firstAttempt.error }; + }; + while (tweets.length < limit) { + // Add delay between pages (but not before the first page) + if (pagesFetched > 0 && pageDelayMs > 0) { + await this.sleep(pageDelayMs); + } + const remaining = limit - tweets.length; + const pageCount = Math.min(pageSize, remaining); + const page = await fetchWithRefresh(pageCount, cursor); + if (!page.success) { + return { success: false, error: page.error }; + } + pagesFetched += 1; + let added = 0; + for (const tweet of page.tweets) { + if (seen.has(tweet.id)) { + continue; + } + seen.add(tweet.id); + tweets.push(tweet); + added += 1; + if (tweets.length >= limit) { + break; + } + } + const pageCursor = page.cursor; + if (!pageCursor || pageCursor === cursor || page.tweets.length === 0 || added === 0) { + nextCursor = undefined; + break; + } + if (pagesFetched >= effectiveMaxPages) { + nextCursor = pageCursor; + break; + } + cursor = pageCursor; + nextCursor = pageCursor; + } + return { success: true, tweets, nextCursor }; + } + } + return TwitterClientUserTweets; +} +//# sourceMappingURL=twitter-client-user-tweets.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-user-tweets.js.map b/vendor/package/dist/lib/twitter-client-user-tweets.js.map new file mode 100644 index 0000000..51f81da --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-user-tweets.js.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-user-tweets.js","sourceRoot":"","sources":["../../src/lib/twitter-client-user-tweets.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-users.d.ts b/vendor/package/dist/lib/twitter-client-users.d.ts new file mode 100644 index 0000000..b93fdcc --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-users.d.ts @@ -0,0 +1,9 @@ +import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js'; +import type { CurrentUserResult, FollowingResult } from './twitter-client-types.js'; +export interface TwitterClientUserMethods { + getCurrentUser(): Promise; + getFollowing(userId: string, count?: number, cursor?: string): Promise; + getFollowers(userId: string, count?: number, cursor?: string): Promise; +} +export declare function withUsers>(Base: TBase): Mixin; +//# sourceMappingURL=twitter-client-users.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-users.d.ts.map b/vendor/package/dist/lib/twitter-client-users.d.ts.map new file mode 100644 index 0000000..e81482a --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-users.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-users.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-users.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAQ9F,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAe,MAAM,2BAA2B,CAAC;AAgBjG,MAAM,WAAW,wBAAwB;IACvC,cAAc,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC7C,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IACxF,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;CACzF;AAED,wBAAgB,SAAS,CAAC,KAAK,SAAS,mBAAmB,CAAC,iBAAiB,CAAC,EAC5E,IAAI,EAAE,KAAK,GACV,KAAK,CAAC,KAAK,EAAE,wBAAwB,CAAC,CA4cxC"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-users.js b/vendor/package/dist/lib/twitter-client-users.js new file mode 100644 index 0000000..e1e931f --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-users.js @@ -0,0 +1,358 @@ +import { SETTINGS_NAME_REGEX, SETTINGS_SCREEN_NAME_REGEX, SETTINGS_USER_ID_REGEX, TWITTER_API_BASE, } from './twitter-client-constants.js'; +import { buildFollowingFeatures } from './twitter-client-features.js'; +import { extractCursorFromInstructions, parseUsersFromInstructions } from './twitter-client-utils.js'; +export function withUsers(Base) { + class TwitterClientUsers extends Base { + // biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement. + constructor(...args) { + super(...args); + } + async getFollowingQueryIds() { + const primary = await this.getQueryId('Following'); + return Array.from(new Set([primary, 'BEkNpEt5pNETESoqMsTEGA'])); + } + async getFollowersQueryIds() { + const primary = await this.getQueryId('Followers'); + return Array.from(new Set([primary, 'kuFUYP9eV1FPoEy4N-pi7w'])); + } + parseUsersFromRestResponse(users) { + return (users ?? []) + .map((u) => { + const id = typeof u.id_str === 'string' ? u.id_str : typeof u.id === 'number' ? String(u.id) : null; + const username = typeof u.screen_name === 'string' ? u.screen_name : null; + if (!id || !username) { + return null; + } + return { + id, + username, + name: typeof u.name === 'string' && u.name.length > 0 ? u.name : username, + description: typeof u.description === 'string' ? u.description : undefined, + followersCount: typeof u.followers_count === 'number' ? u.followers_count : undefined, + followingCount: typeof u.friends_count === 'number' ? u.friends_count : undefined, + isBlueVerified: typeof u.verified === 'boolean' ? u.verified : undefined, + profileImageUrl: typeof u.profile_image_url_https === 'string' ? u.profile_image_url_https : undefined, + createdAt: typeof u.created_at === 'string' ? u.created_at : undefined, + }; + }) + .filter((u) => u !== null); + } + async getFollowersViaRest(userId, count, cursor) { + const params = new URLSearchParams({ + user_id: userId, + count: String(count), + skip_status: 'true', + include_user_entities: 'false', + }); + if (cursor) { + params.set('cursor', cursor); + } + const urls = [ + `https://x.com/i/api/1.1/followers/list.json?${params.toString()}`, + `https://api.twitter.com/1.1/followers/list.json?${params.toString()}`, + ]; + let lastError; + for (const url of urls) { + try { + const response = await this.fetchWithTimeout(url, { + method: 'GET', + headers: this.getHeaders(), + }); + if (!response.ok) { + const text = await response.text(); + lastError = `HTTP ${response.status}: ${text.slice(0, 200)}`; + continue; + } + const data = (await response.json()); + const users = this.parseUsersFromRestResponse(data.users); + const nextCursor = data.next_cursor_str && data.next_cursor_str !== '0' ? data.next_cursor_str : undefined; + return { success: true, users, nextCursor }; + } + catch (error) { + lastError = error instanceof Error ? error.message : String(error); + } + } + return { success: false, error: lastError ?? 'Unknown error fetching followers' }; + } + async getFollowingViaRest(userId, count, cursor) { + const params = new URLSearchParams({ + user_id: userId, + count: String(count), + skip_status: 'true', + include_user_entities: 'false', + }); + if (cursor) { + params.set('cursor', cursor); + } + const urls = [ + `https://x.com/i/api/1.1/friends/list.json?${params.toString()}`, + `https://api.twitter.com/1.1/friends/list.json?${params.toString()}`, + ]; + let lastError; + for (const url of urls) { + try { + const response = await this.fetchWithTimeout(url, { + method: 'GET', + headers: this.getHeaders(), + }); + if (!response.ok) { + const text = await response.text(); + lastError = `HTTP ${response.status}: ${text.slice(0, 200)}`; + continue; + } + const data = (await response.json()); + const users = this.parseUsersFromRestResponse(data.users); + const nextCursor = data.next_cursor_str && data.next_cursor_str !== '0' ? data.next_cursor_str : undefined; + return { success: true, users, nextCursor }; + } + catch (error) { + lastError = error instanceof Error ? error.message : String(error); + } + } + return { success: false, error: lastError ?? 'Unknown error fetching following' }; + } + /** + * Fetch the account associated with the current cookies + */ + async getCurrentUser() { + const candidateUrls = [ + 'https://x.com/i/api/account/settings.json', + 'https://api.twitter.com/1.1/account/settings.json', + 'https://x.com/i/api/account/verify_credentials.json?skip_status=true&include_entities=false', + 'https://api.twitter.com/1.1/account/verify_credentials.json?skip_status=true&include_entities=false', + ]; + let lastError; + for (const url of candidateUrls) { + try { + const response = await this.fetchWithTimeout(url, { + method: 'GET', + headers: this.getHeaders(), + }); + if (!response.ok) { + const text = await response.text(); + lastError = `HTTP ${response.status}: ${text.slice(0, 200)}`; + continue; + } + // biome-ignore lint/suspicious/noExplicitAny: Twitter API response is dynamic here + let data; + try { + data = await response.json(); + } + catch (error) { + lastError = error instanceof Error ? error.message : String(error); + continue; + } + const username = typeof data?.screen_name === 'string' + ? data.screen_name + : typeof data?.user?.screen_name === 'string' + ? data.user.screen_name + : null; + const name = typeof data?.name === 'string' + ? data.name + : typeof data?.user?.name === 'string' + ? data.user.name + : (username ?? ''); + const userId = typeof data?.user_id === 'string' + ? data.user_id + : typeof data?.user_id_str === 'string' + ? data.user_id_str + : typeof data?.user?.id_str === 'string' + ? data.user.id_str + : typeof data?.user?.id === 'string' + ? data.user.id + : null; + if (username && userId) { + this.clientUserId = userId; + return { + success: true, + user: { + id: userId, + username, + name: name || username, + }, + }; + } + lastError = 'Could not determine current user from response'; + } + catch (error) { + lastError = error instanceof Error ? error.message : String(error); + } + } + // Fallback: scrape the authenticated settings page (HTML) for screen_name/user_id + const profilePages = ['https://x.com/settings/account', 'https://twitter.com/settings/account']; + for (const page of profilePages) { + try { + const response = await this.fetchWithTimeout(page, { + headers: { + cookie: this.cookieHeader, + 'user-agent': this.userAgent, + }, + }); + if (!response.ok) { + lastError = `HTTP ${response.status} (settings page)`; + continue; + } + const html = await response.text(); + const usernameMatch = SETTINGS_SCREEN_NAME_REGEX.exec(html); + const idMatch = SETTINGS_USER_ID_REGEX.exec(html); + const nameMatch = SETTINGS_NAME_REGEX.exec(html); + const username = usernameMatch?.[1]; + const userId = idMatch?.[1]; + const name = nameMatch?.[1]?.replace(/\\"/g, '"'); + if (username && userId) { + return { + success: true, + user: { + id: userId, + username, + name: name || username, + }, + }; + } + lastError = 'Could not parse settings page for user info'; + } + catch (error) { + lastError = error instanceof Error ? error.message : String(error); + } + } + return { + success: false, + error: lastError ?? 'Unknown error fetching current user', + }; + } + /** + * Get users that a user is following + */ + async getFollowing(userId, count = 20, cursor) { + const variables = { + userId, + count, + includePromotedContent: false, + }; + if (cursor) { + variables.cursor = cursor; + } + const features = buildFollowingFeatures(); + const params = new URLSearchParams({ + variables: JSON.stringify(variables), + features: JSON.stringify(features), + }); + const tryOnce = async () => { + let lastError; + let had404 = false; + const queryIds = await this.getFollowingQueryIds(); + for (const queryId of queryIds) { + const url = `${TWITTER_API_BASE}/${queryId}/Following?${params.toString()}`; + try { + const response = await this.fetchWithTimeout(url, { + method: 'GET', + headers: this.getHeaders(), + }); + if (response.status === 404) { + had404 = true; + lastError = `HTTP ${response.status}`; + continue; + } + if (!response.ok) { + const text = await response.text(); + return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}`, had404 }; + } + const data = (await response.json()); + if (data.errors && data.errors.length > 0) { + return { success: false, error: data.errors.map((e) => e.message).join(', '), had404 }; + } + const instructions = data.data?.user?.result?.timeline?.timeline?.instructions; + const users = parseUsersFromInstructions(instructions); + const nextCursor = extractCursorFromInstructions(instructions); + return { success: true, users, nextCursor, had404 }; + } + catch (error) { + lastError = error instanceof Error ? error.message : String(error); + } + } + return { success: false, error: lastError ?? 'Unknown error fetching following', had404 }; + }; + const { result, refreshed } = await this.withRefreshedQueryIdsOn404(tryOnce); + if (result.success) { + return { success: true, users: result.users, nextCursor: result.nextCursor }; + } + if (refreshed) { + // GraphQL Following can also return 404 (queryId churn / endpoint flakiness). + // Fallback to the internal v1.1 REST endpoint used by the web client (cookie-auth; no dev API key). + const restAttempt = await this.getFollowingViaRest(userId, count, cursor); + if (restAttempt.success) { + return restAttempt; + } + } + return { success: false, error: result.error }; + } + /** + * Get users that follow a user + */ + async getFollowers(userId, count = 20, cursor) { + const variables = { + userId, + count, + includePromotedContent: false, + }; + if (cursor) { + variables.cursor = cursor; + } + const features = buildFollowingFeatures(); + const params = new URLSearchParams({ + variables: JSON.stringify(variables), + features: JSON.stringify(features), + }); + const tryOnce = async () => { + let lastError; + let had404 = false; + const queryIds = await this.getFollowersQueryIds(); + for (const queryId of queryIds) { + const url = `${TWITTER_API_BASE}/${queryId}/Followers?${params.toString()}`; + try { + const response = await this.fetchWithTimeout(url, { + method: 'GET', + headers: this.getHeaders(), + }); + if (response.status === 404) { + had404 = true; + lastError = `HTTP ${response.status}`; + continue; + } + if (!response.ok) { + const text = await response.text(); + return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}`, had404 }; + } + const data = (await response.json()); + if (data.errors && data.errors.length > 0) { + return { success: false, error: data.errors.map((e) => e.message).join(', '), had404 }; + } + const instructions = data.data?.user?.result?.timeline?.timeline?.instructions; + const users = parseUsersFromInstructions(instructions); + const nextCursor = extractCursorFromInstructions(instructions); + return { success: true, users, nextCursor, had404 }; + } + catch (error) { + lastError = error instanceof Error ? error.message : String(error); + } + } + return { success: false, error: lastError ?? 'Unknown error fetching followers', had404 }; + }; + const { result, refreshed } = await this.withRefreshedQueryIdsOn404(tryOnce); + if (result.success) { + return { success: true, users: result.users, nextCursor: result.nextCursor }; + } + if (refreshed) { + // GraphQL Followers regularly returns 404 (queryId churn / endpoint flakiness). + // Fallback to the internal v1.1 REST endpoint used by the web client (cookie-auth; no dev API key). + const restAttempt = await this.getFollowersViaRest(userId, count, cursor); + if (restAttempt.success) { + return restAttempt; + } + } + return { success: false, error: result.error }; + } + } + return TwitterClientUsers; +} +//# sourceMappingURL=twitter-client-users.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-users.js.map b/vendor/package/dist/lib/twitter-client-users.js.map new file mode 100644 index 0000000..b99d6e7 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-users.js.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-users.js","sourceRoot":"","sources":["../../src/lib/twitter-client-users.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-utils.d.ts b/vendor/package/dist/lib/twitter-client-utils.d.ts new file mode 100644 index 0000000..c41d2f6 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-utils.d.ts @@ -0,0 +1,173 @@ +import type { GraphqlTweetResult, TweetData, TweetMedia, TwitterUser } from './twitter-client-types.js'; +export declare function normalizeQuoteDepth(value?: number): number; +export declare function firstText(...values: Array): string | undefined; +export declare function collectTextFields(value: unknown, keys: Set, output: string[]): void; +export declare function uniqueOrdered(values: string[]): string[]; +/** Inline style range for text formatting (Bold, Italic, etc.) */ +interface InlineStyleRange { + offset: number; + length: number; + style: string; +} +/** Entity range linking a portion of text to an entity in entityMap */ +interface EntityRange { + key: number; + offset: number; + length: number; +} +/** A content block in Draft.js format */ +interface ContentBlock { + key: string; + type: string; + text: string; + data?: { + mentions?: Array<{ + fromIndex: number; + toIndex: number; + text: string; + }>; + }; + entityRanges?: EntityRange[]; + inlineStyleRanges?: InlineStyleRange[]; +} +/** Entity data for different entity types */ +interface EntityValue { + type: string; + mutability: string; + data: { + markdown?: string; + url?: string; + tweetId?: string; + }; +} +/** Entity map entry */ +interface EntityMapEntry { + key: string; + value: EntityValue; +} +/** Draft.js content state structure */ +interface ContentState { + blocks: ContentBlock[]; + entityMap?: Array | Record; +} +/** + * Renders a Draft.js content_state into readable markdown/text format. + * Handles blocks (paragraphs, headers, lists) and entities (code blocks, links, tweets, dividers). + */ +export declare function renderContentState(contentState: ContentState | undefined): string | undefined; +export declare function extractArticleText(result: GraphqlTweetResult | undefined): string | undefined; +export declare function extractNoteTweetText(result: GraphqlTweetResult | undefined): string | undefined; +export declare function extractTweetText(result: GraphqlTweetResult | undefined): string | undefined; +export declare function extractArticleMetadata(result: GraphqlTweetResult | undefined): { + title: string; + previewText?: string; +} | undefined; +export declare function extractMedia(result: GraphqlTweetResult | undefined): TweetMedia[] | undefined; +export declare function unwrapTweetResult(result: GraphqlTweetResult | undefined): GraphqlTweetResult | undefined; +export interface MapTweetResultOptions { + quoteDepth: number; + includeRaw?: boolean; +} +export declare function mapTweetResult(result: GraphqlTweetResult | undefined, quoteDepthOrOptions: number | MapTweetResultOptions): TweetData | undefined; +export declare function findTweetInInstructions(instructions: Array<{ + entries?: Array<{ + content?: { + itemContent?: { + tweet_results?: { + result?: GraphqlTweetResult; + }; + }; + }; + }>; +}> | undefined, tweetId: string): GraphqlTweetResult | undefined; +export declare function collectTweetResultsFromEntry(entry: { + content?: { + itemContent?: { + tweet_results?: { + result?: GraphqlTweetResult; + }; + }; + item?: { + itemContent?: { + tweet_results?: { + result?: GraphqlTweetResult; + }; + }; + }; + items?: Array<{ + item?: { + itemContent?: { + tweet_results?: { + result?: GraphqlTweetResult; + }; + }; + }; + itemContent?: { + tweet_results?: { + result?: GraphqlTweetResult; + }; + }; + content?: { + itemContent?: { + tweet_results?: { + result?: GraphqlTweetResult; + }; + }; + }; + }>; + }; +}): GraphqlTweetResult[]; +export interface ParseTweetsOptions { + quoteDepth: number; + includeRaw?: boolean; +} +export declare function parseTweetsFromInstructions(instructions: Array<{ + entries?: Array<{ + content?: { + itemContent?: { + tweet_results?: { + result?: GraphqlTweetResult; + }; + }; + item?: { + itemContent?: { + tweet_results?: { + result?: GraphqlTweetResult; + }; + }; + }; + items?: Array<{ + item?: { + itemContent?: { + tweet_results?: { + result?: GraphqlTweetResult; + }; + }; + }; + itemContent?: { + tweet_results?: { + result?: GraphqlTweetResult; + }; + }; + content?: { + itemContent?: { + tweet_results?: { + result?: GraphqlTweetResult; + }; + }; + }; + }>; + }; + }>; +}> | undefined, quoteDepthOrOptions: number | ParseTweetsOptions): TweetData[]; +export declare function extractCursorFromInstructions(instructions: Array<{ + entries?: Array<{ + content?: unknown; + }>; +}> | undefined, cursorType?: string): string | undefined; +export declare function parseUsersFromInstructions(instructions: Array<{ + type?: string; + entries?: Array; +}> | undefined): TwitterUser[]; +export {}; +//# sourceMappingURL=twitter-client-utils.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-utils.d.ts.map b/vendor/package/dist/lib/twitter-client-utils.d.ts.map new file mode 100644 index 0000000..72c881b --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-utils.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-utils.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client-utils.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-utils.js b/vendor/package/dist/lib/twitter-client-utils.js new file mode 100644 index 0000000..b6e869d --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-utils.js @@ -0,0 +1,511 @@ +export function normalizeQuoteDepth(value) { + if (value === undefined || value === null) { + return 1; + } + if (!Number.isFinite(value)) { + return 1; + } + return Math.max(0, Math.floor(value)); +} +export function firstText(...values) { + for (const value of values) { + if (typeof value === 'string') { + const trimmed = value.trim(); + if (trimmed) { + return trimmed; + } + } + } + return undefined; +} +export function collectTextFields(value, keys, output) { + if (!value) { + return; + } + if (typeof value === 'string') { + return; + } + if (Array.isArray(value)) { + for (const item of value) { + collectTextFields(item, keys, output); + } + return; + } + if (typeof value === 'object') { + for (const [key, nested] of Object.entries(value)) { + if (keys.has(key)) { + if (typeof nested === 'string') { + const trimmed = nested.trim(); + if (trimmed) { + output.push(trimmed); + } + continue; + } + } + collectTextFields(nested, keys, output); + } + } +} +export function uniqueOrdered(values) { + const seen = new Set(); + const result = []; + for (const value of values) { + if (seen.has(value)) { + continue; + } + seen.add(value); + result.push(value); + } + return result; +} +/** + * Renders a Draft.js content_state into readable markdown/text format. + * Handles blocks (paragraphs, headers, lists) and entities (code blocks, links, tweets, dividers). + */ +export function renderContentState(contentState) { + if (!contentState?.blocks || contentState.blocks.length === 0) { + return undefined; + } + // Build entity lookup map from array/object formats + const entityMap = new Map(); + const rawEntityMap = contentState.entityMap ?? []; + if (Array.isArray(rawEntityMap)) { + for (const entry of rawEntityMap) { + const key = Number.parseInt(entry.key, 10); + if (!Number.isNaN(key)) { + entityMap.set(key, entry.value); + } + } + } + else { + for (const [key, value] of Object.entries(rawEntityMap)) { + const keyNumber = Number.parseInt(key, 10); + if (!Number.isNaN(keyNumber)) { + entityMap.set(keyNumber, value); + } + } + } + const outputLines = []; + let orderedListCounter = 0; + let previousBlockType; + for (const block of contentState.blocks) { + // Reset ordered list counter when leaving ordered list context + if (block.type !== 'ordered-list-item' && previousBlockType === 'ordered-list-item') { + orderedListCounter = 0; + } + switch (block.type) { + case 'unstyled': { + // Plain paragraph - just output text with any inline formatting + const text = renderBlockText(block, entityMap); + if (text) { + outputLines.push(text); + } + break; + } + case 'header-one': { + const text = renderBlockText(block, entityMap); + if (text) { + outputLines.push(`# ${text}`); + } + break; + } + case 'header-two': { + const text = renderBlockText(block, entityMap); + if (text) { + outputLines.push(`## ${text}`); + } + break; + } + case 'header-three': { + const text = renderBlockText(block, entityMap); + if (text) { + outputLines.push(`### ${text}`); + } + break; + } + case 'unordered-list-item': { + const text = renderBlockText(block, entityMap); + if (text) { + outputLines.push(`- ${text}`); + } + break; + } + case 'ordered-list-item': { + orderedListCounter++; + const text = renderBlockText(block, entityMap); + if (text) { + outputLines.push(`${orderedListCounter}. ${text}`); + } + break; + } + case 'blockquote': { + const text = renderBlockText(block, entityMap); + if (text) { + outputLines.push(`> ${text}`); + } + break; + } + case 'atomic': { + // Atomic blocks are placeholders for embedded entities + const entityContent = renderAtomicBlock(block, entityMap); + if (entityContent) { + outputLines.push(entityContent); + } + break; + } + default: { + // Fallback: just output the text + const text = renderBlockText(block, entityMap); + if (text) { + outputLines.push(text); + } + } + } + previousBlockType = block.type; + } + const result = outputLines.join('\n\n'); + return result.trim() || undefined; +} +/** + * Renders text content of a block, applying inline link entities. + */ +function renderBlockText(block, entityMap) { + let text = block.text; + // Handle LINK entities by appending URL in markdown format + // Process in reverse order to not mess up offsets + const linkRanges = (block.entityRanges ?? []) + .filter((range) => { + const entity = entityMap.get(range.key); + return entity?.type === 'LINK' && entity.data.url; + }) + .sort((a, b) => b.offset - a.offset); + for (const range of linkRanges) { + const entity = entityMap.get(range.key); + if (entity?.data.url) { + const linkText = text.slice(range.offset, range.offset + range.length); + const markdownLink = `[${linkText}](${entity.data.url})`; + text = text.slice(0, range.offset) + markdownLink + text.slice(range.offset + range.length); + } + } + return text.trim(); +} +/** + * Renders an atomic block by looking up its entity and returning appropriate content. + */ +function renderAtomicBlock(block, entityMap) { + const entityRanges = block.entityRanges ?? []; + if (entityRanges.length === 0) { + return undefined; + } + const entityKey = entityRanges[0].key; + const entity = entityMap.get(entityKey); + if (!entity) { + return undefined; + } + switch (entity.type) { + case 'MARKDOWN': + // Code blocks and other markdown content - output as-is + return entity.data.markdown?.trim(); + case 'DIVIDER': + return '---'; + case 'TWEET': + if (entity.data.tweetId) { + return `[Embedded Tweet: https://x.com/i/status/${entity.data.tweetId}]`; + } + return undefined; + case 'LINK': + if (entity.data.url) { + return `[Link: ${entity.data.url}]`; + } + return undefined; + case 'IMAGE': + // Images in atomic blocks - could extract URL if available + return '[Image]'; + default: + return undefined; + } +} +export function extractArticleText(result) { + const article = result?.article; + if (!article) { + return undefined; + } + const articleResult = article.article_results?.result ?? article; + if (process.env.BIRD_DEBUG_ARTICLE === '1') { + console.error('[bird][debug][article] payload:', JSON.stringify({ + rest_id: result?.rest_id, + article: articleResult, + note_tweet: result?.note_tweet?.note_tweet_results?.result ?? null, + }, null, 2)); + } + const title = firstText(articleResult.title, article.title); + // Try to render from rich content_state first (Draft.js format with blocks + entityMap) + // This preserves code blocks, embedded tweets, markdown, etc. + const contentState = article.article_results?.result?.content_state; + const richBody = renderContentState(contentState); + if (richBody) { + // Rich content found - prepend title if not already included + if (title) { + const normalizedTitle = title.trim(); + const trimmedBody = richBody.trimStart(); + const headingMatches = [`# ${normalizedTitle}`, `## ${normalizedTitle}`, `### ${normalizedTitle}`]; + const hasTitle = trimmedBody === normalizedTitle || + trimmedBody.startsWith(`${normalizedTitle}\n`) || + headingMatches.some((heading) => trimmedBody.startsWith(heading)); + if (!hasTitle) { + return `${title}\n\n${richBody}`; + } + } + return richBody; + } + // Fallback to plain text extraction for articles without rich content_state + let body = firstText(articleResult.plain_text, article.plain_text, articleResult.body?.text, articleResult.body?.richtext?.text, articleResult.body?.rich_text?.text, articleResult.content?.text, articleResult.content?.richtext?.text, articleResult.content?.rich_text?.text, articleResult.text, articleResult.richtext?.text, articleResult.rich_text?.text, article.body?.text, article.body?.richtext?.text, article.body?.rich_text?.text, article.content?.text, article.content?.richtext?.text, article.content?.rich_text?.text, article.text, article.richtext?.text, article.rich_text?.text); + if (body && title && body.trim() === title.trim()) { + body = undefined; + } + if (!body) { + const collected = []; + collectTextFields(articleResult, new Set(['text', 'title']), collected); + collectTextFields(article, new Set(['text', 'title']), collected); + const unique = uniqueOrdered(collected); + const filtered = title ? unique.filter((value) => value !== title) : unique; + if (filtered.length > 0) { + body = filtered.join('\n\n'); + } + } + if (title && body && !body.startsWith(title)) { + return `${title}\n\n${body}`; + } + return body ?? title; +} +export function extractNoteTweetText(result) { + const note = result?.note_tweet?.note_tweet_results?.result; + if (!note) { + return undefined; + } + return firstText(note.text, note.richtext?.text, note.rich_text?.text, note.content?.text, note.content?.richtext?.text, note.content?.rich_text?.text); +} +export function extractTweetText(result) { + return extractArticleText(result) ?? extractNoteTweetText(result) ?? firstText(result?.legacy?.full_text); +} +export function extractArticleMetadata(result) { + const article = result?.article; + if (!article) { + return undefined; + } + const articleResult = article.article_results?.result ?? article; + const title = firstText(articleResult.title, article.title); + if (!title) { + return undefined; + } + // preview_text is available in home timeline responses + const previewText = firstText(articleResult.preview_text, article.preview_text); + return { title, previewText }; +} +export function extractMedia(result) { + // Prefer extended_entities (has video info), fall back to entities + const rawMedia = result?.legacy?.extended_entities?.media ?? result?.legacy?.entities?.media; + if (!rawMedia || rawMedia.length === 0) { + return undefined; + } + const media = []; + for (const item of rawMedia) { + if (!item.type || !item.media_url_https) { + continue; + } + const mediaItem = { + type: item.type, + url: item.media_url_https, + }; + // Get dimensions from largest available size + const sizes = item.sizes; + if (sizes?.large) { + mediaItem.width = sizes.large.w; + mediaItem.height = sizes.large.h; + } + else if (sizes?.medium) { + mediaItem.width = sizes.medium.w; + mediaItem.height = sizes.medium.h; + } + // For thumbnails/previews + if (sizes?.small) { + mediaItem.previewUrl = `${item.media_url_https}:small`; + } + // Extract video URL for video/animated_gif + if ((item.type === 'video' || item.type === 'animated_gif') && item.video_info?.variants) { + // Prefer highest bitrate MP4, fall back to first MP4 when bitrate is missing. + const mp4Variants = item.video_info.variants.filter((v) => v.content_type === 'video/mp4' && typeof v.url === 'string'); + const mp4WithBitrate = mp4Variants + .filter((v) => typeof v.bitrate === 'number') + .sort((a, b) => b.bitrate - a.bitrate); + const selectedVariant = mp4WithBitrate[0] ?? mp4Variants[0]; + if (selectedVariant) { + mediaItem.videoUrl = selectedVariant.url; + } + if (typeof item.video_info.duration_millis === 'number') { + mediaItem.durationMs = item.video_info.duration_millis; + } + } + media.push(mediaItem); + } + return media.length > 0 ? media : undefined; +} +export function unwrapTweetResult(result) { + if (!result) { + return undefined; + } + if (result.tweet) { + return result.tweet; + } + return result; +} +export function mapTweetResult(result, quoteDepthOrOptions) { + const options = typeof quoteDepthOrOptions === 'number' ? { quoteDepth: quoteDepthOrOptions } : quoteDepthOrOptions; + const { quoteDepth, includeRaw = false } = options; + const userResult = result?.core?.user_results?.result; + const userLegacy = userResult?.legacy; + const userCore = userResult?.core; + const username = userLegacy?.screen_name ?? userCore?.screen_name; + const name = userLegacy?.name ?? userCore?.name ?? username; + const userId = userResult?.rest_id; + if (!result?.rest_id || !username) { + return undefined; + } + const text = extractTweetText(result); + if (!text) { + return undefined; + } + let quotedTweet; + if (quoteDepth > 0) { + const quotedResult = unwrapTweetResult(result.quoted_status_result?.result); + if (quotedResult) { + quotedTweet = mapTweetResult(quotedResult, { quoteDepth: quoteDepth - 1, includeRaw }); + } + } + const media = extractMedia(result); + const article = extractArticleMetadata(result); + const tweetData = { + id: result.rest_id, + text, + createdAt: result.legacy?.created_at, + replyCount: result.legacy?.reply_count, + retweetCount: result.legacy?.retweet_count, + likeCount: result.legacy?.favorite_count, + conversationId: result.legacy?.conversation_id_str, + inReplyToStatusId: result.legacy?.in_reply_to_status_id_str ?? undefined, + author: { + username, + name: name || username, + }, + authorId: userId, + quotedTweet, + media, + article, + }; + if (includeRaw) { + tweetData._raw = result; + } + return tweetData; +} +export function findTweetInInstructions(instructions, tweetId) { + if (!instructions) { + return undefined; + } + for (const instruction of instructions) { + for (const entry of instruction.entries || []) { + const result = entry.content?.itemContent?.tweet_results?.result; + if (result?.rest_id === tweetId) { + return result; + } + } + } + return undefined; +} +export function collectTweetResultsFromEntry(entry) { + const results = []; + const pushResult = (result) => { + if (result?.rest_id) { + results.push(result); + } + }; + const content = entry.content; + pushResult(content?.itemContent?.tweet_results?.result); + pushResult(content?.item?.itemContent?.tweet_results?.result); + for (const item of content?.items ?? []) { + pushResult(item?.item?.itemContent?.tweet_results?.result); + pushResult(item?.itemContent?.tweet_results?.result); + pushResult(item?.content?.itemContent?.tweet_results?.result); + } + return results; +} +export function parseTweetsFromInstructions(instructions, quoteDepthOrOptions) { + const options = typeof quoteDepthOrOptions === 'number' ? { quoteDepth: quoteDepthOrOptions } : quoteDepthOrOptions; + const { quoteDepth, includeRaw = false } = options; + const tweets = []; + const seen = new Set(); + for (const instruction of instructions ?? []) { + for (const entry of instruction.entries ?? []) { + const results = collectTweetResultsFromEntry(entry); + for (const result of results) { + const mapped = mapTweetResult(result, { quoteDepth, includeRaw }); + if (!mapped || seen.has(mapped.id)) { + continue; + } + seen.add(mapped.id); + tweets.push(mapped); + } + } + } + return tweets; +} +export function extractCursorFromInstructions(instructions, cursorType = 'Bottom') { + for (const instruction of instructions ?? []) { + for (const entry of instruction.entries ?? []) { + const content = entry.content; + if (content?.cursorType === cursorType && typeof content.value === 'string' && content.value.length > 0) { + return content.value; + } + } + } + return undefined; +} +export function parseUsersFromInstructions(instructions) { + if (!instructions) { + return []; + } + const users = []; + for (const instruction of instructions) { + if (!instruction.entries) { + continue; + } + for (const entry of instruction.entries) { + const content = entry?.content; + const rawUserResult = content?.itemContent?.user_results?.result; + const userResult = rawUserResult?.__typename === 'UserWithVisibilityResults' && rawUserResult.user + ? rawUserResult.user + : rawUserResult; + if (!userResult || userResult.__typename !== 'User') { + continue; + } + const legacy = userResult.legacy; + const core = userResult.core; + const username = legacy?.screen_name ?? core?.screen_name; + if (!userResult.rest_id || !username) { + continue; + } + users.push({ + id: userResult.rest_id, + username, + name: legacy?.name ?? core?.name ?? username, + description: legacy?.description, + followersCount: legacy?.followers_count, + followingCount: legacy?.friends_count, + isBlueVerified: userResult.is_blue_verified, + profileImageUrl: legacy?.profile_image_url_https ?? userResult.avatar?.image_url, + createdAt: legacy?.created_at ?? core?.created_at, + }); + } + } + return users; +} +//# sourceMappingURL=twitter-client-utils.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client-utils.js.map b/vendor/package/dist/lib/twitter-client-utils.js.map new file mode 100644 index 0000000..3c7eb80 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client-utils.js.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client-utils.js","sourceRoot":"","sources":["../../src/lib/twitter-client-utils.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client.d.ts b/vendor/package/dist/lib/twitter-client.d.ts new file mode 100644 index 0000000..8a743cb --- /dev/null +++ b/vendor/package/dist/lib/twitter-client.d.ts @@ -0,0 +1,23 @@ +import type { AbstractConstructor } from './twitter-client-base.js'; +import { TwitterClientBase } from './twitter-client-base.js'; +import { type TwitterClientBookmarkMethods } from './twitter-client-bookmarks.js'; +import { type TwitterClientEngagementMethods } from './twitter-client-engagement.js'; +import { type TwitterClientFollowMethods } from './twitter-client-follow.js'; +import { type TwitterClientHomeMethods } from './twitter-client-home.js'; +import { type TwitterClientListMethods } from './twitter-client-lists.js'; +import { type TwitterClientMediaMethods } from './twitter-client-media.js'; +import { type TwitterClientNewsMethods } from './twitter-client-news.js'; +import { type TwitterClientPostingMethods } from './twitter-client-posting.js'; +import { type TwitterClientSearchMethods } from './twitter-client-search.js'; +import { type TwitterClientTimelineMethods } from './twitter-client-timelines.js'; +import { type TwitterClientTweetDetailMethods } from './twitter-client-tweet-detail.js'; +import { type TwitterClientUserLookupMethods } from './twitter-client-user-lookup.js'; +import { type TwitterClientUserTweetsMethods } from './twitter-client-user-tweets.js'; +import { type TwitterClientUserMethods } from './twitter-client-users.js'; +type TwitterClientInstance = TwitterClientBase & TwitterClientBookmarkMethods & TwitterClientEngagementMethods & TwitterClientFollowMethods & TwitterClientHomeMethods & TwitterClientListMethods & TwitterClientMediaMethods & TwitterClientNewsMethods & TwitterClientPostingMethods & TwitterClientSearchMethods & TwitterClientTimelineMethods & TwitterClientTweetDetailMethods & TwitterClientUserMethods & TwitterClientUserLookupMethods & TwitterClientUserTweetsMethods; +declare const MixedTwitterClient: AbstractConstructor; +export declare class TwitterClient extends MixedTwitterClient { +} +export type { NewsFetchOptions, NewsItem, NewsResult } from './twitter-client-news.js'; +export type { BookmarkMutationResult, CurrentUserResult, FollowingResult, FollowMutationResult, GetTweetResult, ListsResult, SearchResult, TweetData, TweetResult, TwitterClientOptions, TwitterList, TwitterUser, UploadMediaResult, } from './twitter-client-types.js'; +//# sourceMappingURL=twitter-client.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client.d.ts.map b/vendor/package/dist/lib/twitter-client.d.ts.map new file mode 100644 index 0000000..07af398 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client.d.ts","sourceRoot":"","sources":["../../src/lib/twitter-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,KAAK,4BAA4B,EAAiB,MAAM,+BAA+B,CAAC;AACjG,OAAO,EAAE,KAAK,8BAA8B,EAAkB,MAAM,gCAAgC,CAAC;AACrG,OAAO,EAAE,KAAK,0BAA0B,EAAc,MAAM,4BAA4B,CAAC;AACzF,OAAO,EAAE,KAAK,wBAAwB,EAAY,MAAM,0BAA0B,CAAC;AACnF,OAAO,EAAE,KAAK,wBAAwB,EAAa,MAAM,2BAA2B,CAAC;AACrF,OAAO,EAAE,KAAK,yBAAyB,EAAa,MAAM,2BAA2B,CAAC;AACtF,OAAO,EAAE,KAAK,wBAAwB,EAAY,MAAM,0BAA0B,CAAC;AACnF,OAAO,EAAE,KAAK,2BAA2B,EAAe,MAAM,6BAA6B,CAAC;AAC5F,OAAO,EAAE,KAAK,0BAA0B,EAAc,MAAM,4BAA4B,CAAC;AACzF,OAAO,EAAE,KAAK,4BAA4B,EAAiB,MAAM,+BAA+B,CAAC;AACjG,OAAO,EAAE,KAAK,+BAA+B,EAAoB,MAAM,kCAAkC,CAAC;AAC1G,OAAO,EAAE,KAAK,8BAA8B,EAAkB,MAAM,iCAAiC,CAAC;AACtG,OAAO,EAAE,KAAK,8BAA8B,EAAkB,MAAM,iCAAiC,CAAC;AACtG,OAAO,EAAE,KAAK,wBAAwB,EAAa,MAAM,2BAA2B,CAAC;AAErF,KAAK,qBAAqB,GAAG,iBAAiB,GAC5C,4BAA4B,GAC5B,8BAA8B,GAC9B,0BAA0B,GAC1B,wBAAwB,GACxB,wBAAwB,GACxB,yBAAyB,GACzB,wBAAwB,GACxB,2BAA2B,GAC3B,0BAA0B,GAC1B,4BAA4B,GAC5B,+BAA+B,GAC/B,wBAAwB,GACxB,8BAA8B,GAC9B,8BAA8B,CAAC;AAIjC,QAAA,MAAM,kBAAkB,EAgBnB,mBAAmB,CAAC,qBAAqB,CAAC,CAAC;AAEhD,qBAAa,aAAc,SAAQ,kBAAkB;CAAG;AAExD,YAAY,EAAE,gBAAgB,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACvF,YAAY,EACV,sBAAsB,EACtB,iBAAiB,EACjB,eAAe,EACf,oBAAoB,EACpB,cAAc,EACd,WAAW,EACX,YAAY,EACZ,SAAS,EACT,WAAW,EACX,oBAAoB,EACpB,WAAW,EACX,WAAW,EACX,iBAAiB,GAClB,MAAM,2BAA2B,CAAC"} \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client.js b/vendor/package/dist/lib/twitter-client.js new file mode 100644 index 0000000..a80c2c1 --- /dev/null +++ b/vendor/package/dist/lib/twitter-client.js @@ -0,0 +1,21 @@ +import { TwitterClientBase } from './twitter-client-base.js'; +import { withBookmarks } from './twitter-client-bookmarks.js'; +import { withEngagement } from './twitter-client-engagement.js'; +import { withFollow } from './twitter-client-follow.js'; +import { withHome } from './twitter-client-home.js'; +import { withLists } from './twitter-client-lists.js'; +import { withMedia } from './twitter-client-media.js'; +import { withNews } from './twitter-client-news.js'; +import { withPosting } from './twitter-client-posting.js'; +import { withSearch } from './twitter-client-search.js'; +import { withTimelines } from './twitter-client-timelines.js'; +import { withTweetDetails } from './twitter-client-tweet-detail.js'; +import { withUserLookup } from './twitter-client-user-lookup.js'; +import { withUserTweets } from './twitter-client-user-tweets.js'; +import { withUsers } from './twitter-client-users.js'; +// News mixin wraps search because it depends on the search() method +// Engagement mixin adds like/unlike/retweet/unretweet/bookmark methods +const MixedTwitterClient = withNews(withUserTweets(withUserLookup(withUsers(withLists(withHome(withTimelines(withSearch(withTweetDetails(withPosting(withEngagement(withFollow(withBookmarks(withMedia(TwitterClientBase)))))))))))))); +export class TwitterClient extends MixedTwitterClient { +} +//# sourceMappingURL=twitter-client.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/twitter-client.js.map b/vendor/package/dist/lib/twitter-client.js.map new file mode 100644 index 0000000..030335d --- /dev/null +++ b/vendor/package/dist/lib/twitter-client.js.map @@ -0,0 +1 @@ +{"version":3,"file":"twitter-client.js","sourceRoot":"","sources":["../../src/lib/twitter-client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAqC,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACjG,OAAO,EAAuC,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrG,OAAO,EAAmC,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACzF,OAAO,EAAiC,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACnF,OAAO,EAAiC,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACrF,OAAO,EAAkC,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtF,OAAO,EAAiC,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACnF,OAAO,EAAoC,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC5F,OAAO,EAAmC,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACzF,OAAO,EAAqC,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACjG,OAAO,EAAwC,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAC1G,OAAO,EAAuC,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACtG,OAAO,EAAuC,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACtG,OAAO,EAAiC,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAkBrF,oEAAoE;AACpE,uEAAuE;AACvE,MAAM,kBAAkB,GAAG,QAAQ,CACjC,cAAc,CACZ,cAAc,CACZ,SAAS,CACP,SAAS,CACP,QAAQ,CACN,aAAa,CACX,UAAU,CACR,gBAAgB,CAAC,WAAW,CAAC,cAAc,CAAC,UAAU,CAAC,aAAa,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CACvG,CACF,CACF,CACF,CACF,CACF,CACF,CAC4C,CAAC;AAEhD,MAAM,OAAO,aAAc,SAAQ,kBAAkB;CAAG"} \ No newline at end of file diff --git a/vendor/package/dist/lib/version.d.ts b/vendor/package/dist/lib/version.d.ts new file mode 100644 index 0000000..5f4a056 --- /dev/null +++ b/vendor/package/dist/lib/version.d.ts @@ -0,0 +1,6 @@ +export declare const FALLBACK_VERSION = "unknown"; +export declare function resolvePackageVersion(importMetaUrl?: string): string; +export declare function resolveGitSha(importMetaUrl?: string): string | null; +export declare function formatVersionLine(importMetaUrl?: string): string; +export declare function getCliVersion(): string; +//# sourceMappingURL=version.d.ts.map \ No newline at end of file diff --git a/vendor/package/dist/lib/version.d.ts.map b/vendor/package/dist/lib/version.d.ts.map new file mode 100644 index 0000000..c3c1417 --- /dev/null +++ b/vendor/package/dist/lib/version.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../src/lib/version.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,gBAAgB,YAAY,CAAC;AA2C1C,wBAAgB,qBAAqB,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CA0BpE;AAgED,wBAAgB,aAAa,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA2CnE;AAED,wBAAgB,iBAAiB,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAIhE;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC"} \ No newline at end of file diff --git a/vendor/package/dist/lib/version.js b/vendor/package/dist/lib/version.js new file mode 100644 index 0000000..a6bcddc --- /dev/null +++ b/vendor/package/dist/lib/version.js @@ -0,0 +1,174 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +export const FALLBACK_VERSION = 'unknown'; +const REF_PREFIX_REGEX = /^ref:\s*/i; +const LINE_SPLIT_REGEX = /\r?\n/; +const GITDIR_REGEX = /gitdir:\s*(.+)\s*$/i; +function readPackageVersionFromJsonFile(candidate) { + try { + const raw = fs.readFileSync(candidate, 'utf8'); + const json = JSON.parse(raw); + if (json && typeof json.version === 'string' && json.version.trim().length > 0) { + return json.version.trim(); + } + return null; + } + catch { + return null; + } +} +function readVersionFromTextFile(candidate) { + try { + const raw = fs.readFileSync(candidate, 'utf8').trim(); + return raw.length > 0 ? raw : null; + } + catch { + return null; + } +} +function resolveStartDir(importMetaUrl) { + if (typeof importMetaUrl === 'string' && importMetaUrl.trim().length > 0) { + try { + return path.dirname(fileURLToPath(importMetaUrl)); + } + catch { + // ignore + } + } + if (typeof __dirname === 'string' && __dirname.length > 0) { + return __dirname; + } + return process.cwd(); +} +export function resolvePackageVersion(importMetaUrl) { + const injected = typeof process !== 'undefined' && typeof process.env.BIRD_VERSION === 'string' + ? process.env.BIRD_VERSION.trim() + : ''; + if (injected.length > 0) { + return injected; + } + let dir = resolveStartDir(importMetaUrl); + for (let i = 0; i < 10; i += 1) { + const version = readPackageVersionFromJsonFile(path.join(dir, 'package.json')) ?? + readVersionFromTextFile(path.join(dir, 'VERSION')); + if (version) { + return version; + } + const parent = path.dirname(dir); + if (parent === dir) { + break; + } + dir = parent; + } + return FALLBACK_VERSION; +} +function truncateSha(sha, length = 8) { + const trimmed = sha.trim(); + if (!trimmed) { + return ''; + } + if (trimmed.length <= length) { + return trimmed; + } + return trimmed.slice(0, length); +} +function resolveGitShaFromGitDir(gitDir) { + const headPath = path.join(gitDir, 'HEAD'); + let head = ''; + try { + head = fs.readFileSync(headPath, 'utf8').trim(); + } + catch { + return null; + } + if (!head) { + return null; + } + if (!head.startsWith('ref:')) { + const sha = truncateSha(head); + return sha.length > 0 ? sha : null; + } + const ref = head.replace(REF_PREFIX_REGEX, '').trim(); + if (!ref) { + return null; + } + const refPath = path.join(gitDir, ref); + try { + const sha = truncateSha(fs.readFileSync(refPath, 'utf8')); + return sha.length > 0 ? sha : null; + } + catch { + // fall through to packed-refs + } + const packedRefsPath = path.join(gitDir, 'packed-refs'); + try { + const packed = fs.readFileSync(packedRefsPath, 'utf8'); + const lines = packed.split(LINE_SPLIT_REGEX); + for (const line of lines) { + if (!line || line.startsWith('#') || line.startsWith('^')) { + continue; + } + const [shaRaw, refName] = line.split(' '); + if (refName?.trim() === ref) { + const sha = truncateSha(shaRaw ?? ''); + return sha.length > 0 ? sha : null; + } + } + } + catch { + // ignore + } + return null; +} +export function resolveGitSha(importMetaUrl) { + const injected = typeof process !== 'undefined' && typeof process.env.BIRD_GIT_SHA === 'string' + ? process.env.BIRD_GIT_SHA.trim() + : ''; + if (injected.length > 0) { + return truncateSha(injected); + } + let dir = resolveStartDir(importMetaUrl); + for (let i = 0; i < 10; i += 1) { + const dotGit = path.join(dir, '.git'); + try { + const stat = fs.statSync(dotGit); + if (stat.isDirectory()) { + const sha = resolveGitShaFromGitDir(dotGit); + if (sha) { + return sha; + } + } + else if (stat.isFile()) { + const txt = fs.readFileSync(dotGit, 'utf8'); + const match = GITDIR_REGEX.exec(txt); + const gitDir = match?.[1]?.trim(); + if (gitDir) { + const resolved = path.isAbsolute(gitDir) ? gitDir : path.resolve(dir, gitDir); + const sha = resolveGitShaFromGitDir(resolved); + if (sha) { + return sha; + } + } + } + } + catch { + // ignore + } + const parent = path.dirname(dir); + if (parent === dir) { + break; + } + dir = parent; + } + return null; +} +export function formatVersionLine(importMetaUrl) { + const version = resolvePackageVersion(importMetaUrl); + const sha = resolveGitSha(importMetaUrl); + return sha ? `${version} (${sha})` : version; +} +export function getCliVersion() { + return formatVersionLine(import.meta.url); +} +//# sourceMappingURL=version.js.map \ No newline at end of file diff --git a/vendor/package/dist/lib/version.js.map b/vendor/package/dist/lib/version.js.map new file mode 100644 index 0000000..56ceddf --- /dev/null +++ b/vendor/package/dist/lib/version.js.map @@ -0,0 +1 @@ +{"version":3,"file":"version.js","sourceRoot":"","sources":["../../src/lib/version.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAIzC,MAAM,CAAC,MAAM,gBAAgB,GAAG,SAAS,CAAC;AAC1C,MAAM,gBAAgB,GAAG,WAAW,CAAC;AACrC,MAAM,gBAAgB,GAAG,OAAO,CAAC;AACjC,MAAM,YAAY,GAAG,qBAAqB,CAAC;AAE3C,SAAS,8BAA8B,CAAC,SAAiB;IACvD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAiC,CAAC;QAC7D,IAAI,IAAI,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC/E,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QAC7B,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,uBAAuB,CAAC,SAAiB;IAChD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QACtD,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CAAC,aAAsB;IAC7C,IAAI,OAAO,aAAa,KAAK,QAAQ,IAAI,aAAa,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACzE,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC,CAAC;QACpD,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;IACH,CAAC;IAED,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1D,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,OAAO,OAAO,CAAC,GAAG,EAAE,CAAC;AACvB,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,aAAsB;IAC1D,MAAM,QAAQ,GACZ,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,OAAO,CAAC,GAAG,CAAC,YAAY,KAAK,QAAQ;QAC5E,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,EAAE;QACjC,CAAC,CAAC,EAAE,CAAC;IACT,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,IAAI,GAAG,GAAG,eAAe,CAAC,aAAa,CAAC,CAAC;IACzC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/B,MAAM,OAAO,GACX,8BAA8B,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;YAC9D,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC,CAAC;QACrD,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,OAAO,CAAC;QACjB,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;YACnB,MAAM;QACR,CAAC;QACD,GAAG,GAAG,MAAM,CAAC;IACf,CAAC;IAED,OAAO,gBAAgB,CAAC;AAC1B,CAAC;AAED,SAAS,WAAW,CAAC,GAAW,EAAE,SAAiB,CAAC;IAClD,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,IAAI,MAAM,EAAE,CAAC;QAC7B,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;AAClC,CAAC;AAED,SAAS,uBAAuB,CAAC,MAAc;IAC7C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3C,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,CAAC;QACH,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;QAC9B,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;IACrC,CAAC;IAED,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACtD,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACvC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,WAAW,CAAC,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;QAC1D,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,8BAA8B;IAChC,CAAC;IAED,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACxD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,EAAE,CAAC,YAAY,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;QACvD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC;QAC7C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC1D,SAAS;YACX,CAAC;YACD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAC1C,IAAI,OAAO,EAAE,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;gBAC5B,MAAM,GAAG,GAAG,WAAW,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;gBACtC,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;YACrC,CAAC;QACH,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,SAAS;IACX,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,aAAsB;IAClD,MAAM,QAAQ,GACZ,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,OAAO,CAAC,GAAG,CAAC,YAAY,KAAK,QAAQ;QAC5E,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,EAAE;QACjC,CAAC,CAAC,EAAE,CAAC;IACT,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,OAAO,WAAW,CAAC,QAAQ,CAAC,CAAC;IAC/B,CAAC;IAED,IAAI,GAAG,GAAG,eAAe,CAAC,aAAa,CAAC,CAAC;IACzC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACtC,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YACjC,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;gBACvB,MAAM,GAAG,GAAG,uBAAuB,CAAC,MAAM,CAAC,CAAC;gBAC5C,IAAI,GAAG,EAAE,CAAC;oBACR,OAAO,GAAG,CAAC;gBACb,CAAC;YACH,CAAC;iBAAM,IAAI,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;gBACzB,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;gBAC5C,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBACrC,MAAM,MAAM,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;gBAClC,IAAI,MAAM,EAAE,CAAC;oBACX,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;oBAC9E,MAAM,GAAG,GAAG,uBAAuB,CAAC,QAAQ,CAAC,CAAC;oBAC9C,IAAI,GAAG,EAAE,CAAC;wBACR,OAAO,GAAG,CAAC;oBACb,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;YACnB,MAAM;QACR,CAAC;QACD,GAAG,GAAG,MAAM,CAAC;IACf,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,aAAsB;IACtD,MAAM,OAAO,GAAG,qBAAqB,CAAC,aAAa,CAAC,CAAC;IACrD,MAAM,GAAG,GAAG,aAAa,CAAC,aAAa,CAAC,CAAC;IACzC,OAAO,GAAG,CAAC,CAAC,CAAC,GAAG,OAAO,KAAK,GAAG,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,OAAO,iBAAiB,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC5C,CAAC"} \ No newline at end of file diff --git a/vendor/package/package.json b/vendor/package/package.json new file mode 100644 index 0000000..642bfe8 --- /dev/null +++ b/vendor/package/package.json @@ -0,0 +1,61 @@ +{ + "name": "@steipete/bird", + "version": "0.8.0", + "description": "CLI tool for tweeting and replying via Twitter/X GraphQL API", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "bin": { + "bird": "dist/cli.js" + }, + "files": [ + "dist", + "README.md", + "CHANGELOG.md", + "LICENSE" + ], + "scripts": { + "build": "pnpm run build:dist && pnpm run build:binary", + "build:dist": "tsc && node scripts/copy-dist-assets.js", + "build:binary": "BIRD_VERSION=$(node -p \"require('./package.json').version\") BIRD_GIT_SHA=$(git rev-parse --short=8 HEAD 2>/dev/null || true) bun build --compile --minify --env=BIRD_* src/cli.ts --outfile bird", + "dev": "tsx src/index.ts", + "bird": "pnpm run build:dist && node dist/cli.js", + "test": "vitest run", + "test:watch": "vitest", + "test:live": "pnpm run build:dist && BIRD_LIVE=1 vitest run --no-file-parallelism tests/live/live.test.ts", + "test:live:all": "pnpm run build:dist && BIRD_LIVE=1 vitest run --no-file-parallelism tests/live/live-all.test.ts", + "lint": "pnpm run lint:biome && pnpm run lint:oxlint", + "lint:biome": "biome check .", + "lint:oxlint": "oxlint --type-aware --tsconfig tsconfig.oxlint.json --import-plugin --node-plugin --vitest-plugin --deny-warnings src tests scripts", + "lint:fix": "pnpm run lint:biome:fix && pnpm run lint:oxlint:fix", + "lint:biome:fix": "biome check --write .", + "lint:oxlint:fix": "oxlint --type-aware --tsconfig tsconfig.oxlint.json --import-plugin --node-plugin --vitest-plugin --deny-warnings --fix src tests scripts", + "format": "biome format --write .", + "binary": "pnpm run build:binary", + "graphql:update": "tsx scripts/update-query-ids.ts" + }, + "dependencies": { + "@steipete/sweet-cookie": "0.1.0", + "commander": "^14.0.2", + "json5": "^2.2.3", + "kleur": "^4.1.5" + }, + "pnpm": { + "patchedDependencies": { + "@steipete/sweet-cookie": "patches/@steipete__sweet-cookie.patch" + } + }, + "devDependencies": { + "@biomejs/biome": "^2.3.10", + "@types/node": "^25.0.3", + "@vitest/coverage-v8": "4.0.16", + "oxlint": "^1.36.0", + "oxlint-tsgolint": "^0.10.0", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "vitest": "^4.0.16" + }, + "engines": { + "node": ">=22" + } +} diff --git a/vendor/steipete-bird-0.8.0.tgz b/vendor/steipete-bird-0.8.0.tgz new file mode 100644 index 0000000..8c2d1cd Binary files /dev/null and b/vendor/steipete-bird-0.8.0.tgz differ