Commit Graph

19 Commits

Author SHA1 Message Date
Trevin Chow 9f95efb215 fix(skill): use portable trailing-XXXXXX mktemp form for plan tmpfiles
Greptile's review flagged mktemp -t as non-portable between BSD and GNU.
The suggested replacement (mktemp "$TMPDIR/...XXXXXX.json") is correct
about dropping -t but still puts X's in the middle of the template name
(XXXXXX.json), which BSD mktemp does not substitute — only X's at the
end of the basename are replaced on BSD. Verified on macOS:

  mktemp "$TMPDIR/last30days-test.XXXXXX.json"
  → /var/folders/.../last30days-test.XXXXXX.json  (X's left literal)

The fully portable form uses trailing X's and drops the .json suffix
(engine reads by path, not extension):

  mktemp "$TMPDIR/last30days-test.XXXXXX"
  → /var/folders/.../last30days-test.DXAHzR     (X's substituted)

Verified on bash and zsh, BSD/macOS. GNU/Linux is already fine since
GNU substitutes X's wherever they appear in the basename.

Applied to both --competitors-plan (comparison-mode block) and --plan
(Step 1 block) tmpfile writes.
2026-05-15 22:50:56 -07:00
Trevin Chow ff54c07a3b fix(skill): write --plan / --competitors-plan to tmpfile, bump 3.2.2 -> 3.2.3
Closes #403.

The SKILL.md templates instructed the model to invoke the engine with
inline single-quoted JSON: `--plan '$JSON'` and `--competitors-plan '{...}'`.
When any resolved field value contained an apostrophe (common in `context`
strings like "McDonald's", "people's choice", or contracted forms like
"don't", "won't"), the inner `'` closed the outer single-quote and broke
shell parsing before the engine was even invoked.

Observed during PR #400 testing: a Codex run hit the trap and self-healed
by re-encoding, wasting one engine invocation and ~30s of latency.

Fix: switch both templates to the heredoc + tmpfile pattern. The engine's
`parse_plan()` and `parse_competitors_plan()` already check
`os.path.isfile(plan_str)` and read from disk — only the SKILL.md prose
needed to change.

The quoted heredoc marker (<<'PLAN_EOF') is load-bearing: it suppresses
shell interpolation so apostrophes, $, backticks, etc. pass through verbatim.
A trap on EXIT cleans up the tmpfile after the engine call returns.

LAW 7's "MUST contain --plan" self-check guidance and Step 1's invocation
example both updated to reference the file form. Comparison-mode invocation
block updated the same way for --competitors-plan.

Version bump 3.2.2 -> 3.2.3 because this is a behavior change users
running comparison-mode queries will notice (no more "shell quoting error,
retrying" sequences on apostrophe-containing context strings).
2026-05-15 22:43:02 -07:00
Trevin Chow 2f277dfc66 fix(skill): address greptile P1+P2 review feedback on PR #400
Two real bugs flagged in the automated review of PR #400; both small.

1. render.py::_skill_version manifest with no "version" key

   `json.loads(manifest.read_text()).get("version", "?")` returned "?"
   immediately on a valid JSON manifest that lacked the "version" key,
   never falling through to the SKILL.md frontmatter fallback. Contradicted
   the docstring's "Returns '?' only if both sources are missing" contract.
   Same shape if version is present but empty string ("" produces the
   broken badge `🌐 last30days v · synced ...`).

   Fix: pull the version out of the parsed dict, then `continue` to the
   next ancestor if it's None or empty. Falls through to the SKILL.md
   walk only after exhausting every ancestor.

2. SKILL.md STEP 0 re-read target hardcoded to nested cache layout

   STEP 0 told the model to re-read from
   `$CLAUDE_CACHE_LATEST/skills/last30days/SKILL.md` — the new nested
   layout. But Step 1's resolver explicitly handles both shapes
   (nested `{cache}/{version}/skills/last30days/` and flat
   `{cache}/{version}/`), noting "Both shapes ship in the wild." On an
   install where the highest-versioned cache happens to be the older flat
   shape, STEP 0's re-read target wouldn't exist; the model would silently
   stay on the stale marketplaces/ copy STEP 0 was supposed to move it
   away from — the exact failure mode this guard was added to prevent.

   Fix: extend the STEP 0 bash to resolve $CLAUDE_CACHE_SKILL_MD by
   probing both layouts, then have the model hop to that resolved path
   instead of constructing the path from a hardcoded suffix.

Two new tests in tests/test_skill_version.py cover the missing-key and
empty-string cases for fix 1. Fix 2 is exercised via the bash probe at
verify time (the STEP 0 prose-contract test isn't unit-testable from
Python, but the dual-layout bash is verified to resolve to the correct
SKILL.md on both shapes).

Stale finding skipped: greptile also flagged a missing try/except on the
SKILL.md read_text() call, but that was already addressed during the
ce-code-review safe_auto pass earlier in this PR — current code wraps it
in `try/except (OSError, UnicodeDecodeError)`, strictly more defensive
than the suggested fix.
2026-05-15 22:34:52 -07:00
Trevin Chow 6c2c55733c fix(skill): use find instead of ls+glob in cache resolvers (zsh compatibility)
zsh errors on globs that match nothing instead of returning the literal
pattern (bash's default), and `2>/dev/null` does not suppress the error
because it comes from the shell's glob expansion before `ls` even runs.
Under Codex (which executes the SKILL.md bash via zsh), STEP 0 and the
Step 1 / comparison-mode resolvers emitted noisy "no matches found"
errors on machines without a Claude plugin cache populated.

Replaces all three `ls -d $HOME/.claude/plugins/cache/last30days-skill/last30days/*/`
invocations with `find ... -mindepth 1 -maxdepth 1 -type d 2>/dev/null`.
find is POSIX-portable, errors silently when the base dir doesn't exist,
and never triggers shell glob errors. `sort -V | tail -1` precedence
preserved (verified: picks 3.10.0 over 3.2.1 over 3.1.0). Trailing-slash
strip removed because find doesn't append slashes.

Observed in Codex session running /last30days against PR #400 with the
Claude plugin cache deleted - bash output was:
  zsh:1: no matches found: /Users/.../last30days/*/

After fix: clean empty output, exit 0, STEP 0 correctly treats it as
"no cache present, do not hop", resolver falls through to per-harness
skill dirs as designed.
2026-05-15 22:14:37 -07:00
Trevin Chow 997708ad48 refactor(skill): apply ce-code-review fixes — bump to 3.2.2, fallback tests, comparison resolver
12 fixes from the multi-agent code review on PR #400:

Version 3.2.1 -> 3.2.2 across all manifests (SKILL.md frontmatter + body
header, pyproject.toml, .claude-plugin/{plugin,marketplace}.json, sync.sh
cache path). The PR ships observable behavior changes (STEP 0 logic flip,
resolver order change, badge fallback) that should not silently appear
under the same version number — the new fallback reads SKILL.md version
directly so the badge would otherwise be misleading.

render.py::_skill_version:
- `import re` moved to module top
- _VERSION_RE extracted as a module-level compiled pattern that accepts
  double-quoted, single-quoted, and unquoted YAML version scalars
- `break` -> `continue` on corrupt manifest, so a corrupt inner manifest
  no longer shadows a valid outer one
- Wrap SKILL.md read_text() in try/except for UnicodeDecodeError to keep
  badge emission from crashing on mis-encoded SKILL.md
- Docstring clarifies precedence; inline comment marks the fallback boundary
  between the manifest walk and the SKILL.md walk

tests/test_skill_version.py (new): 7 unit tests for the fallback paths
(manifest absent, manifest corrupt, corrupt-inner + valid-outer, both
absent, SKILL.md without version, single-quoted, unquoted).

tests/test_plugin_contract.py: tombstone test asserting .codex-plugin/
stays removed (was the only CI guard against accidental reintroduction).

SKILL.md:
- STEP 0 bash echoes CLAUDE_CACHE_LATEST so the model can see the
  resolved value when deciding whether to hop
- "Both shapes ship in the wild" comment now names the two cache layouts
  (nested {cache}/{version}/skills/last30days/ vs flat {cache}/{version}/)
- Comparison-mode bash invocation gets its own inline SKILL_ROOT resolver
  (latent gap: the contract tells the model to skip Step 1 on comparison
  queries, so SKILL_ROOT was previously unset there)

CHANGELOG.md: [Unreleased] entries for the resolver rewrite and the
breaking removal of Codex native-plugin support.

All 9 reviewer personas surfaced findings; 3 cross-reviewer corroboration
clusters were promoted (import re, "both shapes" comment, missing fallback
tests). Maintainability follow-up flagged: regex now duplicated across
render.py and 2 test files; could consolidate via shared lib/skill_meta.py
helper in a future PR.
2026-05-15 21:45:25 -07:00
Trevin Chow c913e1cf89 refactor(skill): SKILL.md-relative path resolver, drop Codex native plugin
STEP 0 (CANONICAL PATH SELF-CHECK) used to force any SKILL.md load that wasn't
under $HOME/.claude/plugins/cache/last30days-skill/last30days/{version}/ to
re-Read from there. That guard is Claude-Code-specific (defends against the
marketplaces/ stale-clone bug) and broke under non-Claude installers like
`npx skills add`, ~/.codex/skills/, and ~/.agents/skills/.

The new STEP 0 narrows the check to its actual target: fire only when the
loaded SKILL.md path contains /.claude/plugins/marketplaces/. Every other
install path is trusted. The 2026-04-22 incident workaround is preserved
without breaking other harnesses.

Step 1 SKILL_ROOT resolver collapses the Codex-first / Claude-fallback /
CWD-fallback chain into a single precedence walk: Claude plugin cache
(versioned) first, then ~/.codex/skills, ~/.agents/skills, repo checkout,
./.skills/last30days (npx skills install dir), CWD, and GEMINI_EXTENSION_DIR.

Also drops Codex native plugin support: .codex-plugin/plugin.json is deleted,
the badge VERSION jq fallback in line 108 stops looking at it, and render.py's
_skill_version no longer scans for it. Codex users install via `npx skills add`
or the per-harness skill dir going forward.

render.py::_skill_version gains a SKILL.md frontmatter fallback so the badge
no longer emits `v?` on install dirs that sync.sh populates (which don't
include .claude-plugin/plugin.json).
2026-05-15 21:44:55 -07:00
Matt Van Horn dc934ddb6a feat(digg): rename to 'Digg' and bump per-cluster post limits (#372)
* feat(digg): bump POSTS_PER_CLUSTER to 5 and render limit to 3

Match the per-item enrichment cap and inline-display cap used by the
other sources (Reddit, HN, YouTube, TikTok, GitHub all use 5 fetched /
3 displayed). At the previous 3/2 caps the engine routinely truncated
cluster context — a recent run on cli-printing-press lost the Jason
Calacanis quote tweet entirely because the display cut off after Garry
Tan's first two posts.

* feat(digg): rename 'Digg AI 1000' to 'Digg' in user-facing strings

Drop the 'AI 1000' suffix from the footer line, source label, inline
quote attribution ('via Digg'), why_relevant, container, mock title,
SKILL.md source list, and README sources table. Internal code comments
and docstrings still reference the upstream Digg AI 1000 product.

Bumps version to 3.2.1 and adds a CHANGELOG entry covering this rename
and the POSTS_PER_CLUSTER / render-limit bumps from the prior commit.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-05-09 21:04:23 -07:00
Matt Van Horn 80392061d4 chore(release): v3.2.0 (#371)
Release / build-and-release (push) Has been cancelled
* chore(release): v3.2.0

Bumps plugin/marketplace/codex/pyproject versions from 3.1.1 to 3.2.0.
Promotes the Unreleased CHANGELOG entries (--emit=html, Digg AI 1000
source) to the 3.2.0 release section.

* chore(release): bump SKILL.md header and sync.sh path to 3.2.0

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-05-09 19:29:16 -07:00
Matt Van Horn c04bd67922 feat: add Digg AI 1000 as an opt-in source (#370)
* feat(digg): add Digg AI 1000 source module with cluster search and post enrichment

- search_digg shells out to digg-pp-cli with --since 30d --agent
- parse_digg_response normalizes clusters to last30days dict shape
- enrich_with_top_posts attaches top-ranked X posts to top-K clusters
- shutil.which gate plus subproc.run_with_timeout discipline matches
  bird_x.py / youtube_yt.py patterns

25 unit tests cover parse, age window, relevance, binary-missing
fallback, timeout recovery, and partial enrichment failures.

* feat(digg): wire Digg source into pipeline, normalize, signals, and render

pipeline.py:
- Import digg, add to MOCK_AVAILABLE_SOURCES, gate via shutil.which
- Dispatch case calls search_digg + parse_digg_response, runs
  enrich_with_top_posts at default/deep depth
- Mock fixture includes one enriched cluster + one bare cluster

normalize.py:
- _normalize_digg maps cluster dicts to SourceItem with
  container='Digg AI 1000' and metadata.posts pass-through

signals.py:
- SOURCE_QUALITY['digg'] = 0.85 (top tier alongside YouTube,
  reflecting Digg's curatorial layer)
- ENGAGEMENT_WEIGHTS['digg'] balances postCount, uniqueAuthors,
  and the rank_score derived from Digg's curatorial position

render.py:
- SOURCE_LABELS['digg'] = 'Digg AI 1000'
- _FOOTER_SOURCES adds '⛏️ Digg AI 1000' line after GitHub
- ENGAGEMENT_DISPLAY mirrors footer keys
- New _digg_posts_for + _format_digg_quote helpers emit inline
  '@handle via Digg AI 1000' quotes for clusters with attached X
  posts; both compact and full-dump renderers call them

* feat(digg): polish per-item engagement display and progress label

- ENGAGEMENT_DISPLAY for digg uses 'posts' / 'auth' to match the
  codebase abbreviation convention (HN: 'pts'/'cmt', X: 'rt'/'re')
- Footer item word changes from 'story' to 'cluster' to dodge the
  pre-existing naive plural in _footer_line_for_source ('storys')
  and to match Digg's actual data model
- ui.py SOURCE_COMPLETION_META adds digg with correct 'cluster'/
  'clusters' plural so 'Research complete' shows 'Digg: N clusters'

* feat(digg): document Digg AI 1000 source in skill, README, and changelog

- planner.py SOURCE_CAPABILITIES adds digg with discussion/social/link
  capabilities so the planner offers it through the standard fanout
- SKILL.md ACTIVE_SOURCES_LIST gate includes 'which digg-pp-cli' check
  and the source list / available-sources line names digg as opt-in
- README.md Sources table adds the Digg AI 1000 row with the activation
  gate so first-time readers see what they get
- CHANGELOG.md Unreleased section calls out the source addition

* fix(digg): enrich post-dedupe so brief survivors carry inline quotes

Pipeline dispatch was attaching X posts to the top-3 items returned by
search, but dedupe later picked different survivors when multiple
clusters compared similar (common for trending topics). The brief
ended up showing clusters with no posts attached even though
enrichment ran successfully on positions 0-2.

Move enrichment to _finalize_items_by_source. The new
digg.enrich_source_items helper reads metadata['clusterUrlId'] and
writes metadata['posts'] in place on the SourceItems that actually
survive dedupe.

Verified live on 'openclaw': 2 surviving clusters, both now carry
real X-post quotes from @sama and @jeremyphoward attributed
'via Digg AI 1000'.

Adds 3 unit tests covering survivor enrichment, non-digg skip, and
clusterUrlId fallback to item_id.

* test(digg): relax live off-topic test to check shape, not emptiness

Digg's live search uses fuzzy/popularity fallback, so an impossible
token can still return some loosely-related clusters. The contract
the pipeline depends on is shape (results is always a list);
token-overlap relevance handles the noise downstream.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-05-09 19:05:41 -07:00
Trevin Chow b1773be8f3 feat(emit): --emit=html for shareable self-contained briefs (#332)
Adds a one-command shareable HTML mode to /last30days. The skill detects
HTML intent (explicit --emit=html / --emit:html / --html flag in
$ARGUMENTS, or natural-language asks like "give me a shareable brief",
"for Slack", "export as HTML"), runs the normal research + chat synthesis
flow, then saves a self-contained HTML file to
~/Documents/Last30Days/{topic}-brief.html. The synthesis appears in chat
as usual; the HTML is an additional artifact for sharing.

User experience:

  /last30days OpenClaw --emit=html
  /last30days OpenClaw, give me an HTML brief for Slack

Synthesis prints to chat. Last line of the response: "📎 Shareable brief
saved to ~/Documents/Last30Days/openclaw-brief.html". Open it, drag it
into a message, browser-print to PDF, email it.

Architecture:

  - SKILL.md gets a small detection block (triggers + early exit +
    MUST/MUST NOT rules + rationale) that points to a reference file.
  - references/save-html-brief.md owns the implementation: capture the
    synthesis verbatim into a temp file via heredoc, invoke the engine
    with --emit=html --synthesis-file, save to disk, append the
    confirmation line to chat.
  - lib/render.py exposes render_for_html(report, synthesis_md=None) and
    render_for_html_comparison(...) -- clean markdown for HTML
    conversion. Omits debug file header, model-facing safety note, and
    data quality warnings (those stay in engine stderr; recipients can't
    act on them in a shared artifact).
  - lib/html_render.py is a new module: ~200-line CSS template (dark
    mode default, prefers-color-scheme switch, print stylesheet, mobile
    breakpoint), stdlib-regex markdown-to-HTML converter, marker-based
    META + engine-footer wrapping, PROSE_LABELS registry promoting plain
    -text labels to <h2>, colophon builder.
  - last30days.py adds --emit=html argparse choice and --synthesis-file
    PATH flag (engine still callable directly without the skill in the
    loop).

Design:

  - Voice-led research brief, not corporate report. Inter + JetBrains
    Mono via Google Fonts with full system fallbacks (no FOIT, works
    offline). Brand purple #a855f7 (#7c3aed in light mode). Type ramp:
    body 17px/400/muted, bold lead-in 17px/600/fg, h2 + .prose-label
    20px/600/fg, monospace badge/meta/footer/colophon at 13-13.5px.
  - 720px max-width, generous whitespace, no card layouts or shadows.
  - Print stylesheet: light theme, A4 margins, [href]::after URL
    footnotes, page-break-inside:avoid on the engine footer.

Templated (locked) shell:

  - HTML5 boilerplate, Google Fonts <link> with preconnect, all CSS
    inline.
  - .badge / .meta / .engine-footer / .colophon containers.

Flexible (role-based):

  - <h2> rendering covers BOTH plain ## headers (comparison mode per
    LAW 4 exception) AND promoted prose labels via PROSE_LABELS
    registry. Adding a new SKILL.md prose label is a one-line tuple
    addition; no CSS or template changes.
  - Marker-based engine boundaries (<!-- META: ... -->,
    <!-- PASS-THROUGH FOOTER -->) survive the markdown converter and
    get promoted post-conversion. Robust to engine output format
    changes.
  - Generic markdown-to-HTML for body content; future SKILL.md additions
    (new sections, tables, blockquotes) render correctly without code
    changes.

Tests: 30 new tests in tests/test_html_render.py covering snapshots
(rich/thin/comparison), CLI parsing, --synthesis-file end-to-end, prose
label promotion, warning exclusion from artifact, parseability via
html.parser, no-script self-containment.

No SKILL.md voice contract changes, no LAWs 1-8 changes, no new pip
dependencies, no JavaScript anywhere.
2026-05-02 11:30:22 -07:00
Claire Novotny f03cb866aa fix: address plugin layout review feedback 2026-04-24 11:52:48 -04:00
Claire Novotny 72495c1c14 Restructure as Codex plugin 2026-04-23 20:15:02 -04:00
Matt Van Horn 361e9d6c13 fix: v3.0.8 - SKILL.md was too big and LAWs too deep - move to top + engine emits badge (#279)
Three independent Opus 4.7 self-debugs on 2026-04-18 converged on the same
root cause of the v3.0.6/v3.0.7 canonical-compliance regression: SKILL.md is
42,860 tokens / 1,478 lines, LAWs lived at line 1094+, every realistic reading
strategy failed to reach them before synthesis.

Unit 1 - Moved the BADGE MANDATORY block and VOICE CONTRACT LAW 1-5 (plus
the formatting-authority preface) from line ~1090 to line ~75 (right after
the SKILL CONTRACT preface, before HOW TO INVOKE THIS SKILL). Every reading
strategy now lands the LAWs in active context before synthesis.

Unit 2 - Engine now emits the badge as the first line of --emit=compact
stdout. Passing through the script output becomes the default-correct
behavior; emitting the badge no longer depends on model compliance. Reads
version from .claude-plugin/plugin.json at runtime with graceful fallback.

Unit 3 - Deleted skills/last30days/SKILL.md stub (231-line v3-spec file).
This was the wrong-file-capture hazard Ron Conway's self-debug identified:
model grabbed the first SKILL.md find surfaced and treated it as
authoritative. Only ONE SKILL.md in the plugin package now.

Diagnoses verbatim:
- Kanye thread: "I read lines 1-600 in chunks, jumped to 300-899, then
  stopped. File is 1478 lines. I never saw past ~900."
- Peter thread: "I tried Read once, hit the 25K token cap on a 42,860-token
  file, and bailed instead of chunked-reading with offset/limit. I never
  opened SKILL.md at all."
- Ron Conway thread: "I read one SKILL.md (231 lines)... the v3 spec stub.
  I never opened the operational SKILL.md sitting next to the script."

Validation: direct engine invocation confirms badge at line 1 of compact
output. Module imports clean.

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:50:10 -07:00
Matt Van Horn 8d8ca68781 chore: bump version to 3.0.1 + changelog entry
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.
2026-04-14 12:21:24 -04:00
Matt Van Horn 99b167d03a fix: resolve duplicate skill name causing marketplace validation failure (#204) (#214)
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>
2026-04-11 02:36:55 -04:00
Pejman Pour-Moezzi bb7c956db5 fix: drop stale last30days-v3 argument hint 2026-04-09 21:37:36 -07:00
Pejman Pour-Moezzi 9be0780c46 fix: rename skills/last30days-v3 directory so plugin install resolves correctly 2026-04-09 21:37:00 -07:00
Matt Van Horn 0a9ff16dfc feat: v3.0.0 - intelligent search, GitHub person/project mode, ELI5, 13+ sources
v3 rewrites the search engine from the ground up:

- Intelligent pre-research: resolves X handles, GitHub repos, subreddits,
  TikTok hashtags, and YouTube channels before searching
- GitHub person-mode: PR velocity, top repos by stars, release notes
- GitHub project-mode: live star counts, README, releases, top issues
- ELI5 mode: plain language synthesis, no jargon
- 13+ sources: Reddit, X, YouTube, TikTok, Instagram, HN, Polymarket,
  GitHub, Threads, Pinterest, Perplexity, Bluesky, Web
- Free Reddit comments via public JSON (no API key needed)
- Fun judge v2: humor scoring baked into narrative
- Cookie consent before browser scanning
- 10,000 free ScrapeCreators calls
- 1,012 tests

Thank you to the community contributors whose issues and PRs shaped v3:
@uppinote20 (#143), @zerone0x (#134, #136), @thinkun (#116),
@thomasmktong (#124), @fanispoulinakisai-boop (#100), @pejmanjohn (#78),
@zl190 (#115), @hnshah (#84, #85, #86)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:52:23 -07:00
Matt Van Horn 2f16ff1ee8 feat(gemini): add Gemini CLI extension support
Add gemini-extension.json manifest with correct array-format settings,
symlink skills/last30days/SKILL.md to root SKILL.md for Gemini skill
discovery, add Gemini install paths to bash for-loop in both main and
open variant, and add Gemini CLI install instructions to README.

Incorporates the good parts of PR #53 (manifest, paths, README) while
avoiding duplicate SKILL.md, tool name scattering, and allowed-tools
pollution that would have created maintenance issues.

Closes #45

Co-Authored-By: Alex Ferrari <alex@thealexferrari.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 12:07:55 -07:00