Atomic bump across all four manifests:
- SKILL.md (root)
- skills/last30days/SKILL.md (internal spec)
- .claude-plugin/plugin.json
- gemini-extension.json
CHANGELOG entry documents the skill-upload packaging fix, vendor/ removal,
legacy plans/ removal, and the new scripts/build-skill.sh builder.
Wraps git archive with --prefix=last30days/ so the zip contains a single
top-level skill folder matching SKILL.md's name: frontmatter. Enforces:
- refuses to build with a dirty working tree (prevents shipping untracked changes)
- fails if zip exceeds 200 files (claude.ai's empirical upload cap)
- fails if zip contains more than one SKILL.md (avoids name: confusion)
Output at dist/last30days.skill (gitignored).
Both plans describe work that was already shipped:
- feat-add-websearch-source.md - websearch is in the v3 pipeline (scripts/lib/perplexity.py etc)
- fix-strict-date-filtering.md - date filtering is enforced in scripts/lib/dates.py
New planning goes in docs/plans/ following the ce:plan convention.
Root vendor/package/ was an accidentally committed extracted npm tarball
(steipete-bird-0.8.0). Zero importers: the real vendored X client lives
at scripts/lib/vendor/bird-search/, referenced by scripts/lib/bird_x.py
and tests/test_bird_x.py.
Removes 215 files + 1 .tgz, dropping repo from 406 to 191 files and
clearing the claude.ai skill-upload 200-file cap.
Adds /vendor/ to .gitignore (leading slash so scripts/lib/vendor/ is unaffected).
Six source modules each defined an identical 8-line _sc_headers(token)
function returning {"x-api-key": token, "Content-Type": "application/json"}.
Moved it to http.scrapecreators_headers() and migrated all 33 call sites.
Affected files: reddit.py, threads.py, tiktok.py, instagram.py, pinterest.py,
youtube_yt.py. Zero per-source variation, zero behavior change.
Net: -40 lines. 1022 tests pass (15 pre-existing failures unchanged).
Live smoke test: reddit search returns 12 threads with full engagement.
When a tweet has no engagement metrics, _first_of() returns None for
every key, producing {"likes": None, "reposts": None, ...}. This
all-None dict propagates to signals.py where it is treated as "data
exists but is zero" rather than "no data available." Return None
instead when every engagement field is missing.
evaluate_search_quality.py and e2e_comparison.py both reference
fixtures/eval_topics.json with hardcoded fallbacks. Supply the
actual fixture: 8 topics spanning all intent types, selected via
MMR dispersion across domains (tech, health, sports, finance,
consumer products).
pyproject.toml declares pytest-cov as a dev dependency and configures
[tool.coverage.run], but the generated .coverage database and htmlcov/
report directory are not gitignored. Also add .venv/ (standard Python
virtualenv) and .memsearch/ (session memory) to keep the working tree
clean for contributors.
The SKILL.md prompt header still said v2.9.5 while pyproject.toml
and the rest of the codebase are on v3.0.0.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Makes the `variants/open/` sync steps in `scripts/sync.sh` conditional on
the directory actually existing in the source tree. The script is shared
between the public and private repos of last30days-skill, but the OpenClaw
variant only lives in the private repo (it's sanitized via
`strip_for_openclaw.py` and published separately to ClawhHub). When the
script runs from a checkout of the public repo, the variants/open paths
don't exist and the unconditional `rsync` and `sync_target` calls error
out immediately.
Changes:
- `sync_target()` now only creates `variants/open/references` and rsyncs
`variants/open/` when `$SRC/variants/open` exists.
- The trailing `sync_target "$OPENCLAW_TARGET" ...` call is now gated by
the same check, with an explanatory skip message when the directory is
absent.
No behavior change when running from the private repo (which has
`variants/open/`). When running from the public repo, the script now
completes its COMMON_TARGETS loop without erroring.
This also closes out the confusion from PR #211, where a contributor saw
the broken `variants/open/` reference and tried to add the variant back
to the public repo. The real fix was making the script tolerate the
absence, not recreating the directory.
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
release-notes.md:
- Drop the "What's New" section entirely. It repeated the same items
as the Headline features section above it in bulleted form, a
holdover from the old v2.9 release notes pattern. CHANGELOG.md is
the canonical Added/Changed/Fixed list; release notes is marketing
copy and shouldn't duplicate it. Added a one-line pointer to
CHANGELOG.md [3.0.0] for anyone looking for the detail.
- Rename "Credits" to "Earlier contributors" and note they are from
the v1 and v2 lineage, so readers don't confuse them with v3
contributors.
- Remove @steipete credit (did not actually contribute to this repo).
CHANGELOG.md [2.1.0] Credits:
- Remove @steipete credit (did not actually contribute to this repo).
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Rewrites release-notes.md from its stale v2.9.1 focus into the v3
story: intelligent pre-research as the killer feature, fun judge /
Best Takes, cross-source cluster merging, single-pass comparisons,
GitHub person-mode and project-mode, 13+ sources, ELI5 mode. Credits
@j-sperling as the v3 engine architect in the hero section and
updates the install instructions from `git clone` to the real install
paths for Claude Code, OpenClaw, and OpenAI Codex CLI.
Also extends the CHANGELOG [3.0.0] entry with a Fixed section covering
the two post-merge prep fixes that landed just before release:
- #214 resolved a duplicate `name: last30days` collision in
skills/last30days/SKILL.md that caused strict marketplace validators
to reject the plugin (reported by @Cody-Coyote in #204)
- #219 added the real Codex CLI integration at
.agents/skills/last30days/SKILL.md (regular file, since Codex's
loader skips symlinked files) plus .codex-plugin/plugin.json as the
namespace marker (inspired by @Jah-yee in #153 and @dannyshmueli
on X)
Bumps the [3.0.0] date from `2026-04` to `2026-04-11` to match the
actual release date, and adds @Cody-Coyote and @Jah-yee to the
[3.0.0] Contributors list.
No code changes. Pure docs prep for the v3.0.0 GitHub release.
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Adds a Codex CLI skill integration by creating the two files Codex's real
loader actually reads:
- .agents/skills/last30days/SKILL.md (real file, not a symlink - Codex's
loader skips symlinked files per codex-rs/core-skills/src/loader.rs)
- .codex-plugin/plugin.json with {"name": "last30days"} as a namespace
marker, per codex-rs/utils/plugins/src/plugin_namespace.rs
When Codex CLI runs in a checkout of this repo, it walks .agents/skills/
from CWD up to the project root, picks up .agents/skills/last30days/SKILL.md,
and walks ancestors looking for .codex-plugin/plugin.json to resolve the
plugin namespace. The skill registers as last30days:last30days.
The SKILL.md is a verbatim copy of the root SKILL.md at this point to
avoid content drift during the rollout. A future PR can slim the Codex copy
or introduce a sync mechanism.
Verified against Codex CLI's own source by running codex exec from the
repo CWD and having it trace the loader logic.
Replaces PR #153, which used a fake $schema URL
(https://openai.com/codex/plugin.schema.json returns 404) and put a
misunderstanding of Codex's plugin manifest (Codex only reads the `name`
field - all other fields like version, description, author, skills[] are
silently ignored).
This contribution was developed with AI assistance (Codex + Claude Code).
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
`stat -f '%Lp'` is BSD/macOS syntax. On Linux, `stat -f` prints
filesystem info (Block size / Inodes / ...) and still exits 0, so the
`||` fallback to `stat -c '%a'` never fires. That left `$perms` as
multi-line garbage, the `!= "600"` check was always true, and every
Linux SessionStart hook invocation printed a bogus warning plus the
whole `stat -f` filesystem dump.
Reorder to try GNU stat first, fall back to BSD for macOS. Verified on
Linux (cpython-3.12 / bash 5.x) — hook now emits the expected compact
Ready banner with no false warning.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two SKILL.md files declared `name: last30days` with `user-invocable: true`,
which caused strict marketplace validators to reject the plugin with "Some
plugins in this marketplace have validation errors":
- ./SKILL.md (canonical, also reachable via skills/last30days-nux/ symlink)
- ./skills/last30days/SKILL.md (v3 architecture spec, real file)
In v2.9.6, skills/last30days/SKILL.md was a symlink to ../../SKILL.md so
only one skill existed. Commit 0a9ff16 (v3.0.0) added a new real file at
skills/last30days-v3/SKILL.md, and commit 9be0780 then renamed that
directory to skills/last30days/, replacing the original symlink with a
different real file. The collision has been live since v3.0.0 shipped.
This change:
- Renames skills/last30days/SKILL.md to name: last30days-v3-spec and sets
user-invocable: false. The file stays in place as internal architecture
documentation, but it no longer competes with the canonical skill.
- Fixes README.md link that pointed to the deleted skills/last30days-v3/
path (left over from the rename).
- Removes a stale variants/open/SKILL.md reference (variants/open was
deleted in v3.0.0).
After the change, only one canonical name=last30days user-invocable=true
skill exists (the root SKILL.md, also reachable via the
skills/last30days-nux/ symlink, same inode).
Closes#204.
This contribution was developed with AI assistance (Codex).
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
github.py _parse_date used naive string slicing (return iso_str[:10])
which accepted any 10+ character string as a "date." For input
"hello world" it returned "hello worl". Now delegates to
dates.parse_date() which validates the format and returns None for
non-dates.
Also migrated reddit.py and threads.py _parse_date to the shared
dates.parse_date(). Both previously reimplemented ISO-with-trailing-
offset handling (the .replace("Z", "+00:00") dance) and reddit.py
also had its own Unix timestamp branch. dates.parse_date() already
handles all of this, including the +0000 no-colon variant Reddit emits.
Preserved reddit.py's original falsy-check so 0 still returns None
(epoch 0 would otherwise parse as "1970-01-01", breaking an existing
test and changing long-standing behavior).
Added 4 new github tests for garbage rejection and offset variants.
All 1026 existing tests pass (15 pre-existing failures unchanged).
Added params kwarg to http.request()/http.get() that urlencodes a dict
into the query string. None values are dropped, ints and bools are
stringified, and params append correctly if the URL already has a
query string.
Migrated reddit.py to use this helper for all three ScrapeCreators
call sites (global search, subreddit search, post comments). Deleted
the try/import requests/except ImportError fallback and the paired
if not _requests: / else: branches. Six new http tests cover the
params-encoding behavior.
Net: reddit.py -70 lines. Behavior is identical - the existing http.py
urllib implementation already had retry logic, 429 handling, and
HTTPError types that are strictly better than the ad-hoc requests
branches we deleted.
99 reddit tests pass. Live smoke test on a real ScrapeCreators run
returned 12 threads with the same engagement data as before.
marketplace.json had stale v3 rename artifacts: plugin name was
"last30days-3" (should be "last30days" to match plugin.json) and
version was "3.0.0-alpha" (should be "3.0.0" to match the stable
release).
The module-level _cached_token was set once and never refreshed. AT
Protocol tokens expire after ~2 hours, causing silent 401 errors in
long-running watchlist cron sessions. Adds a 90-minute expiry check
using time.monotonic() and logs re-authentication.
Fixes#92
Add column whitelists to prevent SQL injection via kwargs keys in
dynamic UPDATE queries. Values were already parameterized but column
names were string-interpolated directly from kwargs.
Fixes#90