From 57ec92c2990ac08d2204f645b47cae4fc0cd49ac Mon Sep 17 00:00:00 2001 From: Pejman Pour-Moezzi Date: Wed, 8 Apr 2026 14:40:19 -0700 Subject: [PATCH] last30days: require Python 3.12 in setup flows --- SKILL.md | 30 ++++++++++++++++++++++++------ scripts/last30days.py | 19 +++++++++++++++++++ skills/last30days-v3/SKILL.md | 26 +++++++++++++++++++------- tests/test_cli_v3.py | 14 ++++++++++++++ 4 files changed, 76 insertions(+), 13 deletions(-) diff --git a/SKILL.md b/SKILL.md index a349841..f85ab9d 100644 --- a/SKILL.md +++ b/SKILL.md @@ -65,6 +65,24 @@ metadata: 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. @@ -82,7 +100,7 @@ To detect first run: check if `~/.config/last30days/.env` exists. If it does NOT Run environment detection first: ```bash -python3 "${SKILL_ROOT}/scripts/last30days.py" setup --openclaw +"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" setup --openclaw ``` Read the JSON output. It tells you what's already configured. Display a status summary: @@ -100,7 +118,7 @@ Detected: 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". Before running the command, 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 / Ctrl+V)." Then run `python3 "${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={api_key}` to .env. + - 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". Before running the command, 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 / Ctrl+V)." 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={api_key}` to .env. - Option B: "I have a key" -- accept paste, write to .env - Option C: "Skip for now" @@ -160,7 +178,7 @@ Options: Run the setup subcommand: ```bash -cd {SKILL_DIR} && python3 scripts/last30days.py setup +cd {SKILL_DIR} && "${LAST30DAYS_PYTHON}" scripts/last30days.py setup ``` Show the user the results (what cookies were found, whether yt-dlp was installed). @@ -173,7 +191,7 @@ Want TikTok and Instagram too? ScrapeCreators adds those platforms - 10,000 free **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} && python3 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={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. +- "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={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={key} to ~/.config/last30days/.env - "I have a key" - accept the key, write to .env - "Skip for now" - proceed without ScrapeCreators @@ -541,7 +559,7 @@ When the user asks "X vs Y", run ONE research pass with a comparison-optimized p **Single pass with entity-aware subqueries:** ```bash -python3 "${SKILL_ROOT}/scripts/last30days.py" "{TOPIC_A} vs {TOPIC_B}" --emit=compact --save-dir=~/Documents/Last30Days --save-suffix=v3 --plan 'COMPARISON_PLAN_JSON' --x-handle={TOPIC_A_HANDLE} --x-related={TOPIC_B_HANDLE},{COMPANY_A_HANDLE},{COMPANY_B_HANDLE},{COMMENTATOR_HANDLES} --subreddits={RESOLVED_SUBREDDITS} --tiktok-hashtags={RESOLVED_HASHTAGS} --tiktok-creators={RESOLVED_TIKTOK_CREATORS} --ig-creators={RESOLVED_IG_CREATORS} +"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" "{TOPIC_A} vs {TOPIC_B}" --emit=compact --save-dir=~/Documents/Last30Days --save-suffix=v3 --plan 'COMPARISON_PLAN_JSON' --x-handle={TOPIC_A_HANDLE} --x-related={TOPIC_B_HANDLE},{COMPANY_A_HANDLE},{COMPANY_B_HANDLE},{COMMENTATOR_HANDLES} --subreddits={RESOLVED_SUBREDDITS} --tiktok-hashtags={RESOLVED_HASHTAGS} --tiktok-creators={RESOLVED_TIKTOK_CREATORS} --ig-creators={RESOLVED_IG_CREATORS} ``` **The `--plan` JSON for comparisons should include 3-4 subqueries:** @@ -730,7 +748,7 @@ if [ -z "${SKILL_ROOT:-}" ]; then exit 1 fi -python3 "${SKILL_ROOT}/scripts/last30days.py" $ARGUMENTS --emit=compact --save-dir=~/Documents/Last30Days --save-suffix=v3 +"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" $ARGUMENTS --emit=compact --save-dir=~/Documents/Last30Days --save-suffix=v3 ``` **If you ran Steps 0.55 and 0.75 (agent planning), add these flags:** diff --git a/scripts/last30days.py b/scripts/last30days.py index d5b5b87..03b8a8f 100644 --- a/scripts/last30days.py +++ b/scripts/last30days.py @@ -14,6 +14,25 @@ import sys import threading from pathlib import Path +MIN_PYTHON = (3, 12) + + +def ensure_supported_python(version_info: tuple[int, int, int] | object | None = None) -> None: + if version_info is None: + version_info = sys.version_info + major, minor, micro = tuple(version_info[:3]) + if (major, minor) >= MIN_PYTHON: + return + sys.stderr.write( + "last30days v3 requires Python 3.12+.\n" + f"Detected Python {major}.{minor}.{micro}.\n" + "Install and use python3.12 or python3.13, then rerun this command.\n" + ) + raise SystemExit(1) + + +ensure_supported_python() + SCRIPT_DIR = Path(__file__).parent.resolve() sys.path.insert(0, str(SCRIPT_DIR)) diff --git a/skills/last30days-v3/SKILL.md b/skills/last30days-v3/SKILL.md index 53e7e93..a5d38b5 100644 --- a/skills/last30days-v3/SKILL.md +++ b/skills/last30days-v3/SKILL.md @@ -45,23 +45,35 @@ if [ -z "${SKILL_ROOT:-}" ]; then echo "ERROR: Could not find scripts/last30days.py" >&2 exit 1 fi + +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 ``` ## Default command ```bash -python3 "${SKILL_ROOT}/scripts/last30days.py" $ARGUMENTS --emit=compact +"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" $ARGUMENTS --emit=compact ``` ## Useful commands ```bash -python3 "${SKILL_ROOT}/scripts/last30days.py" $ARGUMENTS --emit=json -python3 "${SKILL_ROOT}/scripts/last30days.py" $ARGUMENTS --quick -python3 "${SKILL_ROOT}/scripts/last30days.py" $ARGUMENTS --deep -python3 "${SKILL_ROOT}/scripts/last30days.py" $ARGUMENTS --search=reddit,x,grounding -python3 "${SKILL_ROOT}/scripts/last30days.py" $ARGUMENTS --store -python3 "${SKILL_ROOT}/scripts/last30days.py" --diagnose +"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" $ARGUMENTS --emit=json +"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" $ARGUMENTS --quick +"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" $ARGUMENTS --deep +"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" $ARGUMENTS --search=reddit,x,grounding +"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" $ARGUMENTS --store +"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" --diagnose ``` ## Runtime expectations diff --git a/tests/test_cli_v3.py b/tests/test_cli_v3.py index b578538..538e36f 100644 --- a/tests/test_cli_v3.py +++ b/tests/test_cli_v3.py @@ -77,6 +77,20 @@ class CliV3Tests(unittest.TestCase): with self.assertRaises(SystemExit): cli.parse_search_flag(" , ") + def test_ensure_supported_python_rejects_old_interpreter_with_actionable_error(self): + stderr = io.StringIO() + with redirect_stderr(stderr): + with self.assertRaises(SystemExit) as exc: + cli.ensure_supported_python((3, 9, 6)) + self.assertEqual(1, exc.exception.code) + message = stderr.getvalue() + self.assertIn("last30days v3 requires Python 3.12+", message) + self.assertIn("Detected Python 3.9.6", message) + self.assertIn("python3.12", message) + + def test_ensure_supported_python_allows_supported_interpreter(self): + cli.ensure_supported_python((3, 12, 0)) + def test_missing_sources_for_promo_prefers_reddit_x_then_web(self): self.assertEqual( "both",