Restructure as Codex plugin

This commit is contained in:
Claire Novotny
2026-04-23 20:14:23 -04:00
parent 1f7e85a03f
commit 72495c1c14
156 changed files with 218 additions and 140 deletions
+20
View File
@@ -0,0 +1,20 @@
{
"name": "last30days-skill",
"interface": {
"displayName": "Last 30 Days"
},
"plugins": [
{
"name": "last30days",
"source": {
"source": "local",
"path": "./"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Research"
}
]
}
+1 -1
View File
@@ -10,7 +10,7 @@
{ {
"name": "last30days", "name": "last30days",
"description": "Research any topic across Reddit, X, YouTube, TikTok, Instagram, HN, Polymarket, GitHub, and 5+ more sources.", "description": "Research any topic across Reddit, X, YouTube, TikTok, Instagram, HN, Polymarket, GitHub, and 5+ more sources.",
"version": "3.0.9", "version": "3.0.10",
"author": { "author": {
"name": "Matt Van Horn", "name": "Matt Van Horn",
"url": "https://github.com/mvanhorn" "url": "https://github.com/mvanhorn"
+41 -1
View File
@@ -1,3 +1,43 @@
{ {
"name": "last30days" "name": "last30days",
"version": "3.0.10",
"description": "Research any topic across Reddit, X, YouTube, TikTok, Instagram, Hacker News, Polymarket, GitHub, and more sources.",
"author": {
"name": "Matt Van Horn",
"email": "mvanhorn@gmail.com",
"url": "https://github.com/mvanhorn"
},
"homepage": "https://github.com/mvanhorn/last30days-skill",
"repository": "https://github.com/mvanhorn/last30days-skill",
"license": "MIT",
"keywords": [
"research",
"reddit",
"twitter",
"youtube",
"tiktok",
"instagram",
"trends",
"polymarket",
"github",
"hacker-news"
],
"skills": "./skills/",
"interface": {
"displayName": "Last 30 Days",
"shortDescription": "Research recent discussion across social and web sources",
"longDescription": "Use Last 30 Days to research what people are saying, upvoting, sharing, and betting on across Reddit, X, YouTube, TikTok, Instagram, Hacker News, Polymarket, GitHub, and web sources.",
"developerName": "Matt Van Horn",
"category": "Research",
"capabilities": [
"Interactive",
"Read",
"Write"
],
"websiteURL": "https://github.com/mvanhorn/last30days-skill",
"privacyPolicyURL": "https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement",
"termsOfServiceURL": "https://docs.github.com/en/site-policy/github-terms/github-terms-of-service",
"defaultPrompt": "Use Last 30 Days to research this topic from the last 30 days across Reddit, X, YouTube, and web.",
"brandColor": "#FF6B35"
}
} }
+4 -4
View File
@@ -1,5 +1,6 @@
# Exclude non-runtime files from `git archive` output. # Exclude non-runtime files from `git archive` output.
# Used by scripts/build-skill.sh to produce a claude.ai-upload-ready .skill file. # Used by skills/last30days/scripts/build-skill.sh to produce a
# claude.ai-upload-ready .skill file from the canonical skills/last30days tree.
# See docs/plans/2026-04-14-001-fix-skill-upload-200-file-limit-plan.md. # See docs/plans/2026-04-14-001-fix-skill-upload-200-file-limit-plan.md.
# Anthropic canonical skill-packaging excludes # Anthropic canonical skill-packaging excludes
@@ -32,9 +33,8 @@ release-notes.md export-ignore
CHANGELOG.md export-ignore CHANGELOG.md export-ignore
uv.lock export-ignore uv.lock export-ignore
# Platform adapters - skill-upload path is platform-agnostic # Platform adapters are kept in git archives because Claude Code and Codex
.agents/ export-ignore # plugin installs use the same repository archive as their source payload.
.codex-plugin/ export-ignore
.hermes-plugin/ export-ignore .hermes-plugin/ export-ignore
# CI workflows - repo-only, not needed at skill runtime # CI workflows - repo-only, not needed at skill runtime
+1
View File
@@ -78,6 +78,7 @@ Individual changelog entries for 3.0.10 through 3.0.14 below document the increm
### Fixed ### Fixed
- **TikTok author preference.** `_fetch_post_comments` in `scripts/lib/tiktok.py` preferred `user.nickname` over `user.unique_id`, so the engine captured display names ("Moosa Noormahomed") instead of @handles ("moosanoormahomed"). Flipped to prefer `unique_id`. Nickname still wins as a fallback when `unique_id` is missing. Display names can contain emoji, spaces, and non-Latin characters that do not round-trip to a profile URL; the @handle is the stable identifier. - **TikTok author preference.** `_fetch_post_comments` in `scripts/lib/tiktok.py` preferred `user.nickname` over `user.unique_id`, so the engine captured display names ("Moosa Noormahomed") instead of @handles ("moosanoormahomed"). Flipped to prefer `unique_id`. Nickname still wins as a fallback when `unique_id` is missing. Display names can contain emoji, spaces, and non-Latin characters that do not round-trip to a profile URL; the @handle is the stable identifier.
- **Single plugin payload layout.** The canonical runtime moved to `skills/last30days/` for both Claude Code and Codex plugin loading. Root-level `SKILL.md`, `scripts/`, `agents/`, and `assets/` are no longer maintained as duplicate copies.
### Behavior fallback ### Behavior fallback
+7 -7
View File
@@ -4,20 +4,20 @@ Claude Code skill for researching any topic across Reddit, X, YouTube, and web.
Python scripts with multi-source search aggregation. Python scripts with multi-source search aggregation.
## Structure ## Structure
- `scripts/last30days.py` — main research engine - `skills/last30days/SKILL.md` — canonical skill definition
- `scripts/lib/`search, enrichment, rendering modules - `skills/last30days/scripts/last30days.py` — main research engine
- `scripts/lib/vendor/bird-search/` — vendored X search client - `skills/last30days/scripts/lib/` — search, enrichment, rendering modules
- `SKILL.md` — skill definition (deployed to ~/.claude/skills/last30days/) - `skills/last30days/scripts/lib/vendor/bird-search/` — vendored X search client
## Commands ## Commands
```bash ```bash
python3 scripts/last30days.py "test query" --emit=compact # Run research python3 skills/last30days/scripts/last30days.py "test query" --emit=compact
bash scripts/sync.sh # Deploy to ~/.claude, ~/.agents, ~/.codex bash skills/last30days/scripts/sync.sh
``` ```
## Rules ## Rules
- `lib/__init__.py` must be bare package marker (comment only, NO eager imports) - `lib/__init__.py` must be bare package marker (comment only, NO eager imports)
- After edits: run `bash scripts/sync.sh` to deploy - After edits: run `bash skills/last30days/scripts/sync.sh` to deploy
- Git remote: origin = public (`mvanhorn/last30days-skill`) - Git remote: origin = public (`mvanhorn/last30days-skill`)
## Beta channel ## Beta channel
+4 -4
View File
@@ -18,7 +18,7 @@ git clone https://github.com/mvanhorn/last30days-skill.git
cd last30days-skill cd last30days-skill
# Run the sync script # Run the sync script
bash scripts/sync.sh bash skills/last30days/scripts/sync.sh
``` ```
This will auto-detect Hermes and deploy to `~/.hermes/skills/research/last30days/` This will auto-detect Hermes and deploy to `~/.hermes/skills/research/last30days/`
@@ -30,8 +30,8 @@ This will auto-detect Hermes and deploy to `~/.hermes/skills/research/last30days
mkdir -p ~/.hermes/skills/research/last30days mkdir -p ~/.hermes/skills/research/last30days
# Copy files # Copy files
cp -r scripts ~/.hermes/skills/research/last30days/ cp skills/last30days/SKILL.md ~/.hermes/skills/research/last30days/
cp .hermes-plugin/SKILL.md ~/.hermes/skills/research/last30days/ cp -r skills/last30days/scripts ~/.hermes/skills/research/last30days/
``` ```
## Usage ## Usage
@@ -111,7 +111,7 @@ To update to the latest version:
```bash ```bash
cd last30days-skill cd last30days-skill
git pull git pull
bash scripts/sync.sh bash skills/last30days/scripts/sync.sh
``` ```
## Support ## Support
+1 -1
View File
@@ -189,7 +189,7 @@ gemini extensions install ./last30days-skill
git clone https://github.com/mvanhorn/last30days-skill.git ~/.claude/skills/last30days git clone https://github.com/mvanhorn/last30days-skill.git ~/.claude/skills/last30days
``` ```
Or build the claude.ai `.skill` file from source: `bash scripts/build-skill.sh` produces `dist/last30days.skill`. Or build the claude.ai `.skill` file from source: `bash skills/last30days/scripts/build-skill.sh` produces `dist/last30days.skill`.
Reddit (with comments), Hacker News, Polymarket, and GitHub work immediately. Zero configuration. Run `/last30days` once and the setup wizard unlocks more sources in 30 seconds. Reddit (with comments), Hacker News, Polymarket, and GitHub work immediately. Zero configuration. Run `/last30days` once and the setup wizard unlocks more sources in 30 seconds.
+4 -5
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "last30days-skill" name = "last30days-skill"
version = "3.0.0" version = "3.0.10"
description = "Multi-source last-30-days research skill" description = "Multi-source last-30-days research skill"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
@@ -24,9 +24,9 @@ addopts = [
[tool.coverage.run] [tool.coverage.run]
branch = true branch = true
source = ["scripts", "tests"] source = ["skills/last30days/scripts", "tests"]
omit = [ omit = [
"scripts/lib/vendor/*", "skills/last30days/scripts/lib/vendor/*",
"dist/*", "dist/*",
] ]
@@ -34,7 +34,6 @@ omit = [
skip_empty = true skip_empty = true
show_missing = true show_missing = true
omit = [ omit = [
"scripts/lib/vendor/*", "skills/last30days/scripts/lib/vendor/*",
"dist/*", "dist/*",
] ]
+1 -1
View File
@@ -58,7 +58,7 @@ OpenClaw:
clawhub install last30days-official 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. OpenAI Codex CLI: install the repo as a local Codex marketplace/plugin. The plugin manifest lives at `.codex-plugin/plugin.json`, and the canonical skill payload is `skills/last30days/SKILL.md`.
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. 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.
+19 -15
View File
@@ -1,6 +1,6 @@
--- ---
name: last30days name: last30days
version: "3.0.1" version: "3.0.10"
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." 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' argument-hint: 'last30days nvidia earnings reaction | last30days AI video tools | last30days what users want in react'
allowed-tools: Bash, Read, Write, AskUserQuestion, WebSearch allowed-tools: Bash, Read, Write, AskUserQuestion, WebSearch
@@ -104,7 +104,7 @@ These anchors used to live at line 1094 of this file. Three independent Opus 4.7
🌐 last30days v{VERSION} · synced {YYYY-MM-DD} 🌐 last30days v{VERSION} · synced {YYYY-MM-DD}
``` ```
Replace `{VERSION}` with the installed plugin version (`jq -r '.version' "$SKILL_ROOT/.claude-plugin/plugin.json"`) and `{YYYY-MM-DD}` with today's date. No other text on this line. One blank line after, then the synthesis begins. Replace `{VERSION}` with the installed plugin version (`jq -r '.version' "$SKILL_ROOT/../../.codex-plugin/plugin.json" 2>/dev/null || jq -r '.version' "$SKILL_ROOT/.claude-plugin/plugin.json"`) and `{YYYY-MM-DD}` with today's date. No other text on this line. One blank line after, then the synthesis begins.
**Why the badge is MANDATORY:** it is the structural anchor for the canonical output shape. Without it the model drifts into blog-post narrative format with `##` section headers and invented titles, violating LAW 2 and LAW 4. The 2026-04-18 public v3.0.6 0/8 regression produced outputs with section headers like "The headline", "Why he is everywhere", "1. gstack dominates", "The 'Homecoming' peak". Direct cause: this anchor was absent. Do NOT skip the badge. Do NOT describe it. Do NOT paraphrase it. Emit it verbatim as line 1. **Why the badge is MANDATORY:** it is the structural anchor for the canonical output shape. Without it the model drifts into blog-post narrative format with `##` section headers and invented titles, violating LAW 2 and LAW 4. The 2026-04-18 public v3.0.6 0/8 regression produced outputs with section headers like "The headline", "Why he is everywhere", "1. gstack dominates", "The 'Homecoming' peak". Direct cause: this anchor was absent. Do NOT skip the badge. Do NOT describe it. Do NOT paraphrase it. Emit it verbatim as line 1.
@@ -233,7 +233,7 @@ If your Bash call to `last30days.py` does NOT include the FULL pre-flight checkl
--- ---
# last30days v3.0.1: Research Any Topic from the Last 30 Days # last30days v3.0.10: 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. > **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.
@@ -867,26 +867,30 @@ Store your plan as `QUERY_PLAN_JSON` - you'll pass it to the script in the next
**IMPORTANT: Include `--x-handle={RESOLVED_HANDLE}` in the command. For comparison mode: Pass `--x-handle={TOPIC_A_HANDLE}` to the first pass, `--x-handle={TOPIC_B_HANDLE}` to the second pass, and both to the head-to-head pass. Also include `--subreddits={RESOLVED_SUBREDDITS}`, `--tiktok-hashtags={RESOLVED_HASHTAGS}`, `--tiktok-creators={RESOLVED_TIKTOK_CREATORS}`, and `--ig-creators={RESOLVED_IG_CREATORS}` from Step 0.55. Omit any flag where the value was not resolved (empty).** **IMPORTANT: Include `--x-handle={RESOLVED_HANDLE}` in the command. For comparison mode: Pass `--x-handle={TOPIC_A_HANDLE}` to the first pass, `--x-handle={TOPIC_B_HANDLE}` to the second pass, and both to the head-to-head pass. Also include `--subreddits={RESOLVED_SUBREDDITS}`, `--tiktok-hashtags={RESOLVED_HASHTAGS}`, `--tiktok-creators={RESOLVED_TIKTOK_CREATORS}`, and `--ig-creators={RESOLVED_IG_CREATORS}` from Step 0.55. Omit any flag where the value was not resolved (empty).**
```bash ```bash
# PIN SKILL_ROOT to the public plugin cache (highest-version dir wins on upgrade). # PIN SKILL_ROOT to an installed plugin cache first (highest-version dir wins on upgrade).
# DO NOT write your own path-discovery loop. The 2026-04-18 Peter Steinberger run 1 # Prefer Codex's skill package path when installed as a Codex plugin. Keep the Claude
# regression was caused by a custom discovery loop landing on ~/.openclaw/skills/last30days/ # plugin-root fallback for other hosts, then fall back to a repo checkout.
# (a stale copy from a private-repo sync pattern). That path contains a pre-plan-007 SKILL_ROOT="$(ls -d "$HOME/.codex/plugins/cache/"*/last30days/*/skills/last30days/ 2>/dev/null | sort -V | tail -1)"
# engine and produces non-canonical output. This pinned resolution ignores every stale
# copy (~/.openclaw/, ~/.agents/, ~/.codex/) and picks the plugin cache exclusively.
SKILL_ROOT="$(ls -d "$HOME/.claude/plugins/cache/last30days-skill/last30days/"*/ 2>/dev/null | sort -V | tail -1)"
SKILL_ROOT="${SKILL_ROOT%/}" SKILL_ROOT="${SKILL_ROOT%/}"
# Fallback for repo checkout / Gemini / Codex hosts where the plugin cache does not exist. # Fallback for Claude plugin cache.
# Only runs if the public plugin cache is missing entirely.
if [ -z "$SKILL_ROOT" ] || [ ! -f "$SKILL_ROOT/scripts/last30days.py" ]; then if [ -z "$SKILL_ROOT" ] || [ ! -f "$SKILL_ROOT/scripts/last30days.py" ]; then
for dir in "." "${CLAUDE_PLUGIN_ROOT:-}" "${GEMINI_EXTENSION_DIR:-}"; do 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"
fi
# Fallback for repo checkout / Gemini / local development hosts where the plugin cache does not exist.
if [ -z "$SKILL_ROOT" ] || [ ! -f "$SKILL_ROOT/scripts/last30days.py" ]; then
for dir in "." "./skills/last30days" "${CLAUDE_PLUGIN_ROOT:-}" "${GEMINI_EXTENSION_DIR:-}"; do
[ -n "$dir" ] && [ -f "$dir/scripts/last30days.py" ] && SKILL_ROOT="$dir" && break [ -n "$dir" ] && [ -f "$dir/scripts/last30days.py" ] && SKILL_ROOT="$dir" && break
done done
fi fi
if [ -z "${SKILL_ROOT:-}" ] || [ ! -f "$SKILL_ROOT/scripts/last30days.py" ]; then if [ -z "${SKILL_ROOT:-}" ] || [ ! -f "$SKILL_ROOT/scripts/last30days.py" ]; then
echo "ERROR: Could not find scripts/last30days.py in public plugin cache or repo checkout" >&2 echo "ERROR: Could not find scripts/last30days.py in Codex/Claude plugin cache or repo checkout" >&2
echo "Expected: $HOME/.claude/plugins/cache/last30days-skill/last30days/{VERSION}/scripts/last30days.py" >&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
exit 1 exit 1
fi fi

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Before

Width:  |  Height:  |  Size: 3.8 MiB

After

Width:  |  Height:  |  Size: 3.8 MiB

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 2.6 MiB

@@ -1,13 +1,14 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# build-skill.sh - package this repo as a claude.ai-upload-ready .skill file # build-skill.sh - package this repo as a claude.ai-upload-ready .skill file
# Usage: bash scripts/build-skill.sh (run from repo root) # Usage: bash skills/last30days/scripts/build-skill.sh (run from repo root)
# #
# Produces dist/last30days.skill, a zip with a single top-level `last30days/` # Produces dist/last30days.skill, a zip with a single top-level `last30days/`
# directory containing SKILL.md and the scripts/ runtime. See # directory containing SKILL.md and the scripts/ runtime from skills/last30days.
# See
# docs/plans/2026-04-14-001-fix-skill-upload-200-file-limit-plan.md. # docs/plans/2026-04-14-001-fix-skill-upload-200-file-limit-plan.md.
set -euo pipefail set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
cd "$REPO_ROOT" cd "$REPO_ROOT"
if ! git diff --quiet || ! git diff --cached --quiet; then if ! git diff --quiet || ! git diff --cached --quiet; then
@@ -17,14 +18,7 @@ fi
mkdir -p dist mkdir -p dist
OUT="dist/last30days.skill" OUT="dist/last30days.skill"
git archive --format=zip --prefix=last30days/ --output="$OUT" HEAD git archive --format=zip --prefix=last30days/ --output="$OUT" HEAD:skills/last30days
# claude.ai's .skill bundle only needs the root SKILL.md + scripts/ runtime.
# Claude Code needs skills/ and .claude-plugin/ in the git archive
# (that's why they're NOT in .gitattributes export-ignore), but the .skill
# bundle must strip them to keep a single canonical SKILL.md and stay under
# the 200-file cap.
zip -d "$OUT" "last30days/skills/*" "last30days/.claude-plugin/*" > /dev/null 2>&1 || true
COUNT=$(unzip -l "$OUT" | tail -1 | awk '{print $2}') COUNT=$(unzip -l "$OUT" | tail -1 | awk '{print $2}')
SIZE=$(du -h "$OUT" | cut -f1) SIZE=$(du -h "$OUT" | cut -f1)
@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# A/B test runner: public release vs private beta # A/B test runner: public release vs private beta
# Usage: bash scripts/compare.sh "Kanye West" # Usage: bash skills/last30days/scripts/compare.sh "Kanye West"
# #
# Runs /last30days (public release) and /last30days-beta (private beta) # Runs /last30days (public release) and /last30days-beta (private beta)
# sequentially with a 30s gap, saves raw results with distinct suffixes, # sequentially with a 30s gap, saves raw results with distinct suffixes,
@@ -9,8 +9,8 @@
set -e set -e
if [ $# -eq 0 ]; then if [ $# -eq 0 ]; then
echo "Usage: bash scripts/compare.sh <topic>" echo "Usage: bash skills/last30days/scripts/compare.sh <topic>"
echo " Example: bash scripts/compare.sh Kevin Rose" echo " Example: bash skills/last30days/scripts/compare.sh Kevin Rose"
exit 1 exit 1
fi fi
TOPIC="$*" TOPIC="$*"
@@ -22,7 +22,8 @@ from lib import env as envlib
from lib import schema from lib import schema
REPO_ROOT = Path(__file__).resolve().parent.parent SKILL_ROOT = Path(__file__).resolve().parents[1]
REPO_ROOT = Path(__file__).resolve().parents[3]
EVAL_TOPICS_FILE = REPO_ROOT / "fixtures" / "eval_topics.json" EVAL_TOPICS_FILE = REPO_ROOT / "fixtures" / "eval_topics.json"
@@ -307,7 +308,10 @@ def create_eval_env() -> dict[str, str]:
def run_last30days(repo_dir: Path, topic: str, *, search: str, timeout_seconds: int, quick: bool, mock: bool, env: dict[str, str]) -> dict[str, Any]: def run_last30days(repo_dir: Path, topic: str, *, search: str, timeout_seconds: int, quick: bool, mock: bool, env: dict[str, str]) -> dict[str, Any]:
cmd = [sys.executable, "scripts/last30days.py", topic, "--emit=json"] engine = repo_dir / "skills" / "last30days" / "scripts" / "last30days.py"
if not engine.exists():
engine = repo_dir / "scripts" / "last30days.py"
cmd = [sys.executable, str(engine), topic, "--emit=json"]
if search: if search:
cmd.extend(["--search", search]) cmd.extend(["--search", search])
if quick: if quick:
@@ -12,7 +12,7 @@ from . import dates, schema
def _skill_version() -> str: def _skill_version() -> str:
"""Read plugin version from .claude-plugin/plugin.json if available. """Read plugin version from a plugin manifest if available.
Tries nearest plugin.json by walking up from render.py's own location. Tries nearest plugin.json by walking up from render.py's own location.
Falls back to "?" if not found. This keeps the badge emission from Falls back to "?" if not found. This keeps the badge emission from
@@ -20,7 +20,8 @@ def _skill_version() -> str:
""" """
here = pathlib.Path(__file__).resolve() here = pathlib.Path(__file__).resolve()
for parent in [here.parent, *here.parents]: for parent in [here.parent, *here.parents]:
candidate = parent / ".claude-plugin" / "plugin.json" for manifest_dir in (".codex-plugin", ".claude-plugin"):
candidate = parent / manifest_dir / "plugin.json"
if candidate.is_file(): if candidate.is_file():
try: try:
return json.loads(candidate.read_text()).get("version", "?") return json.loads(candidate.read_text()).get("version", "?")
@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# sync.sh - Deploy last30days skill to all host locations # sync.sh - Deploy last30days skill to all host locations
# Usage: bash scripts/sync.sh (run from repo root) # Usage: bash skills/last30days/scripts/sync.sh (run from repo root)
set -euo pipefail set -euo pipefail
SRC="$(cd "$(dirname "$0")/.." && pwd)" SRC="$(cd "$(dirname "$0")/.." && pwd)"
@@ -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.1" "$HOME/.claude/plugins/cache/last30days-skill-private/last30days-3/3.0.10"
"$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"
@@ -13,8 +13,11 @@ import time
from pathlib import Path from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent SKILL_ROOT = Path(__file__).resolve().parents[1]
REPO_ROOT = Path(__file__).resolve().parents[3]
PYTHON = sys.executable PYTHON = sys.executable
ENGINE = SKILL_ROOT / "scripts" / "last30days.py"
EVALUATOR = SKILL_ROOT / "scripts" / "evaluate_search_quality.py"
SMOKE_TOPIC = "openclaw skills" SMOKE_TOPIC = "openclaw skills"
SMOKE_CASES = [ SMOKE_CASES = [
@@ -56,7 +59,16 @@ def verify_unit() -> dict[str, str]:
"-m", "-m",
"py_compile", "py_compile",
*subprocess.run( *subprocess.run(
["rg", "--files", "scripts", "tests", "-g", "*.py", "-g", "!scripts/lib/vendor/**"], [
"rg",
"--files",
"skills/last30days/scripts",
"tests",
"-g",
"*.py",
"-g",
"!skills/last30days/scripts/lib/vendor/**",
],
cwd=REPO_ROOT, cwd=REPO_ROOT,
text=True, text=True,
capture_output=True, capture_output=True,
@@ -69,7 +81,7 @@ def verify_unit() -> dict[str, str]:
def verify_diagnose() -> dict[str, object]: def verify_diagnose() -> dict[str, object]:
result = run_command([PYTHON, "scripts/last30days.py", "--diagnose"], timeout=120) result = run_command([PYTHON, str(ENGINE), "--diagnose"], timeout=120)
return json.loads(result.stdout) return json.loads(result.stdout)
@@ -80,7 +92,7 @@ def verify_smoke() -> list[dict[str, object]]:
env["LAST30DAYS_REASONING_PROVIDER"] = provider env["LAST30DAYS_REASONING_PROVIDER"] = provider
start = time.time() start = time.time()
result = run_command( result = run_command(
[PYTHON, "scripts/last30days.py", SMOKE_TOPIC, "--emit=json", *extra], [PYTHON, str(ENGINE), SMOKE_TOPIC, "--emit=json", *extra],
env=env, env=env,
timeout=240, timeout=240,
) )
@@ -106,7 +118,7 @@ def verify_latency() -> dict[str, dict[str, object]]:
for topic in LATENCY_TOPICS: for topic in LATENCY_TOPICS:
start = time.time() start = time.time()
run_command( run_command(
[PYTHON, "scripts/last30days.py", topic, "--emit=json", *extra], [PYTHON, str(ENGINE), topic, "--emit=json", *extra],
timeout=300, timeout=300,
) )
timings.append(time.time() - start) timings.append(time.time() - start)
@@ -129,7 +141,7 @@ def verify_eval(
) -> dict[str, object]: ) -> dict[str, object]:
cmd = [ cmd = [
PYTHON, PYTHON,
"scripts/evaluate_search_quality.py", str(EVALUATOR),
f"--baseline={baseline}", f"--baseline={baseline}",
f"--candidate={candidate}", f"--candidate={candidate}",
f"--output-dir={output_dir}", f"--output-dir={output_dir}",
+1 -1
View File
@@ -9,7 +9,7 @@ import sys
import unittest import unittest
from pathlib import Path from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib import planner from lib import planner
+5 -3
View File
@@ -7,13 +7,13 @@ import textwrap
import unittest import unittest
from pathlib import Path from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib.bird_x import parse_bird_response from lib.bird_x import parse_bird_response
REPO_ROOT = Path(__file__).resolve().parents[1] REPO_ROOT = Path(__file__).resolve().parents[1]
VENDORED_BIRD = REPO_ROOT / "scripts" / "lib" / "vendor" / "bird-search" / "bird-search.mjs" VENDORED_BIRD = REPO_ROOT / "skills" / "last30days" / "scripts" / "lib" / "vendor" / "bird-search" / "bird-search.mjs"
class TestBirdXEngagementZero(unittest.TestCase): class TestBirdXEngagementZero(unittest.TestCase):
@@ -77,6 +77,8 @@ class TestVendoredBirdRuntime(unittest.TestCase):
def test_browser_cookie_helpers_lazy_load_sweet_cookie(self): def test_browser_cookie_helpers_lazy_load_sweet_cookie(self):
sweet_cookie_dir = ( sweet_cookie_dir = (
REPO_ROOT REPO_ROOT
/ "skills"
/ "last30days"
/ "scripts" / "scripts"
/ "lib" / "lib"
/ "vendor" / "vendor"
@@ -130,7 +132,7 @@ class TestVendoredBirdRuntime(unittest.TestCase):
extractCookiesFromSafari, extractCookiesFromSafari,
extractCookiesFromChrome, extractCookiesFromChrome,
extractCookiesFromFirefox, extractCookiesFromFirefox,
} from "./scripts/lib/vendor/bird-search/lib/cookies.js"; } from "./skills/last30days/scripts/lib/vendor/bird-search/lib/cookies.js";
const payload = await Promise.all([ const payload = await Promise.all([
extractCookiesFromSafari(), extractCookiesFromSafari(),
+1 -1
View File
@@ -5,7 +5,7 @@ import unittest
from pathlib import Path from pathlib import Path
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
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 bluesky from lib import bluesky
+1 -1
View File
@@ -4,7 +4,7 @@ import unittest
from pathlib import Path from pathlib import Path
from unittest import mock from unittest import mock
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
import briefing import briefing
import store import store
+1 -1
View File
@@ -11,7 +11,7 @@ import sys
import unittest import unittest
from pathlib import Path from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib import categories from lib import categories
from lib.categories import CATEGORY_PEERS, detect_category, peer_subs_for from lib.categories import CATEGORY_PEERS, detect_category, peer_subs_for
+1 -1
View File
@@ -19,7 +19,7 @@ from contextlib import redirect_stderr
from pathlib import Path from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "skills" / "last30days" / "scripts"))
from lib import resolve from lib import resolve

Some files were not shown because too many files have changed in this diff Show More