Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 93fbed2705 | |||
| 65be6196c1 | |||
| 7dc530b4c9 | |||
| b159f8b1ff | |||
| cff005b038 | |||
| 460565c107 | |||
| e6493033b0 | |||
| ca00cacf83 | |||
| b982ed5b30 | |||
| a9d13d695a | |||
| 877706da4d | |||
| 1a6d8d07d0 | |||
| 3bc12cdc57 | |||
| ad59e60269 | |||
| 9d037786f2 | |||
| 8b67378964 | |||
| 2b015b64ab | |||
| 56cabf33c6 | |||
| 13dcea781d | |||
| 01812ec185 | |||
| 86b2b9dd69 | |||
| 6e7c0ba7aa | |||
| 71e0492840 | |||
| 8b2cf41f13 | |||
| 3d57db9644 | |||
| 99b167d03a |
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,5 @@
|
|||||||
"repository": "https://github.com/mvanhorn/last30days-skill",
|
"repository": "https://github.com/mvanhorn/last30days-skill",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": ["research", "reddit", "twitter", "youtube", "tiktok", "instagram", "trends", "prompts", "polymarket", "github", "perplexity", "threads", "pinterest", "eli5", "hacker-news"],
|
"keywords": ["research", "reddit", "twitter", "youtube", "tiktok", "instagram", "trends", "prompts", "polymarket", "github", "perplexity", "threads", "pinterest", "eli5", "hacker-news"],
|
||||||
"skills": ["./"],
|
|
||||||
"hooks": {}
|
"hooks": {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"name": "last30days"
|
||||||
|
}
|
||||||
@@ -15,3 +15,7 @@ variants/open/references/research.md
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
mise.toml
|
mise.toml
|
||||||
|
.memsearch/
|
||||||
|
.venv/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|||||||
@@ -0,0 +1,269 @@
|
|||||||
|
---
|
||||||
|
name: last30days
|
||||||
|
version: "3.0.0"
|
||||||
|
description: "Multi-query social search with intelligent planning. Research any topic across Reddit, X, YouTube, TikTok, Instagram, Hacker News, Polymarket, and the web."
|
||||||
|
argument-hint: 'last30days AI video tools, last30days best noise cancelling headphones'
|
||||||
|
allowed-tools: Bash, Read, Write, AskUserQuestion, WebSearch
|
||||||
|
homepage: https://github.com/mvanhorn/last30days-skill
|
||||||
|
repository: https://github.com/mvanhorn/last30days-skill
|
||||||
|
author: mvanhorn
|
||||||
|
license: MIT
|
||||||
|
user-invocable: true
|
||||||
|
metadata:
|
||||||
|
hermes:
|
||||||
|
emoji: "📰"
|
||||||
|
tags:
|
||||||
|
- research
|
||||||
|
- deep-research
|
||||||
|
- reddit
|
||||||
|
- x
|
||||||
|
- twitter
|
||||||
|
- youtube
|
||||||
|
- tiktok
|
||||||
|
- instagram
|
||||||
|
- hackernews
|
||||||
|
- polymarket
|
||||||
|
- trends
|
||||||
|
- recency
|
||||||
|
- news
|
||||||
|
- citations
|
||||||
|
- multi-source
|
||||||
|
- social-media
|
||||||
|
- analysis
|
||||||
|
- web-search
|
||||||
|
requires:
|
||||||
|
env:
|
||||||
|
- SCRAPECREATORS_API_KEY
|
||||||
|
optionalEnv:
|
||||||
|
- OPENAI_API_KEY
|
||||||
|
- XAI_API_KEY
|
||||||
|
- OPENROUTER_API_KEY
|
||||||
|
- PARALLEL_API_KEY
|
||||||
|
- BRAVE_API_KEY
|
||||||
|
- APIFY_API_TOKEN
|
||||||
|
- AUTH_TOKEN
|
||||||
|
- CT0
|
||||||
|
- BSKY_HANDLE
|
||||||
|
- BSKY_APP_PASSWORD
|
||||||
|
- TRUTHSOCIAL_TOKEN
|
||||||
|
bins:
|
||||||
|
- node
|
||||||
|
- python3
|
||||||
|
primaryEnv: SCRAPECREATORS_API_KEY
|
||||||
|
files:
|
||||||
|
- "scripts/*"
|
||||||
|
homepage: https://github.com/mvanhorn/last30days-skill
|
||||||
|
---
|
||||||
|
|
||||||
|
# last30days v3.0.0: Research Any Topic from the Last 30 Days
|
||||||
|
|
||||||
|
> **Permissions overview:** Reads public web/platform data and optionally saves research briefings to `~/Documents/Last30Days/`. X/Twitter search uses optional user-provided tokens (AUTH_TOKEN/CT0 env vars). Bluesky search uses optional app password (BSKY_HANDLE/BSKY_APP_PASSWORD env vars - create at bsky.app/settings/app-passwords). All credential usage and data writes are documented in the [Security & Permissions](#security--permissions) section.
|
||||||
|
|
||||||
|
Research ANY topic across Reddit, X, YouTube, and other sources. Surface what people are actually discussing, recommending, betting on, and debating right now.
|
||||||
|
|
||||||
|
## Runtime Preflight
|
||||||
|
|
||||||
|
Before running any `last30days.py` command in this skill, resolve a Python 3.12+ interpreter once and keep it in `LAST30DAYS_PYTHON`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for py in python3.14 python3.13 python3.12 python3; do
|
||||||
|
command -v "$py" >/dev/null 2>&1 || continue
|
||||||
|
"$py" -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 12) else 1)' || continue
|
||||||
|
LAST30DAYS_PYTHON="$py"
|
||||||
|
break
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "${LAST30DAYS_PYTHON:-}" ]; then
|
||||||
|
echo "ERROR: last30days v3 requires Python 3.12+. Install python3.12 or python3.13 and rerun." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 0: First-Run Setup Wizard
|
||||||
|
|
||||||
|
**CRITICAL: ALWAYS execute Step 0 BEFORE Step 1, even if the user provided a topic.** If the user typed `last30days Mercer Island`, you MUST check for FIRST_RUN and present the wizard BEFORE running research. The topic "Mercer Island" is preserved — research runs immediately after the wizard completes. Do NOT skip the wizard because a topic was provided. The wizard takes 10 seconds and only runs once ever.
|
||||||
|
|
||||||
|
To detect first run: check if `~/.config/last30days/.env` exists. If it does NOT exist, this is a first run. **Do NOT run any Bash commands or show any command output to detect this — just check the file existence silently.** If the file exists and contains `SETUP_COMPLETE=true`, skip this section **silently** and proceed to Step 1. **Do NOT say "Setup is complete" or any other status message — just move on.** The user doesn't need to be told setup is done every time they run the skill.
|
||||||
|
|
||||||
|
**When first run is detected, detect your platform first:**
|
||||||
|
|
||||||
|
**If you do NOT have WebSearch capability (raw CLI):** Run the terminal-only setup flow below.
|
||||||
|
**If you DO have WebSearch (Hermes):** Run the standard setup flow below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Terminal-Only / Non-WebSearch Setup Flow
|
||||||
|
|
||||||
|
Run environment detection first:
|
||||||
|
```bash
|
||||||
|
"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" setup --terminal
|
||||||
|
```
|
||||||
|
|
||||||
|
Read the JSON output. It tells you what's already configured. Display a status summary:
|
||||||
|
|
||||||
|
```
|
||||||
|
👋 Welcome to last30days!
|
||||||
|
|
||||||
|
Detected:
|
||||||
|
{✅ or ❌} yt-dlp (YouTube search)
|
||||||
|
{✅ or ❌} X/Twitter ({method} configured)
|
||||||
|
{✅ or ❌} ScrapeCreators (TikTok, Instagram, Reddit backup)
|
||||||
|
{✅ or ❌} Web search ({backend} configured)
|
||||||
|
```
|
||||||
|
|
||||||
|
Then for each missing item, offer setup in priority order:
|
||||||
|
|
||||||
|
1. **ScrapeCreators** (if not configured): "ScrapeCreators adds TikTok and Instagram search (plus a Reddit backup if public Reddit gets rate-limited). 10,000 free calls, no credit card. (No referrals, no kickbacks - we don't get a cut.)"
|
||||||
|
- Option A: "ScrapeCreators via GitHub (recommended)" — Check if `gh` CLI was detected in the environment detection output above. If gh IS detected: description should say "Registers directly via GitHub CLI in ~2 seconds - no browser needed". Before running the command, display: "Registering via GitHub CLI..." If gh is NOT detected: description should say "Copies a one-time code to your clipboard and opens GitHub to authorize". Then run `"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" setup --github`, parse JSON output. Tries PAT first (if `gh` is installed), falls back to device flow which copies a one-time code to your clipboard and opens your browser. If `status` is `success`, write `SCRAPECREATORS_API_KEY=*** to .env.
|
||||||
|
- Option B: "I have a key" — accept paste, write to .env
|
||||||
|
- Option C: "Skip for now"
|
||||||
|
|
||||||
|
2. **X/Twitter** (if not configured): "X search finds tweets and conversations. To unlock X: add FROM_BROWSER=auto (reads browser cookies, free), XAI_API_KEY (no browser access, api.x.ai), or AUTH_TOKEN+CT0 (manual cookies)."
|
||||||
|
- Option A: "I have an xAI API key" (recommended for servers — persistent, no expiry). Write XAI_API_KEY to .env.
|
||||||
|
- Option B: "I have AUTH_TOKEN + CT0 from my browser" — accept both, write to .env
|
||||||
|
- Option C: "Skip for now"
|
||||||
|
|
||||||
|
3. **YouTube** (if yt-dlp not found): "YouTube search needs yt-dlp. Run: `pip install yt-dlp`"
|
||||||
|
|
||||||
|
4. **Web search** (if no Brave/Exa/Serper key): "A web search key enables smarter results. Brave Search is free for 2,000 queries/month at brave.com/search/api"
|
||||||
|
|
||||||
|
After setup, write `SETUP_COMPLETE=true` to .env and proceed to research.
|
||||||
|
|
||||||
|
**Skip to "END OF FIRST-RUN WIZARD" below after completing the terminal-only flow.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Hermes Setup Flow (Standard)
|
||||||
|
|
||||||
|
**You MUST follow these steps IN ORDER. Do NOT skip ahead to the topic picker or research. The sequence is: (1) welcome text -> (2) setup modal -> (3) run setup if chosen -> (4) optional ScrapeCreators modal -> (5) topic picker. You MUST start at step 1.**
|
||||||
|
|
||||||
|
**Step 1: Display the following welcome text ONCE as a normal message (not blockquoted). Then IMMEDIATELY call AskUserQuestion - do NOT repeat any of the welcome text inside the AskUserQuestion call.**
|
||||||
|
|
||||||
|
Welcome to last30days!
|
||||||
|
|
||||||
|
I research any topic across Reddit, X, YouTube, and other sources - synthesizing what people are actually saying right now.
|
||||||
|
|
||||||
|
Auto setup gives you 5 core sources for free in 30 seconds:
|
||||||
|
- X/Twitter - reads your x.com browser cookies to authenticate (not saved to disk). Chrome on macOS will prompt for Keychain access.
|
||||||
|
- Reddit with comments - public JSON, no API key needed
|
||||||
|
- YouTube search + transcripts - installs yt-dlp (open source, 190K+ GitHub stars)
|
||||||
|
- Hacker News + Polymarket + GitHub (if `gh` CLI installed) - always on, zero config
|
||||||
|
|
||||||
|
Want TikTok and Instagram too? ScrapeCreators adds those (10,000 free calls, scrapecreators.com). No kickbacks, no affiliation.
|
||||||
|
|
||||||
|
**Then call AskUserQuestion with ONLY this question and these options - no additional text:**
|
||||||
|
|
||||||
|
Question: "How would you like to set up?"
|
||||||
|
Options:
|
||||||
|
- "Auto setup (~30 seconds) - scans browser cookies for X + installs yt-dlp for YouTube"
|
||||||
|
- "Manual setup - show me what to configure"
|
||||||
|
- "Skip for now - Reddit (with comments), HN, Polymarket, GitHub (if gh installed), Web"
|
||||||
|
|
||||||
|
**If the user picks 1 (Auto setup):**
|
||||||
|
|
||||||
|
**Before running the setup command, get cookie consent:**
|
||||||
|
|
||||||
|
Check if `BROWSER_CONSENT=true` already exists in `~/.config/last30days/.env`. If it does, skip the consent prompt and run setup directly.
|
||||||
|
|
||||||
|
If `BROWSER_CONSENT=true` is NOT present, **call AskUserQuestion:**
|
||||||
|
Question: "Auto setup will scan your browser for x.com cookies to authenticate X search. Cookies are read live, not saved to disk. Chrome on macOS will prompt for Keychain access. OK to proceed?"
|
||||||
|
Options:
|
||||||
|
- "Yes, scan my cookies for X" - Run setup as normal. Append `BROWSER_CONSENT=true` to .env after setup completes.
|
||||||
|
- "Skip X, just set up YouTube" - Run setup with YouTube only (install yt-dlp). Do not scan cookies.
|
||||||
|
- "I have an xAI API key instead" - Ask them to paste it, write XAI_API_KEY to .env. Then install yt-dlp.
|
||||||
|
|
||||||
|
Run the setup subcommand:
|
||||||
|
```bash
|
||||||
|
cd {SKILL_DIR} && "${LAST30DAYS_PYTHON}" scripts/last30days.py setup
|
||||||
|
```
|
||||||
|
Show the user the results (what cookies were found, whether yt-dlp was installed).
|
||||||
|
|
||||||
|
**Then show the optional ScrapeCreators offer (plain text, then modal):**
|
||||||
|
|
||||||
|
Want TikTok and Instagram too? ScrapeCreators adds those platforms - 10,000 free calls, no credit card. It also serves as a Reddit backup if public Reddit ever gets rate-limited.
|
||||||
|
|
||||||
|
**Before showing the ScrapeCreators modal, check for `gh` CLI:** Run `which gh` via Bash silently. Store the result as gh_available (true if found, false if not).
|
||||||
|
|
||||||
|
**Call AskUserQuestion:**
|
||||||
|
Question: "Want to add TikTok, Instagram, and Reddit backup via ScrapeCreators? (We don't get a cut.)"
|
||||||
|
Options:
|
||||||
|
- "ScrapeCreators via GitHub (fastest, recommended)" - If gh_available: description should say "Registers directly via GitHub CLI in ~2 seconds - no browser needed". If NOT gh_available: description should say "Copies a one-time code to your clipboard and opens GitHub to authorize". After the user selects this option: If gh_available, display "Registering via GitHub CLI..." before running the command. If NOT gh_available, display "I'll copy a one-time code to your clipboard and open GitHub. When GitHub asks for a device code, just paste (Cmd+V on Mac, Ctrl+V on Windows/Linux)." Then run `cd {SKILL_DIR} && "${LAST30DAYS_PYTHON}" scripts/last30days.py setup --github` via Bash with a 5-minute timeout. This tries PAT auth first (if `gh` CLI is installed, zero browser needed), then falls back to GitHub device flow which copies a one-time code to your clipboard and opens GitHub in your browser. Parse the JSON stdout. If `status` is `success`, write `SCRAPECREATORS_API_KEY=*** to `~/.config/last30days/.env`. If `method` is `pat`, show: "You're in! Registered via GitHub CLI - zero browser needed. 10,000 free calls. TikTok, Instagram, and Reddit backup are now active." If `method` is `device` and `clipboard_ok` is true, show: "You're in! (The authorization code was copied to your clipboard automatically.) 10,000 free calls. TikTok, Instagram, and Reddit backup are now active." If `method` is `device` and `clipboard_ok` is false, show: "You're in! 10,000 free calls. TikTok, Instagram, and Reddit backup are now active." If `status` is `timeout` or `error`, show: "GitHub auth didn't complete. No worries - you can sign up at scrapecreators.com instead or try again later." Then offer the web signup option.
|
||||||
|
- "Open scrapecreators.com (Google sign-in)" - run `open https://scrapecreators.com` via Bash to open in the user's browser. Then ask them to paste the API key they get. When they paste it, write SCRAPECREATORS_API_KEY=*** to ~/.config/last30days/.env
|
||||||
|
- "I have a key" - accept the key, write to .env
|
||||||
|
- "Skip for now" - proceed without ScrapeCreators
|
||||||
|
|
||||||
|
**After SC key is saved (not if skipped), show the TikTok/Instagram opt-in:**
|
||||||
|
|
||||||
|
**Call AskUserQuestion:**
|
||||||
|
Question: "Enable TikTok and Instagram search?"
|
||||||
|
Options:
|
||||||
|
- "Yes, enable TikTok + Instagram" - Write `TIKTOK_ENABLED=true` and `INSTAGRAM_ENABLED=true` to .env. Then show: "TikTok and Instagram are now enabled. You can disable them later by editing ~/.config/last30days/.env."
|
||||||
|
- "No, skip for now" - proceed without enabling
|
||||||
|
|
||||||
|
**After setup completes, write `SETUP_COMPLETE=true` to .env.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## END OF FIRST-RUN WIZARD
|
||||||
|
|
||||||
|
Proceed to Step 1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: Parse Topic
|
||||||
|
|
||||||
|
The user invoked: `last30days {QUERY}`
|
||||||
|
|
||||||
|
Extract the topic. If the query is empty or ambiguous, ask for clarification.
|
||||||
|
|
||||||
|
## Step 2: Execute Research
|
||||||
|
|
||||||
|
Run the research engine:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd {SKILL_DIR} && "${LAST30DAYS_PYTHON}" scripts/last30days.py "{TOPIC}" --emit=compact --lookback-days=30
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional flags based on user request:
|
||||||
|
- `--search=reddit,youtube,hackernews` - Specific sources only
|
||||||
|
- `--days=7` - Shorter time range
|
||||||
|
- `--deep` - Higher recall mode
|
||||||
|
- `--save` - Save to ~/Documents/Last30Days/
|
||||||
|
|
||||||
|
## Step 3: Display Results
|
||||||
|
|
||||||
|
Show the research output to the user. The compact output includes:
|
||||||
|
- Executive summary
|
||||||
|
- Ranked evidence clusters with scores
|
||||||
|
- Source statistics (upvotes, views, engagement)
|
||||||
|
- Citations with URLs
|
||||||
|
- Confidence levels and uncertainty notes
|
||||||
|
|
||||||
|
## Security & Permissions
|
||||||
|
|
||||||
|
**What this skill does:**
|
||||||
|
- Sends search queries to ScrapeCreators API (`api.scrapecreators.com`) for TikTok and Instagram search, and as a Reddit backup when public Reddit is unavailable (requires SCRAPECREATORS_API_KEY)
|
||||||
|
- Sends search queries to OpenAI's Responses API (`api.openai.com`) for Reddit discovery (fallback if no SCRAPECREATORS_API_KEY)
|
||||||
|
- Sends search queries to Twitter's GraphQL API (via optional user-provided AUTH_TOKEN/CT0 env vars — no browser session access) or xAI's API (`api.x.ai`) for X search
|
||||||
|
- Sends search queries to Algolia HN Search API (`hn.algolia.com`) for Hacker News story and comment discovery (free, no auth)
|
||||||
|
- Sends search queries to Polymarket Gamma API (`gamma-api.polymarket.com`) for prediction market discovery (free, no auth)
|
||||||
|
- Runs `yt-dlp` locally for YouTube search and transcript extraction (no API key, public data)
|
||||||
|
- Sends search queries to ScrapeCreators API (`api.scrapecreators.com`) for TikTok and Instagram search, transcript/caption extraction (PAYG after 10,000 free API calls)
|
||||||
|
- 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)
|
||||||
|
- Saves research briefings as .md files to ~/Documents/Last30Days/
|
||||||
|
|
||||||
|
**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
|
||||||
|
- Hacker News and Polymarket sources are always available (no API key, no binary dependency)
|
||||||
|
- TikTok and Instagram sources require SCRAPECREATORS_API_KEY (10,000 free API calls, then PAYG). Reddit uses ScrapeCreators only as a backup when public Reddit is unavailable.
|
||||||
|
- Can be invoked autonomously by agents via the Skill tool (runs inline, not forked); pass `--agent` for non-interactive report output
|
||||||
|
|
||||||
|
**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.
|
||||||
+9
-2
@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [3.0.0] - 2026-04
|
## [3.0.0] - 2026-04-11
|
||||||
|
|
||||||
### Highlights
|
### Highlights
|
||||||
|
|
||||||
@@ -34,10 +34,18 @@ Intelligent search, fun judge, cross-source cluster merging, single-pass compari
|
|||||||
- Polymarket display shows % odds only; dollar volumes removed
|
- Polymarket display shows % odds only; dollar volumes removed
|
||||||
- 852 tests passing
|
- 852 tests passing
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Marketplace validation: duplicate `name: last30days` collision in `skills/last30days/SKILL.md` caused strict validators to reject the plugin. Resolved by renaming the internal v3 architecture spec to `last30days-v3-spec` with `user-invocable: false`. Fixed in #214 (reported by @Cody-Coyote in #204).
|
||||||
|
- Stale README link to the deleted `skills/last30days-v3/` path from the v3 directory rename. Fixed in #214.
|
||||||
|
- OpenAI Codex CLI discoverability: added `.agents/skills/last30days/SKILL.md` as a real file (Codex's loader skips symlinked files) plus `.codex-plugin/plugin.json` as the namespace marker. The skill now registers as `last30days:last30days` when Codex runs in a checkout of the repo. Fixed in #219 (inspired by @Jah-yee in #153 and @dannyshmueli on X).
|
||||||
|
|
||||||
### Contributors
|
### Contributors
|
||||||
|
|
||||||
- @j-sperling -- v3 engine architecture, Python pre-research brain
|
- @j-sperling -- v3 engine architecture, Python pre-research brain
|
||||||
- @hnshah -- Watchlist features
|
- @hnshah -- Watchlist features
|
||||||
|
- @Cody-Coyote -- Marketplace validation bug report (#204)
|
||||||
|
- @Jah-yee -- Codex CLI integration inspiration (#153)
|
||||||
|
|
||||||
## [2.9.4] - 2026-03-06
|
## [2.9.4] - 2026-03-06
|
||||||
|
|
||||||
@@ -181,7 +189,6 @@ Three headline features: watchlists for always-on bots, YouTube transcripts as a
|
|||||||
|
|
||||||
### Credits
|
### Credits
|
||||||
|
|
||||||
- @steipete -- Bird CLI (vendored X search) and yt-dlp/summarize inspiration for YouTube transcripts
|
|
||||||
- @galligan -- Marketplace plugin inspiration
|
- @galligan -- Marketplace plugin inspiration
|
||||||
- @hutchins -- Pushed for YouTube feature
|
- @hutchins -- Pushed for YouTube feature
|
||||||
|
|
||||||
|
|||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
# Hermes Setup Guide for last30days
|
||||||
|
|
||||||
|
This guide covers installing last30days on Hermes AI Agent.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. **Hermes installed** - See https://github.com/mercurial-tf/hermes
|
||||||
|
2. **Python 3.12+** - `brew install python@3.12` or similar
|
||||||
|
3. **yt-dlp** (optional, for YouTube) - `brew install yt-dlp`
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Option 1: Via sync.sh (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repo
|
||||||
|
git clone https://github.com/mvanhorn/last30days-skill.git
|
||||||
|
cd last30days-skill
|
||||||
|
|
||||||
|
# Run the sync script
|
||||||
|
bash scripts/sync.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This will auto-detect Hermes and deploy to `~/.hermes/skills/research/last30days/`
|
||||||
|
|
||||||
|
### Option 2: Manual Copy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create directory
|
||||||
|
mkdir -p ~/.hermes/skills/research/last30days
|
||||||
|
|
||||||
|
# Copy files
|
||||||
|
cp -r scripts ~/.hermes/skills/research/last30days/
|
||||||
|
cp .hermes-plugin/SKILL.md ~/.hermes/skills/research/last30days/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
In Hermes, invoke with:
|
||||||
|
|
||||||
|
```
|
||||||
|
last30days "your research topic"
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with options:
|
||||||
|
```
|
||||||
|
last30days "best mechanical keyboards 2025" --search=reddit,youtube
|
||||||
|
last30days "AI news" --days=7 --deep
|
||||||
|
```
|
||||||
|
|
||||||
|
## First Run Setup
|
||||||
|
|
||||||
|
On first run, the skill will guide you through setup:
|
||||||
|
|
||||||
|
1. **Auto setup** (~30 seconds)
|
||||||
|
- Scans browser cookies for X/Twitter
|
||||||
|
- Checks/installs yt-dlp for YouTube
|
||||||
|
- Configures free sources (Reddit, HN, Polymarket)
|
||||||
|
|
||||||
|
2. **Optional: ScrapeCreators**
|
||||||
|
- Adds TikTok, Instagram, Reddit backup
|
||||||
|
- 10,000 free API calls
|
||||||
|
- Sign up at scrapecreators.com
|
||||||
|
|
||||||
|
3. **Optional: API Keys**
|
||||||
|
- XAI_API_KEY for X/Twitter (alternative to browser cookies)
|
||||||
|
- BRAVE_API_KEY for web search
|
||||||
|
|
||||||
|
## Available Sources
|
||||||
|
|
||||||
|
### Free (No API Key)
|
||||||
|
- **Reddit** - Public discussions and comments
|
||||||
|
- **Hacker News** - Tech discussions via Algolia
|
||||||
|
- **Polymarket** - Prediction markets
|
||||||
|
- **YouTube** - Search and transcripts (requires yt-dlp)
|
||||||
|
|
||||||
|
### Requires API Key
|
||||||
|
- **X/Twitter** - xAI API key or browser cookies
|
||||||
|
- **TikTok** - ScrapeCreators API
|
||||||
|
- **Instagram** - ScrapeCreators API
|
||||||
|
- **Web Search** - Brave Search API
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Python not found
|
||||||
|
```bash
|
||||||
|
# Find Python 3.12+
|
||||||
|
which python3.12 python3.13 python3.14
|
||||||
|
|
||||||
|
# If not installed
|
||||||
|
brew install python@3.12
|
||||||
|
```
|
||||||
|
|
||||||
|
### yt-dlp not found
|
||||||
|
```bash
|
||||||
|
brew install yt-dlp
|
||||||
|
# or
|
||||||
|
pip install yt-dlp
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check what's configured
|
||||||
|
```bash
|
||||||
|
cd ~/.hermes/skills/research/last30days
|
||||||
|
python3.12 scripts/last30days.py --diagnose
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
To update to the latest version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd last30days-skill
|
||||||
|
git pull
|
||||||
|
bash scripts/sync.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- Original repo: https://github.com/mvanhorn/last30days-skill
|
||||||
|
- Hermes: https://github.com/mercurial-tf/hermes
|
||||||
|
- Issues: Please report in the original repo
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
**An AI agent-led search engine scored by upvotes, likes, and real money - not editors.**
|
**An AI agent-led search engine scored by upvotes, likes, and real money - not editors.**
|
||||||
|
|
||||||
This README tracks the current v3 pipeline. The runtime skill spec lives in [skills/last30days-v3/SKILL.md](skills/last30days-v3/SKILL.md), which is the source of truth for the latest command and setup behavior.
|
This README tracks the current v3 pipeline. The runtime skill spec lives in [skills/last30days/SKILL.md](skills/last30days/SKILL.md), which is the source of truth for the latest command and setup behavior.
|
||||||
|
|
||||||
Claude Code:
|
Claude Code:
|
||||||
```
|
```
|
||||||
@@ -24,6 +24,12 @@ OpenClaw:
|
|||||||
clawhub install last30days-official
|
clawhub install last30days-official
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Hermes:
|
||||||
|
```
|
||||||
|
# The skill auto-deploys when you run sync.sh
|
||||||
|
# Or manually copy to ~/.hermes/skills/research/last30days/
|
||||||
|
```
|
||||||
|
|
||||||
Zero config. Reddit, HN, Polymarket, and GitHub work immediately. Run it once and the setup wizard unlocks X, YouTube, TikTok, and more in 30 seconds.
|
Zero config. Reddit, HN, Polymarket, and GitHub work immediately. Run it once and the setup wizard unlocks X, YouTube, TikTok, and more in 30 seconds.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -152,6 +158,25 @@ claude plugin update last30days@last30days-skill
|
|||||||
clawhub install last30days-official
|
clawhub install last30days-official
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Gemini CLI
|
||||||
|
|
||||||
|
Gemini CLI supports installing extensions from GitHub repositories, but as of Gemini CLI v0.9.0 there is an upstream installer bug that can fail with:
|
||||||
|
|
||||||
|
`Configuration file not found at /tmp/gemini-extensionXXXXXX/gemini-extension.json`
|
||||||
|
|
||||||
|
even when `gemini-extension.json` exists at the repo root.
|
||||||
|
|
||||||
|
Upstream bug:
|
||||||
|
- https://github.com/google-gemini/gemini-cli/issues/11452
|
||||||
|
|
||||||
|
Workarounds:
|
||||||
|
1) Clone locally, then install from the local path
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/mvanhorn/last30days-skill
|
||||||
|
gemini extensions install ./last30days-skill
|
||||||
|
```
|
||||||
|
2) If GitHub install fails, use the OpenClaw or Claude Code install paths above.
|
||||||
|
|
||||||
### Manual
|
### Manual
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/mvanhorn/last30days-skill.git ~/.claude/skills/last30days
|
git clone https://github.com/mvanhorn/last30days-skill.git ~/.claude/skills/last30days
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ metadata:
|
|||||||
- clawhub
|
- clawhub
|
||||||
---
|
---
|
||||||
|
|
||||||
# last30days v2.9.5: Research Any Topic from the Last 30 Days
|
# last30days v3.0.0: Research Any Topic from the Last 30 Days
|
||||||
|
|
||||||
> **Permissions overview:** Reads public web/platform data and optionally saves research briefings to `~/Documents/Last30Days/`. X/Twitter search uses optional user-provided tokens (AUTH_TOKEN/CT0 env vars). Bluesky search uses optional app password (BSKY_HANDLE/BSKY_APP_PASSWORD env vars - create at bsky.app/settings/app-passwords). All credential usage and data writes are documented in the [Security & Permissions](#security--permissions) section.
|
> **Permissions overview:** Reads public web/platform data and optionally saves research briefings to `~/Documents/Last30Days/`. X/Twitter search uses optional user-provided tokens (AUTH_TOKEN/CT0 env vars). Bluesky search uses optional app password (BSKY_HANDLE/BSKY_APP_PASSWORD env vars - create at bsky.app/settings/app-passwords). All credential usage and data writes are documented in the [Security & Permissions](#security--permissions) section.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"topic": "OpenClaw vs NanoClaw vs ZeroClaw",
|
||||||
|
"query_type": "comparison",
|
||||||
|
"rationale": "Multi-entity extraction, 3-way split across AI agent frameworks."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "how to set up a GLP-1 supplement routine",
|
||||||
|
"query_type": "how_to",
|
||||||
|
"rationale": "Trending health topic. Tests non-tech how_to."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "2026 March Madness",
|
||||||
|
"query_type": "breaking_news",
|
||||||
|
"rationale": "Live sporting event. Tests broad breaking news recall."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "best budget noise cancelling headphones 2026",
|
||||||
|
"query_type": "product",
|
||||||
|
"rationale": "Evergreen consumer query. Tests product review aggregation."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "thoughts on OpenAI Codex pricing",
|
||||||
|
"query_type": "opinion",
|
||||||
|
"rationale": "Active developer debate. Tests opinion mining."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "odds of US recession 2026",
|
||||||
|
"query_type": "prediction",
|
||||||
|
"rationale": "Major macro topic. Tests prediction market + news synthesis."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "what is retrieval augmented generation",
|
||||||
|
"query_type": "concept",
|
||||||
|
"rationale": "Widely discussed AI concept. Tests explanation quality."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"topic": "Google Wiz acquisition price and timeline",
|
||||||
|
"query_type": "factual",
|
||||||
|
"rationale": "Completed event ($32B). Tests factual precision."
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -12,7 +12,11 @@ check_perms() {
|
|||||||
local file="$1"
|
local file="$1"
|
||||||
if [[ ! -f "$file" ]]; then return; fi
|
if [[ ! -f "$file" ]]; then return; fi
|
||||||
local perms
|
local perms
|
||||||
perms=$(stat -f '%Lp' "$file" 2>/dev/null || stat -c '%a' "$file" 2>/dev/null || echo "")
|
# Try GNU stat first (Linux), fall back to BSD stat (macOS).
|
||||||
|
# On Linux, `stat -f` prints filesystem info (not permissions) and exits 0,
|
||||||
|
# so the previous BSD-first ordering left $perms as multi-line garbage on
|
||||||
|
# every Linux session start and printed a false WARNING.
|
||||||
|
perms=$(stat -c '%a' "$file" 2>/dev/null || stat -f '%Lp' "$file" 2>/dev/null || echo "")
|
||||||
if [[ -n "$perms" && "$perms" != "600" && "$perms" != "400" ]]; then
|
if [[ -n "$perms" && "$perms" != "600" && "$perms" != "400" ]]; then
|
||||||
echo "/last30days: WARNING — $file has permissions $perms (should be 600)."
|
echo "/last30days: WARNING — $file has permissions $perms (should be 600)."
|
||||||
echo " Fix: chmod 600 $file"
|
echo " Fix: chmod 600 $file"
|
||||||
|
|||||||
+58
-47
@@ -1,75 +1,86 @@
|
|||||||
The AI world reinvents itself every month. This skill keeps you current.
|
The AI world reinvents itself every month. This skill keeps you current.
|
||||||
|
|
||||||
`/last30days` researches your topic across **Reddit, X, YouTube, TikTok, Instagram, Hacker News, Polymarket, and the web** from the last 30 days, finds what the community is actually upvoting, sharing, betting on, and saying on camera, and writes you a grounded narrative with real citations.
|
`/last30days` researches your topic across Reddit, X, YouTube, TikTok, Instagram, Hacker News, Polymarket, GitHub, and 5+ more sources from the last 30 days, finds what the community is actually upvoting, sharing, betting on, and saying on camera, and writes you a grounded narrative with real citations.
|
||||||
|
|
||||||
## v3 Community
|
## v3 is the intelligent search release
|
||||||
|
|
||||||
v3 was shaped by community contributors whose PRs and issues inspired core features. Their code wasn't merged directly (v3 was a ground-up rewrite), but their ideas drove what shipped. See [CONTRIBUTORS.md](CONTRIBUTORS.md) for the full list.
|
v3 is a ground-up engine rewrite by [@j-sperling](https://github.com/j-sperling). The old engine searched keywords. The new engine understands your topic first, then searches the right people and communities.
|
||||||
|
|
||||||
Thanks to @uppinote20, @zerone0x, @thinkun, @thomasmktong, @fanispoulinakisai-boop, @pejmanjohn, @zl190, and @hnshah.
|
Type "OpenClaw" and v3 resolves @steipete, r/openclaw, r/ClaudeCode, and the right YouTube channels and TikTok hashtags before a single API call fires. Type "Peter Steinberger" and it resolves his X handle and GitHub profile, switches to person mode, and shows what he shipped this month at 85% merge rate across 22 PRs. None of that was on Google.
|
||||||
|
|
||||||
## What's New in v2.9.1
|
## Headline features
|
||||||
|
|
||||||
**Auto-save to ~/Documents/Last30Days/.** Every run now saves the complete research briefing - synthesis, stats, and follow-up suggestions - as a topic-named `.md` file to your Documents folder. Build a personal research library without lifting a finger. Inspired by [@devin_explores](https://x.com/devin_explores) who was already doing this manually.
|
### Intelligent pre-research
|
||||||
|
|
||||||
## Three Headline Features in v2.9
|
The killer feature. A new Python pre-research brain resolves X handles, GitHub repos, subreddits, TikTok hashtags, and YouTube channels before searching. Bidirectional: person to company, product to founder, name to GitHub profile. The right subreddits, the right handles, the right hashtags, all resolved before a single API call.
|
||||||
|
|
||||||
**1. ScrapeCreators Reddit as default.** One `SCRAPECREATORS_API_KEY` now covers Reddit, TikTok, and Instagram - three sources, one key. No more `OPENAI_API_KEY` required for Reddit search. Faster, more reliable, and simpler to configure.
|
### Best Takes
|
||||||
|
|
||||||
**2. Smart subreddit discovery.** Relevance-weighted scoring replaces pure frequency count. Each candidate subreddit is scored by `frequency x recency x topic-word match`, and a `UTILITY_SUBS` blocklist filters noise subs like r/tipofmytongue. Search "Claude Code skills" and get r/ClaudeAI, r/ClaudeCode, r/openclaw - not generic programming subs.
|
A second LLM judge scores every result for humor, wit, and virality alongside relevance. Every brief now ends with a Best Takes section surfacing the cleverest one-liners and most viral quotes. The Reddit and X people are funny, and the old engine buried their best stuff.
|
||||||
|
|
||||||
**3. Top comments elevated.** The best comment on each Reddit thread now carries a 10% weight in engagement scoring and displays prominently with upvote counts. Reddit's value is in the comments - now the skill surfaces them.
|
### Cross-source cluster merging
|
||||||
|
|
||||||
Plus: **Instagram Reels** (v2.8), **Polymarket prediction markets** (v2.5), **YouTube transcripts** (v2.1), **bundled X search** - no external CLI needed.
|
When the same story hits Reddit, X, and YouTube, v3 merges them into one cluster instead of three duplicates. Entity-based overlap detection catches matches even when the titles use different words.
|
||||||
|
|
||||||
## Beta Test Results (v2.9)
|
### Single-pass comparisons
|
||||||
|
|
||||||
| Topic | Time | Threads | Discovered Subreddits |
|
"X vs Y" used to run three serial passes (12+ minutes). v3 runs one pass with entity-aware subqueries for both sides at once. Same depth, 3 minutes.
|
||||||
|-------|------|---------|----------------------|
|
|
||||||
| Claude Code skills | 77.1s | 99 | r/ClaudeAI, r/ClaudeCode, r/openclaw |
|
|
||||||
| Kanye West | 71.7s | 84 | r/hiphopheads, r/NFCWestMemeWar, r/Kanye |
|
|
||||||
| Anthropic odds | 68.0s | 65 | r/Anthropic, r/ClaudeAI, r/OpenAI |
|
|
||||||
| Best rap songs lately | 68.9s | 114 | r/BestofRedditorUpdates, r/rap, r/TeenageRapFans |
|
|
||||||
| Nano Banana Pro | 66.6s | 99 | r/GeminiAI, r/nanobanana2pro, r/macbookpro |
|
|
||||||
|
|
||||||
## What's New
|
### GitHub person-mode and project-mode
|
||||||
|
|
||||||
### Added
|
When the topic is a person, the engine switches from keyword search to author-scoped queries. PR velocity, top repos by stars, release notes for what shipped this month, woven into the narrative alongside X posts and Reddit threads.
|
||||||
- ScrapeCreators Reddit backend with keyword search and subreddit discovery
|
|
||||||
- Smart subreddit discovery with relevance-weighted scoring
|
|
||||||
- Utility subreddit blocklist (`UTILITY_SUBS`)
|
|
||||||
- Top comment scoring (10% engagement weight) and prominent rendering
|
|
||||||
- Comment excerpts increased to 400 chars, insights raised to 10
|
|
||||||
|
|
||||||
### Changed
|
When the topic is a project, it pulls live star counts, READMEs, releases, and top issues from the GitHub API. No stale blog posts.
|
||||||
- `primaryEnv` → `SCRAPECREATORS_API_KEY` (one key for Reddit, TikTok, Instagram)
|
|
||||||
- Reddit engagement scoring: `0.55/0.40/0.05` → `0.50/0.35/0.05/0.10`
|
|
||||||
- SKILL.md synthesis instructions emphasize quoting top comments
|
|
||||||
|
|
||||||
### Fixed
|
### ELI5 mode
|
||||||
- Utility sub noise in subreddit discovery
|
|
||||||
- Reddit no longer requires `OPENAI_API_KEY`
|
|
||||||
|
|
||||||
## New Contributors
|
Say "eli5 on" after any research run. The synthesis rewrites in plain language. No jargon. Same data, same sources, same citations, just clearer. Say "eli5 off" to go back.
|
||||||
|
|
||||||
- @JosephOIbrahim -- Windows Unicode fix ([#17](https://github.com/mvanhorn/last30days-skill/pull/17))
|
### 13+ sources
|
||||||
- @levineam -- Model fallback for unverified orgs ([#16](https://github.com/mvanhorn/last30days-skill/pull/16))
|
|
||||||
- @jonthebeef -- `--days=N` configurable lookback ([#18](https://github.com/mvanhorn/last30days-skill/pull/18))
|
|
||||||
|
|
||||||
## Credits
|
v3 adds Threads, Pinterest, Perplexity, Bluesky, and Parallel AI grounding to the existing Reddit, X, YouTube, TikTok, Instagram, Hacker News, Polymarket, GitHub, and Web lineup. Perplexity Deep Research (`--deep-research`) gives you 50+ citation reports for serious investigation.
|
||||||
|
|
||||||
- [@steipete](https://github.com/steipete) -- Bird CLI (vendored X search) and yt-dlp/summarize inspiration for YouTube transcripts
|
### Per-author cap and entity disambiguation
|
||||||
- [@galligan](https://github.com/galligan) -- Marketplace plugin inspiration
|
|
||||||
- [@hutchins](https://x.com/hutchins) -- Pushed for YouTube feature
|
Max 3 items per author prevents single-voice dominance. Synthesis trusts resolved handles over fuzzy keyword matches.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
```bash
|
Claude Code:
|
||||||
# Claude Code
|
|
||||||
git clone https://github.com/mvanhorn/last30days-skill.git ~/.claude/skills/last30days
|
|
||||||
|
|
||||||
# Codex CLI
|
```
|
||||||
git clone https://github.com/mvanhorn/last30days-skill.git ~/.agents/skills/last30days
|
/plugin marketplace add mvanhorn/last30days-skill
|
||||||
```
|
```
|
||||||
|
|
||||||
30 days of research. 30 seconds of work. Eight sources. Zero stale prompts.
|
OpenClaw:
|
||||||
|
|
||||||
|
```
|
||||||
|
clawhub install last30days-official
|
||||||
|
```
|
||||||
|
|
||||||
|
OpenAI Codex CLI: run `codex` from a checkout of this repo and v3's skill at `.agents/skills/last30days/SKILL.md` will be discovered automatically. Or copy `SKILL.md` to `~/.agents/skills/last30days/SKILL.md` for a global install.
|
||||||
|
|
||||||
|
Zero config. Reddit, Hacker News, Polymarket, and GitHub work immediately. Run it once and the setup wizard unlocks X, YouTube, TikTok, and more in 30 seconds.
|
||||||
|
|
||||||
|
## v3 Community
|
||||||
|
|
||||||
|
v3 was shaped by community contributors whose PRs and issues inspired core features. Their code wasn't merged directly (v3 was a ground-up rewrite), but their ideas drove what shipped.
|
||||||
|
|
||||||
|
Thanks to @uppinote20, @zerone0x, @thinkun, @thomasmktong, @fanispoulinakisai-boop, @pejmanjohn, @zl190, and @hnshah. See [CONTRIBUTORS.md](CONTRIBUTORS.md) for the full list.
|
||||||
|
|
||||||
|
Contributors who shaped the release itself:
|
||||||
|
|
||||||
|
- @Jah-yee (#153) surfaced the need for a real Codex CLI integration, which shipped in #219
|
||||||
|
- @Cody-Coyote (#204) reported the marketplace validation bug that needed fixing before v3 could ship cleanly
|
||||||
|
- @dannyshmueli pushed for v3 and Codex family support publicly on X
|
||||||
|
|
||||||
|
Full Added / Changed / Fixed detail lives in [CHANGELOG.md](CHANGELOG.md) under `[3.0.0]`.
|
||||||
|
|
||||||
|
## Earlier contributors
|
||||||
|
|
||||||
|
From the v1 and v2 lineage:
|
||||||
|
|
||||||
|
- [@galligan](https://github.com/galligan) for marketplace plugin inspiration
|
||||||
|
- [@hutchins](https://x.com/hutchins) for pushing the YouTube feature
|
||||||
|
|
||||||
|
30 days of research. 30 seconds of work. Thirteen sources. Zero stale prompts.
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ def save_output(report: schema.Report, emit: str, save_dir: str, suffix: str = "
|
|||||||
content = emit_output(report, emit)
|
content = emit_output(report, emit)
|
||||||
else:
|
else:
|
||||||
content = render.render_full(report)
|
content = render.render_full(report)
|
||||||
out_path.write_text(content)
|
out_path.write_text(content, encoding="utf-8")
|
||||||
return out_path
|
return out_path
|
||||||
|
|
||||||
|
|
||||||
@@ -165,7 +165,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
parser.add_argument("--tiktok-hashtags", help="Comma-separated TikTok hashtags without # (e.g., tella,screenrecording)")
|
parser.add_argument("--tiktok-hashtags", help="Comma-separated TikTok hashtags without # (e.g., tella,screenrecording)")
|
||||||
parser.add_argument("--tiktok-creators", help="Comma-separated TikTok creator handles (e.g., TellaHQ,taborplace)")
|
parser.add_argument("--tiktok-creators", help="Comma-separated TikTok creator handles (e.g., TellaHQ,taborplace)")
|
||||||
parser.add_argument("--ig-creators", help="Comma-separated Instagram creator handles (e.g., tella.tv,laborstories)")
|
parser.add_argument("--ig-creators", help="Comma-separated Instagram creator handles (e.g., tella.tv,laborstories)")
|
||||||
parser.add_argument("--lookback-days", type=int, default=30, help="Number of days to look back for research (default: 30, watchlist uses 90)")
|
parser.add_argument(
|
||||||
|
"--days",
|
||||||
|
"--lookback-days",
|
||||||
|
dest="lookback_days",
|
||||||
|
type=int,
|
||||||
|
default=30,
|
||||||
|
help="Number of days to look back for research (default: 30, watchlist uses 90)",
|
||||||
|
)
|
||||||
parser.add_argument("--auto-resolve", action="store_true",
|
parser.add_argument("--auto-resolve", action="store_true",
|
||||||
help="Use web search to discover subreddits/handles before planning (for platforms without WebSearch)")
|
help="Use web search to discover subreddits/handles before planning (for platforms without WebSearch)")
|
||||||
parser.add_argument("--github-user", help="GitHub username for person-mode search (e.g., steipete)")
|
parser.add_argument("--github-user", help="GitHub username for person-mode search (e.g., steipete)")
|
||||||
|
|||||||
@@ -460,7 +460,7 @@ def parse_bird_response(response: Dict[str, Any], query: str = "") -> List[Dict[
|
|||||||
"url": url,
|
"url": url,
|
||||||
"author_handle": author_handle.lstrip("@"),
|
"author_handle": author_handle.lstrip("@"),
|
||||||
"date": date,
|
"date": date,
|
||||||
"engagement": engagement,
|
"engagement": engagement if any(v is not None for v in engagement.values()) else None,
|
||||||
"why_relevant": "", # Bird doesn't provide relevance explanations
|
"why_relevant": "", # Bird doesn't provide relevance explanations
|
||||||
"relevance": _compute_relevance(query, str(tweet.get("text", ""))) if query else 0.7,
|
"relevance": _compute_relevance(query, str(tweet.get("text", ""))) if query else 0.7,
|
||||||
}
|
}
|
||||||
|
|||||||
+63
-4
@@ -11,7 +11,7 @@ COMMON_TARGETS=(
|
|||||||
# but local development needs the cache kept in sync with the repo.
|
# but local development needs the cache kept in sync with the repo.
|
||||||
# Do NOT add ~/.claude/skills/last30days - it creates a duplicate
|
# Do NOT add ~/.claude/skills/last30days - it creates a duplicate
|
||||||
# /last30days-3 in the slash command menu alongside the plugin version.
|
# /last30days-3 in the slash command menu alongside the plugin version.
|
||||||
"$HOME/.claude/plugins/cache/last30days-skill-private/last30days-3/3.0.0-alpha"
|
"$HOME/.claude/plugins/cache/last30days-skill-private/last30days-3/3.0.0"
|
||||||
"$HOME/.claude/plugins/cache/last30days-skill-private/last30days-3-nogem/3.0.0-nogem"
|
"$HOME/.claude/plugins/cache/last30days-skill-private/last30days-3-nogem/3.0.0-nogem"
|
||||||
"$HOME/.agents/skills/last30days"
|
"$HOME/.agents/skills/last30days"
|
||||||
"$HOME/.codex/skills/last30days"
|
"$HOME/.codex/skills/last30days"
|
||||||
@@ -24,7 +24,7 @@ sync_target() {
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "--- Syncing to $target ---"
|
echo "--- Syncing to $target ---"
|
||||||
mkdir -p "$target/scripts/lib" "$target/variants/open/references"
|
mkdir -p "$target/scripts/lib"
|
||||||
|
|
||||||
cp "$skill_md" "$target/SKILL.md"
|
cp "$skill_md" "$target/SKILL.md"
|
||||||
|
|
||||||
@@ -35,7 +35,13 @@ sync_target() {
|
|||||||
"$SRC/scripts/store.py" \
|
"$SRC/scripts/store.py" \
|
||||||
"$target/scripts/"
|
"$target/scripts/"
|
||||||
rsync -a "$SRC/scripts/lib/"*.py "$target/scripts/lib/"
|
rsync -a "$SRC/scripts/lib/"*.py "$target/scripts/lib/"
|
||||||
rsync -a "$SRC/variants/open/" "$target/variants/open/"
|
|
||||||
|
# The OpenClaw variant lives in the private repo only. Skip cleanly when
|
||||||
|
# running this script from the public repo where variants/open does not exist.
|
||||||
|
if [ -d "$SRC/variants/open" ]; then
|
||||||
|
mkdir -p "$target/variants/open/references"
|
||||||
|
rsync -a "$SRC/variants/open/" "$target/variants/open/"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -d "$SRC/scripts/lib/vendor" ]; then
|
if [ -d "$SRC/scripts/lib/vendor" ]; then
|
||||||
rsync -a "$SRC/scripts/lib/vendor" "$target/scripts/lib/"
|
rsync -a "$SRC/scripts/lib/vendor" "$target/scripts/lib/"
|
||||||
@@ -63,7 +69,60 @@ for t in "${COMMON_TARGETS[@]}"; do
|
|||||||
sync_target "$t" "$SRC/SKILL.md"
|
sync_target "$t" "$SRC/SKILL.md"
|
||||||
done
|
done
|
||||||
|
|
||||||
sync_target "$OPENCLAW_TARGET" "$SRC/variants/open/SKILL.md"
|
# Hermes sync: deploy to Hermes skills directory if it exists
|
||||||
|
HERMES_TARGET="$HOME/.hermes/skills/research/last30days"
|
||||||
|
if [ -d "$HOME/.hermes/skills/research" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "--- Syncing to Hermes ---"
|
||||||
|
mkdir -p "$HERMES_TARGET/scripts/lib"
|
||||||
|
|
||||||
|
# Use Hermes-specific SKILL.md if available, fallback to main
|
||||||
|
if [ -f "$SRC/.hermes-plugin/SKILL.md" ]; then
|
||||||
|
cp "$SRC/.hermes-plugin/SKILL.md" "$HERMES_TARGET/SKILL.md"
|
||||||
|
else
|
||||||
|
cp "$SRC/SKILL.md" "$HERMES_TARGET/SKILL.md"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rsync -a \
|
||||||
|
"$SRC/scripts/last30days.py" \
|
||||||
|
"$SRC/scripts/watchlist.py" \
|
||||||
|
"$SRC/scripts/briefing.py" \
|
||||||
|
"$SRC/scripts/store.py" \
|
||||||
|
"$HERMES_TARGET/scripts/"
|
||||||
|
rsync -a "$SRC/scripts/lib/"*.py "$HERMES_TARGET/scripts/lib/"
|
||||||
|
|
||||||
|
if [ -d "$SRC/scripts/lib/vendor" ]; then
|
||||||
|
rsync -a "$SRC/scripts/lib/vendor" "$HERMES_TARGET/scripts/lib/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d "$SRC/fixtures" ]; then
|
||||||
|
mkdir -p "$HERMES_TARGET/fixtures"
|
||||||
|
rsync -a "$SRC/fixtures/" "$HERMES_TARGET/fixtures/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mod_count=$(ls "$HERMES_TARGET/scripts/lib/"*.py 2>/dev/null | wc -l | tr -d ' ')
|
||||||
|
echo " Copied $mod_count modules to Hermes"
|
||||||
|
|
||||||
|
if (
|
||||||
|
cd "$HERMES_TARGET/scripts" &&
|
||||||
|
python3 -c "import briefing, store, watchlist; from lib import youtube_yt, bird_x, render, ui; print(' Import check: OK')"
|
||||||
|
); then
|
||||||
|
true
|
||||||
|
else
|
||||||
|
echo " Import check FAILED"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# OpenClaw sync only runs when the private-repo OpenClaw variant is present
|
||||||
|
# in the source tree. The public repo does not ship variants/open (the variant
|
||||||
|
# is sanitized via strip_for_openclaw.py and published separately from
|
||||||
|
# last30days-skill-private).
|
||||||
|
if [ -d "$SRC/variants/open" ]; then
|
||||||
|
sync_target "$OPENCLAW_TARGET" "$SRC/variants/open/SKILL.md"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "Skipping OpenClaw target (no variants/open in this repo)"
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Sync complete."
|
echo "Sync complete."
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
---
|
---
|
||||||
name: last30days
|
name: last30days-v3-spec
|
||||||
version: "3.0.0"
|
version: "3.0.0"
|
||||||
description: "Multi-query social search with intelligent planning. Agent plans queries when possible, falls back to Gemini/OpenAI when not. Research any topic across Reddit, X, YouTube, TikTok, Instagram, Hacker News, Polymarket, and the web."
|
description: "Internal architecture spec for the v3 last30days runtime pipeline. Not user-invocable."
|
||||||
argument-hint: "last30days codex vs claude code"
|
argument-hint: "last30days codex vs claude code"
|
||||||
allowed-tools: Bash, Read, Write, WebSearch
|
allowed-tools: Bash, Read, Write, WebSearch
|
||||||
homepage: https://github.com/mvanhorn/last30days-skill
|
homepage: https://github.com/mvanhorn/last30days-skill
|
||||||
repository: https://github.com/mvanhorn/last30days-skill
|
repository: https://github.com/mvanhorn/last30days-skill
|
||||||
author: mvanhorn
|
author: mvanhorn
|
||||||
license: MIT
|
license: MIT
|
||||||
user-invocable: true
|
user-invocable: false
|
||||||
---
|
---
|
||||||
|
|
||||||
# last30days v3.0.0
|
# last30days v3.0.0
|
||||||
@@ -86,7 +86,6 @@ fi
|
|||||||
- `yt-dlp` enables YouTube.
|
- `yt-dlp` enables YouTube.
|
||||||
- Planning and reranking fall back gracefully: Gemini -> OpenAI -> xAI -> deterministic/local.
|
- Planning and reranking fall back gracefully: Gemini -> OpenAI -> xAI -> deterministic/local.
|
||||||
- Web retrieval stays within Brave/Serper dated results. Undated web hits are dropped.
|
- Web retrieval stays within Brave/Serper dated results. Undated web hits are dropped.
|
||||||
- For OpenClaw-specific watchlist, briefing, and history workflows, use `variants/open/SKILL.md`.
|
|
||||||
|
|
||||||
## Output model
|
## Output model
|
||||||
|
|
||||||
|
|||||||
+27
-1
@@ -175,7 +175,7 @@ class TestVendoredBirdRuntime(unittest.TestCase):
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
items = parse_bird_response(tweets, "test query")
|
items = parse_bird_response(tweets, "test query")
|
||||||
self.assertIsNone(items[0]["engagement"]["likes"])
|
self.assertIsNone(items[0]["engagement"])
|
||||||
|
|
||||||
def test_fallback_to_second_key(self):
|
def test_fallback_to_second_key(self):
|
||||||
tweets = [
|
tweets = [
|
||||||
@@ -203,6 +203,32 @@ class TestVendoredBirdRuntime(unittest.TestCase):
|
|||||||
items = parse_bird_response(tweets, "test query")
|
items = parse_bird_response(tweets, "test query")
|
||||||
self.assertEqual(0, items[0]["engagement"]["likes"])
|
self.assertEqual(0, items[0]["engagement"]["likes"])
|
||||||
|
|
||||||
|
def test_engagement_none_when_all_fields_missing(self):
|
||||||
|
"""All-None engagement dict should become None, not propagate."""
|
||||||
|
tweets = [
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"text": "test",
|
||||||
|
"permanent_url": "https://x.com/u/status/1",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
items = parse_bird_response(tweets, "test query")
|
||||||
|
self.assertIsNone(items[0]["engagement"])
|
||||||
|
|
||||||
|
def test_engagement_preserved_when_any_field_present(self):
|
||||||
|
"""Engagement dict kept when at least one metric exists."""
|
||||||
|
tweets = [
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"text": "test",
|
||||||
|
"permanent_url": "https://x.com/u/status/1",
|
||||||
|
"likeCount": 5,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
items = parse_bird_response(tweets, "test query")
|
||||||
|
self.assertIsNotNone(items[0]["engagement"])
|
||||||
|
self.assertEqual(5, items[0]["engagement"]["likes"])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -77,6 +77,13 @@ class CliV3Tests(unittest.TestCase):
|
|||||||
with self.assertRaises(SystemExit):
|
with self.assertRaises(SystemExit):
|
||||||
cli.parse_search_flag(" , ")
|
cli.parse_search_flag(" , ")
|
||||||
|
|
||||||
|
def test_build_parser_accepts_days_alias_and_preserves_topic_tokens(self):
|
||||||
|
parser = cli.build_parser()
|
||||||
|
args, extra = parser.parse_known_args(["--days", "7", "biosecurity", "ai", "agents"])
|
||||||
|
self.assertEqual(7, args.lookback_days)
|
||||||
|
self.assertEqual(["biosecurity", "ai", "agents"], args.topic)
|
||||||
|
self.assertEqual([], extra)
|
||||||
|
|
||||||
def test_ensure_supported_python_rejects_old_interpreter_with_actionable_error(self):
|
def test_ensure_supported_python_rejects_old_interpreter_with_actionable_error(self):
|
||||||
stderr = io.StringIO()
|
stderr = io.StringIO()
|
||||||
with redirect_stderr(stderr):
|
with redirect_stderr(stderr):
|
||||||
@@ -128,6 +135,14 @@ class CliV3Tests(unittest.TestCase):
|
|||||||
payload = json.loads(path.read_text())
|
payload = json.loads(path.read_text())
|
||||||
self.assertEqual("OpenClaw vs NanoClaw", payload["topic"])
|
self.assertEqual("OpenClaw vs NanoClaw", payload["topic"])
|
||||||
|
|
||||||
|
def test_save_output_writes_utf8_encoded_markdown(self):
|
||||||
|
report = self.make_report()
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
with mock.patch("pathlib.Path.write_text", autospec=True, return_value=1) as write_text:
|
||||||
|
cli.save_output(report, "md", tmp)
|
||||||
|
_, kwargs = write_text.call_args
|
||||||
|
self.assertEqual("utf-8", kwargs.get("encoding"))
|
||||||
|
|
||||||
def test_persist_report_updates_run_status_on_success_and_failure(self):
|
def test_persist_report_updates_run_status_on_success_and_failure(self):
|
||||||
report = self.make_report()
|
report = self.make_report()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import re
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def _skill_version() -> str:
|
||||||
|
text = (ROOT / "SKILL.md").read_text(encoding="utf-8")
|
||||||
|
match = re.search(r'^version:\s*"([^"]+)"\s*$', text, re.MULTILINE)
|
||||||
|
if not match:
|
||||||
|
raise AssertionError("SKILL.md version frontmatter not found")
|
||||||
|
return match.group(1)
|
||||||
|
|
||||||
|
|
||||||
|
class TestVersionConsistency(unittest.TestCase):
|
||||||
|
def test_root_skill_header_matches_frontmatter_version(self) -> None:
|
||||||
|
text = (ROOT / "SKILL.md").read_text(encoding="utf-8")
|
||||||
|
version = _skill_version()
|
||||||
|
self.assertIn(f"# last30days v{version}:", text)
|
||||||
|
|
||||||
|
def test_sync_cache_path_uses_skill_version(self) -> None:
|
||||||
|
sync_text = (ROOT / "scripts" / "sync.sh").read_text(encoding="utf-8")
|
||||||
|
version = _skill_version()
|
||||||
|
self.assertIn(f'last30days-3/{version}"', sync_text)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user