feat: v3.0.0 - intelligent search, GitHub person/project mode, ELI5, 13+ sources

v3 rewrites the search engine from the ground up:

- Intelligent pre-research: resolves X handles, GitHub repos, subreddits,
  TikTok hashtags, and YouTube channels before searching
- GitHub person-mode: PR velocity, top repos by stars, release notes
- GitHub project-mode: live star counts, README, releases, top issues
- ELI5 mode: plain language synthesis, no jargon
- 13+ sources: Reddit, X, YouTube, TikTok, Instagram, HN, Polymarket,
  GitHub, Threads, Pinterest, Perplexity, Bluesky, Web
- Free Reddit comments via public JSON (no API key needed)
- Fun judge v2: humor scoring baked into narrative
- Cookie consent before browser scanning
- 10,000 free ScrapeCreators calls
- 1,012 tests

Thank you to the community contributors whose issues and PRs shaped v3:
@uppinote20 (#143), @zerone0x (#134, #136), @thinkun (#116),
@thomasmktong (#124), @fanispoulinakisai-boop (#100), @pejmanjohn (#78),
@zl190 (#115), @hnshah (#84, #85, #86)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Van Horn
2026-04-08 10:52:23 -07:00
parent 61904b31e3
commit 0a9ff16dfc
397 changed files with 21427 additions and 53106 deletions
+25 -12
View File
@@ -1,26 +1,28 @@
"""HTTP utilities for last30days skill (stdlib only)."""
import json
import os
import re
import sys
import time
import urllib.error
import urllib.request
from typing import Any, Dict, Optional
from typing import Any, Dict, Optional, Union
from urllib.parse import urlencode
from . import log as _log
DEFAULT_TIMEOUT = 30
DEBUG = os.environ.get("LAST30DAYS_DEBUG", "").lower() in ("1", "true", "yes")
def log(msg: str):
"""Log debug message to stderr."""
if DEBUG:
sys.stderr.write(f"[DEBUG] {msg}\n")
sys.stderr.flush()
_log.debug(msg)
MAX_RETRIES = 5
MAX_429_RETRIES = 2
RETRY_DELAY = 2.0
USER_AGENT = "last30days-skill/2.1 (Assistant Skill)"
USER_AGENT = "last30days-skill/3.0 (Assistant Skill)"
class HTTPError(Exception):
@@ -38,8 +40,9 @@ def request(
json_data: Optional[Dict[str, Any]] = None,
timeout: int = DEFAULT_TIMEOUT,
retries: int = MAX_RETRIES,
max_429_retries: int = MAX_429_RETRIES,
raw: bool = False,
) -> Dict[str, Any]:
) -> Union[Dict[str, Any], str]:
"""Make an HTTP request and return JSON response.
Args:
@@ -49,9 +52,11 @@ def request(
json_data: Optional JSON body (for POST)
timeout: Request timeout in seconds
retries: Number of retries on failure
max_429_retries: Maximum 429 retries before giving up (separate cap)
raw: If True, return raw response text instead of parsed JSON
Returns:
Parsed JSON response (or raw text if raw=True)
Parsed JSON response as dict, or raw text string if raw=True.
Raises:
HTTPError: On request failure
@@ -66,9 +71,11 @@ def request(
req = urllib.request.Request(url, data=data, headers=headers, method=method)
log(f"{method} {url}")
safe_url = re.sub(r'([?&])(key|api_key|token|secret)=[^&]*', r'\1\2=***', url)
log(f"{method} {safe_url}")
last_error = None
rate_limit_count = 0
for attempt in range(retries):
try:
with urllib.request.urlopen(req, timeout=timeout) as response:
@@ -81,7 +88,7 @@ def request(
body = None
try:
body = e.read().decode('utf-8')
except:
except (OSError, UnicodeDecodeError):
pass
log(f"HTTP Error {e.code}: {e.reason}")
if body:
@@ -93,6 +100,12 @@ def request(
if 400 <= e.code < 500 and e.code != 429:
raise last_error
# Cap 429 retries separately to avoid wasting latency
if e.code == 429:
rate_limit_count += 1
if rate_limit_count >= max_429_retries:
raise last_error
if attempt < retries - 1:
if e.code == 429:
# Respect Retry-After header, fall back to exponential backoff
@@ -103,7 +116,7 @@ def request(
except ValueError:
delay = RETRY_DELAY * (2 ** attempt) + 1
else:
delay = RETRY_DELAY * (2 ** attempt) + 1 # 2s, 5s, 9s...
delay = RETRY_DELAY * (2 ** attempt) + 1 # 3s, 5s, 9s...
log(f"Rate limited (429). Waiting {delay:.1f}s before retry {attempt + 2}/{retries}")
else:
delay = RETRY_DELAY * (2 ** attempt)