diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index f51866e..90b1b85 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,16 +1,17 @@ { - "$schema": "https://anthropic.com/claude-code/marketplace.schema.json", "name": "last30days-skill", - "description": "Research any topic across Reddit, X, YouTube, TikTok, Instagram, HN, Polymarket, GitHub, and 5+ more sources.", "owner": { "name": "Matt Van Horn", "url": "https://github.com/mvanhorn" }, + "metadata": { + "description": "Marketplace hosting the Last 30 Days research plugin." + }, "plugins": [ { "name": "last30days", "description": "Research any topic across Reddit, X, YouTube, TikTok, Instagram, HN, Polymarket, GitHub, and 5+ more sources.", - "version": "3.0.10", + "version": "3.1.1", "author": { "name": "Matt Van Horn", "url": "https://github.com/mvanhorn" diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index b5ecc61..8a2972a 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "last30days", - "version": "3.1.0", + "version": "3.1.1", "description": "Research any topic across Reddit, X, YouTube, TikTok, Instagram, Hacker News, Polymarket, GitHub, and 5+ more sources. AI agent scores by upvotes, likes, and real money - not editors.", "author": { "name": "Matt Van Horn", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 8399d76..88ba9e7 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "last30days", - "version": "3.0.10", + "version": "3.1.1", "description": "Research any topic across Reddit, X, YouTube, TikTok, Instagram, Hacker News, Polymarket, GitHub, and more sources.", "author": { "name": "Matt Van Horn", diff --git a/CHANGELOG.md b/CHANGELOG.md index fe0ec3e..617b087 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ 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/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.1.1] - 2026-04-24 + +### Fixed + +- **Codex plugin layout.** Move the canonical runtime payload under `skills/last30days/` and update Codex/Claude plugin metadata and tests for the relocated engine path. +- **Claude Code cache resolution.** Resolve Claude plugin installs to `skills/last30days/scripts/last30days.py` after the plugin-layout restructure. + ## [3.1.0] - 2026-04-22 Consolidates the 3.0.10 to 3.0.14 dev cycle (commenter handles, `--competitors`, per-entity Step 0.55, vs-mode N passes, comparison title attribution) and republishes the OpenClaw bundle, which had been frozen on ClawHub at `3.0.0-open` since April 8. diff --git a/pyproject.toml b/pyproject.toml index 0b2548f..8b5da56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "last30days-skill" -version = "3.0.10" +version = "3.1.1" description = "Multi-source last-30-days research skill" readme = "README.md" requires-python = ">=3.12" diff --git a/skills/last30days/SKILL.md b/skills/last30days/SKILL.md index 89c007a..e534415 100644 --- a/skills/last30days/SKILL.md +++ b/skills/last30days/SKILL.md @@ -1,6 +1,6 @@ --- name: last30days -version: "3.0.10" +version: "3.1.1" description: "Research what people actually say about any topic in the last 30 days. Pulls posts and engagement from Reddit, X, YouTube, TikTok, Hacker News, Polymarket, GitHub, and the web." argument-hint: 'last30days nvidia earnings reaction | last30days AI video tools | last30days what users want in react' allowed-tools: Bash, Read, Write, AskUserQuestion, WebSearch @@ -233,7 +233,7 @@ If your Bash call to `last30days.py` does NOT include the FULL pre-flight checkl --- -# last30days v3.0.10: Research Any Topic from the Last 30 Days +# last30days v3.1.1: Research Any Topic from the Last 30 Days > **Permissions overview:** Reads public web/platform data and optionally saves research briefings to `LAST30DAYS_MEMORY_DIR` (defaults 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. @@ -877,7 +877,13 @@ SKILL_ROOT="${SKILL_ROOT%/}" if [ -z "$SKILL_ROOT" ] || [ ! -f "$SKILL_ROOT/scripts/last30days.py" ]; then CLAUDE_PLUGIN_ROOT="$(ls -d "$HOME/.claude/plugins/cache/last30days-skill/last30days/"*/ 2>/dev/null | sort -V | tail -1)" CLAUDE_PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT%/}" - [ -n "$CLAUDE_PLUGIN_ROOT" ] && [ -f "$CLAUDE_PLUGIN_ROOT/scripts/last30days.py" ] && SKILL_ROOT="$CLAUDE_PLUGIN_ROOT" + if [ -n "$CLAUDE_PLUGIN_ROOT" ]; then + if [ -f "$CLAUDE_PLUGIN_ROOT/skills/last30days/scripts/last30days.py" ]; then + SKILL_ROOT="$CLAUDE_PLUGIN_ROOT/skills/last30days" + elif [ -f "$CLAUDE_PLUGIN_ROOT/scripts/last30days.py" ]; then + SKILL_ROOT="$CLAUDE_PLUGIN_ROOT" + fi + fi fi # Fallback for repo checkout / Gemini / local development hosts where the plugin cache does not exist. @@ -890,7 +896,7 @@ fi if [ -z "${SKILL_ROOT:-}" ] || [ ! -f "$SKILL_ROOT/scripts/last30days.py" ]; then echo "ERROR: Could not find scripts/last30days.py in Codex/Claude plugin cache or repo checkout" >&2 echo "Expected Codex: $HOME/.codex/plugins/cache/{MARKETPLACE}/last30days/{VERSION}/skills/last30days/scripts/last30days.py" >&2 - echo "Expected Claude: $HOME/.claude/plugins/cache/last30days-skill/last30days/{VERSION}/scripts/last30days.py" >&2 + echo "Expected Claude: $HOME/.claude/plugins/cache/last30days-skill/last30days/{VERSION}/skills/last30days/scripts/last30days.py" >&2 exit 1 fi diff --git a/skills/last30days/scripts/sync.sh b/skills/last30days/scripts/sync.sh index cbc7247..e5dfd9c 100755 --- a/skills/last30days/scripts/sync.sh +++ b/skills/last30days/scripts/sync.sh @@ -11,7 +11,7 @@ COMMON_TARGETS=( # but local development needs the cache kept in sync with the repo. # Do NOT add ~/.claude/skills/last30days - it creates a duplicate # /last30days-3 in the slash command menu alongside the plugin version. - "$HOME/.claude/plugins/cache/last30days-skill-private/last30days-3/3.0.10" + "$HOME/.claude/plugins/cache/last30days-skill-private/last30days-3/3.1.1" "$HOME/.claude/plugins/cache/last30days-skill-private/last30days-3-nogem/3.0.0-nogem" "$HOME/.agents/skills/last30days" "$HOME/.codex/skills/last30days" diff --git a/tests/test_chrome_cookies.py b/tests/test_chrome_cookies.py index 09aa510..901b259 100644 --- a/tests/test_chrome_cookies.py +++ b/tests/test_chrome_cookies.py @@ -4,12 +4,15 @@ import hashlib import os import sqlite3 import subprocess +import sys import tempfile from pathlib import Path from unittest import mock import pytest +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days")) + from scripts.lib.chrome_cookies import ( CHROME_COOKIES_DB, CHROME_IV_HEX, diff --git a/tests/test_cli_competitors.py b/tests/test_cli_competitors.py index 70e7415..8e8b685 100644 --- a/tests/test_cli_competitors.py +++ b/tests/test_cli_competitors.py @@ -10,7 +10,7 @@ from contextlib import redirect_stderr from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "scripts")) +sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts")) import last30days as cli diff --git a/tests/test_competitor_fanout.py b/tests/test_competitor_fanout.py index acaa1ce..e11dd89 100644 --- a/tests/test_competitor_fanout.py +++ b/tests/test_competitor_fanout.py @@ -13,7 +13,7 @@ from pathlib import Path from unittest import mock REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "scripts")) +sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts")) from lib import fanout diff --git a/tests/test_competitor_subrun_isolation.py b/tests/test_competitor_subrun_isolation.py index b6025ff..b8291c4 100644 --- a/tests/test_competitor_subrun_isolation.py +++ b/tests/test_competitor_subrun_isolation.py @@ -17,7 +17,7 @@ from pathlib import Path from unittest import mock REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "scripts")) +sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts")) def _fake_report(topic: str): diff --git a/tests/test_competitors.py b/tests/test_competitors.py index 4bba9fc..49a6534 100644 --- a/tests/test_competitors.py +++ b/tests/test_competitors.py @@ -11,7 +11,7 @@ from pathlib import Path from unittest import mock REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "scripts")) +sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts")) from lib import competitors diff --git a/tests/test_competitors_plan_threading.py b/tests/test_competitors_plan_threading.py index 8b8803a..81dd9c0 100644 --- a/tests/test_competitors_plan_threading.py +++ b/tests/test_competitors_plan_threading.py @@ -13,7 +13,7 @@ from pathlib import Path from unittest import mock REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "scripts")) +sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts")) import last30days as cli diff --git a/tests/test_competitors_resolve_integration.py b/tests/test_competitors_resolve_integration.py index 5b6d686..a167dbd 100644 --- a/tests/test_competitors_resolve_integration.py +++ b/tests/test_competitors_resolve_integration.py @@ -11,7 +11,7 @@ from pathlib import Path from unittest import mock REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "scripts")) +sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts")) def _fake_report(topic: str): diff --git a/tests/test_cookie_extract.py b/tests/test_cookie_extract.py index ac7663c..3594064 100644 --- a/tests/test_cookie_extract.py +++ b/tests/test_cookie_extract.py @@ -2,6 +2,7 @@ import configparser import sqlite3 +import sys import textwrap from pathlib import Path from typing import Dict, List, Optional, Tuple @@ -9,6 +10,8 @@ from unittest.mock import patch import pytest +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days")) + from scripts.lib.cookie_extract import ( extract_cookies, extract_firefox_cookies, diff --git a/tests/test_env_include_sources_default.py b/tests/test_env_include_sources_default.py index a6e2bfd..d9de221 100644 --- a/tests/test_env_include_sources_default.py +++ b/tests/test_env_include_sources_default.py @@ -1,3 +1,8 @@ +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days")) + from scripts.lib import env diff --git a/tests/test_footer_nudge_suppression.py b/tests/test_footer_nudge_suppression.py index 7631998..592bfbe 100644 --- a/tests/test_footer_nudge_suppression.py +++ b/tests/test_footer_nudge_suppression.py @@ -10,11 +10,11 @@ import unittest from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "scripts")) +sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts")) def _engine() -> Path: - return REPO_ROOT / "scripts" / "last30days.py" + return REPO_ROOT / "skills" / "last30days" / "scripts" / "last30days.py" class FooterNudgeSuppressionTests(unittest.TestCase): diff --git a/tests/test_planner_quiet_mode.py b/tests/test_planner_quiet_mode.py index 6cfcdf2..f6e4d0e 100644 --- a/tests/test_planner_quiet_mode.py +++ b/tests/test_planner_quiet_mode.py @@ -10,7 +10,7 @@ from contextlib import redirect_stderr from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "scripts")) +sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts")) from lib import planner diff --git a/tests/test_polymarket_disambiguation.py b/tests/test_polymarket_disambiguation.py index a74735d..7175655 100644 --- a/tests/test_polymarket_disambiguation.py +++ b/tests/test_polymarket_disambiguation.py @@ -8,7 +8,7 @@ import unittest from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "scripts")) +sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts")) from lib import polymarket diff --git a/tests/test_render_comparison_multi.py b/tests/test_render_comparison_multi.py index a6f0131..83b305a 100644 --- a/tests/test_render_comparison_multi.py +++ b/tests/test_render_comparison_multi.py @@ -9,7 +9,7 @@ import unittest from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "scripts")) +sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts")) import last30days as cli from lib import render, schema diff --git a/tests/test_safari_cookies.py b/tests/test_safari_cookies.py index 8a1d0b8..addd9df 100644 --- a/tests/test_safari_cookies.py +++ b/tests/test_safari_cookies.py @@ -9,6 +9,8 @@ from unittest.mock import patch import pytest +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days")) + # Import the internal parser directly for testability (avoids platform check) from scripts.lib.safari_cookies import ( _parse_binary_cookies, diff --git a/tests/test_save_raw_per_entity.py b/tests/test_save_raw_per_entity.py index bc77ec2..dda4281 100644 --- a/tests/test_save_raw_per_entity.py +++ b/tests/test_save_raw_per_entity.py @@ -15,11 +15,11 @@ import unittest from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "scripts")) +sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts")) def _engine_path() -> Path: - return REPO_ROOT / "scripts" / "last30days.py" + return REPO_ROOT / "skills" / "last30days" / "scripts" / "last30days.py" class PerEntitySaveFilesTests(unittest.TestCase): diff --git a/tests/test_vs_mode_fanout.py b/tests/test_vs_mode_fanout.py index b0dd873..8926b2e 100644 --- a/tests/test_vs_mode_fanout.py +++ b/tests/test_vs_mode_fanout.py @@ -15,7 +15,7 @@ from contextlib import redirect_stderr from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "scripts")) +sys.path.insert(0, str(REPO_ROOT / "skills" / "last30days" / "scripts")) from lib import planner diff --git a/tests/test_xurl_x.py b/tests/test_xurl_x.py index 84fcf59..14bce72 100644 --- a/tests/test_xurl_x.py +++ b/tests/test_xurl_x.py @@ -6,7 +6,7 @@ import unittest from pathlib import Path from unittest import mock -sys.path.insert(0, str(Path(__file__).parent.parent / "scripts")) +sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "last30days" / "scripts")) from lib import xurl_x diff --git a/uv.lock b/uv.lock index f8f7825..819e8e9 100644 --- a/uv.lock +++ b/uv.lock @@ -197,7 +197,7 @@ wheels = [ [[package]] name = "last30days-skill" -version = "3.0.10" +version = "3.1.1" source = { virtual = "." } dependencies = [ { name = "requests" },