luckiest-co 1.0.0 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +16 -0
- package/.claude-plugin/plugin.json +1 -1
- package/LICENSE +24 -0
- package/bin/install.js +7 -5
- package/commands/{luckiest/finish.md → finish.md} +2 -0
- package/commands/go.md +53 -0
- package/commands/plan.md +66 -0
- package/package.json +16 -5
- package/skills/brainstorm/ATTRIBUTION.md +27 -0
- package/skills/brainstorm/SKILL.md +139 -0
- package/skills/luckiest-ab-testing/ATTRIBUTION.md +35 -0
- package/skills/luckiest-ab-testing/CHANGELOG.md +27 -0
- package/skills/luckiest-ab-testing/SKILL.md +379 -0
- package/skills/luckiest-ab-testing/evals/evals.json +118 -0
- package/skills/luckiest-ab-testing/references/sample-size-guide.md +290 -0
- package/skills/luckiest-ab-testing/references/test-templates.md +284 -0
- package/skills/luckiest-ad-creative/ATTRIBUTION.md +35 -0
- package/skills/luckiest-ad-creative/CHANGELOG.md +23 -0
- package/skills/luckiest-ad-creative/SKILL.md +393 -0
- package/skills/luckiest-ad-creative/evals/evals.json +90 -0
- package/skills/luckiest-ad-creative/references/generative-tools.md +637 -0
- package/skills/luckiest-ad-creative/references/platform-specs.md +213 -0
- package/skills/luckiest-ads/ATTRIBUTION.md +36 -0
- package/skills/luckiest-ads/CHANGELOG.md +24 -0
- package/skills/luckiest-ads/LICENSE +21 -0
- package/skills/luckiest-ads/SKILL.md +434 -0
- package/skills/luckiest-ads/evals/evals.json +90 -0
- package/skills/luckiest-ads/references/ad-copy-templates.md +207 -0
- package/skills/luckiest-ads/references/audience-targeting.md +243 -0
- package/skills/luckiest-ads/references/conversion-tracking.md +361 -0
- package/skills/luckiest-ads/references/platform-setup-checklists.md +277 -0
- package/skills/luckiest-advisors/ATTRIBUTION.md +16 -0
- package/skills/luckiest-advisors/CHANGELOG.md +99 -0
- package/skills/luckiest-advisors/SKILL.md +151 -0
- package/skills/luckiest-aeo-grader/ATTRIBUTION.md +19 -0
- package/skills/luckiest-aeo-grader/CHANGELOG.md +52 -0
- package/skills/luckiest-aeo-grader/SKILL.md +200 -0
- package/skills/luckiest-aeo-grader/evals/evals.json +19 -0
- package/skills/luckiest-aeo-grader/references/rubric.md +72 -0
- package/skills/luckiest-aeo-grader/scripts/scan.py +265 -0
- package/skills/luckiest-ai-seo/ATTRIBUTION.md +38 -0
- package/skills/luckiest-ai-seo/CHANGELOG.md +23 -0
- package/skills/luckiest-ai-seo/SKILL.md +510 -0
- package/skills/luckiest-ai-seo/evals/evals.json +90 -0
- package/skills/luckiest-ai-seo/references/content-patterns.md +285 -0
- package/skills/luckiest-ai-seo/references/content-types.md +71 -0
- package/skills/luckiest-ai-seo/references/okf.md +104 -0
- package/skills/luckiest-ai-seo/references/platform-ranking-factors.md +154 -0
- package/skills/luckiest-analytics/ATTRIBUTION.md +42 -0
- package/skills/luckiest-analytics/CHANGELOG.md +36 -0
- package/skills/luckiest-analytics/SKILL.md +380 -0
- package/skills/luckiest-analytics/evals/evals.json +90 -0
- package/skills/luckiest-analytics/references/event-library.md +260 -0
- package/skills/luckiest-analytics/references/ga4-implementation.md +300 -0
- package/skills/luckiest-analytics/references/gtm-implementation.md +390 -0
- package/skills/luckiest-aso/ATTRIBUTION.md +40 -0
- package/skills/luckiest-aso/CHANGELOG.md +33 -0
- package/skills/luckiest-aso/SKILL.md +327 -0
- package/skills/luckiest-aso/evals/evals.json +91 -0
- package/skills/luckiest-aso/references/apple-specs.md +107 -0
- package/skills/luckiest-aso/references/benchmarks.md +129 -0
- package/skills/luckiest-aso/references/google-play-specs.md +131 -0
- package/skills/luckiest-aso/references/report-template.md +213 -0
- package/skills/luckiest-aso/references/scoring-criteria.md +213 -0
- package/skills/luckiest-churn-prevention/ATTRIBUTION.md +41 -0
- package/skills/luckiest-churn-prevention/CHANGELOG.md +24 -0
- package/skills/luckiest-churn-prevention/SKILL.md +445 -0
- package/skills/luckiest-churn-prevention/evals/evals.json +93 -0
- package/skills/luckiest-churn-prevention/references/cancel-flow-patterns.md +316 -0
- package/skills/luckiest-churn-prevention/references/dunning-playbook.md +408 -0
- package/skills/luckiest-co-marketing/ATTRIBUTION.md +38 -0
- package/skills/luckiest-co-marketing/CHANGELOG.md +25 -0
- package/skills/luckiest-co-marketing/SKILL.md +331 -0
- package/skills/luckiest-co-marketing/evals/evals.json +84 -0
- package/skills/luckiest-coder/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder/CHANGELOG.md +30 -0
- package/skills/luckiest-coder/SKILL.md +77 -0
- package/skills/luckiest-coder/references/antigravity-tools.md +23 -0
- package/skills/luckiest-coder/references/codex-tools.md +39 -0
- package/skills/luckiest-coder/references/pi-tools.md +16 -0
- package/skills/luckiest-coder-brainstorming/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-brainstorming/CHANGELOG.md +31 -0
- package/skills/luckiest-coder-brainstorming/SKILL.md +173 -0
- package/skills/luckiest-coder-brainstorming/scripts/frame-template.html +213 -0
- package/skills/luckiest-coder-brainstorming/scripts/helper.js +168 -0
- package/skills/luckiest-coder-brainstorming/scripts/server.cjs +721 -0
- package/skills/luckiest-coder-brainstorming/scripts/start-server.sh +209 -0
- package/skills/luckiest-coder-brainstorming/scripts/stop-server.sh +120 -0
- package/skills/luckiest-coder-brainstorming/spec-document-reviewer-prompt.md +49 -0
- package/skills/luckiest-coder-brainstorming/visual-companion.md +291 -0
- package/skills/luckiest-coder-consistency/ATTRIBUTION.md +30 -0
- package/skills/luckiest-coder-consistency/CHANGELOG.md +26 -0
- package/skills/luckiest-coder-consistency/SKILL.md +98 -0
- package/skills/luckiest-coder-constitution/ATTRIBUTION.md +28 -0
- package/skills/luckiest-coder-constitution/CHANGELOG.md +25 -0
- package/skills/luckiest-coder-constitution/SKILL.md +115 -0
- package/skills/luckiest-coder-debugging/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-debugging/CHANGELOG.md +22 -0
- package/skills/luckiest-coder-debugging/CREATION-LOG.md +119 -0
- package/skills/luckiest-coder-debugging/SKILL.md +309 -0
- package/skills/luckiest-coder-debugging/condition-based-waiting-example.ts +158 -0
- package/skills/luckiest-coder-debugging/condition-based-waiting.md +115 -0
- package/skills/luckiest-coder-debugging/defense-in-depth.md +122 -0
- package/skills/luckiest-coder-debugging/find-polluter.sh +63 -0
- package/skills/luckiest-coder-debugging/root-cause-tracing.md +169 -0
- package/skills/luckiest-coder-debugging/test-academic.md +14 -0
- package/skills/luckiest-coder-debugging/test-pressure-1.md +58 -0
- package/skills/luckiest-coder-debugging/test-pressure-2.md +68 -0
- package/skills/luckiest-coder-debugging/test-pressure-3.md +69 -0
- package/skills/luckiest-coder-dispatching-parallel-agents/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-dispatching-parallel-agents/CHANGELOG.md +23 -0
- package/skills/luckiest-coder-dispatching-parallel-agents/SKILL.md +198 -0
- package/skills/luckiest-coder-executing-plans/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-executing-plans/CHANGELOG.md +22 -0
- package/skills/luckiest-coder-executing-plans/SKILL.md +83 -0
- package/skills/luckiest-coder-finishing-a-branch/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-finishing-a-branch/CHANGELOG.md +22 -0
- package/skills/luckiest-coder-finishing-a-branch/SKILL.md +258 -0
- package/skills/luckiest-coder-git-worktrees/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-git-worktrees/CHANGELOG.md +22 -0
- package/skills/luckiest-coder-git-worktrees/SKILL.md +215 -0
- package/skills/luckiest-coder-guard/ATTRIBUTION.md +29 -0
- package/skills/luckiest-coder-guard/CHANGELOG.md +48 -0
- package/skills/luckiest-coder-guard/LICENSE +21 -0
- package/skills/luckiest-coder-guard/SKILL.md +126 -0
- package/skills/luckiest-coder-guard/bin/check-careful.sh +65 -0
- package/skills/luckiest-coder-guard/bin/check-freeze.sh +48 -0
- package/skills/luckiest-coder-qa/ATTRIBUTION.md +28 -0
- package/skills/luckiest-coder-qa/CHANGELOG.md +49 -0
- package/skills/luckiest-coder-qa/LICENSE +21 -0
- package/skills/luckiest-coder-qa/SKILL.md +160 -0
- package/skills/luckiest-coder-qa/references/issue-taxonomy.md +85 -0
- package/skills/luckiest-coder-qa/references/qa-report-template.md +126 -0
- package/skills/luckiest-coder-receiving-code-review/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-receiving-code-review/CHANGELOG.md +28 -0
- package/skills/luckiest-coder-receiving-code-review/SKILL.md +226 -0
- package/skills/luckiest-coder-requesting-code-review/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-requesting-code-review/CHANGELOG.md +31 -0
- package/skills/luckiest-coder-requesting-code-review/SKILL.md +118 -0
- package/skills/luckiest-coder-requesting-code-review/code-reviewer.md +172 -0
- package/skills/luckiest-coder-shipping/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-shipping/CHANGELOG.md +48 -0
- package/skills/luckiest-coder-shipping/LICENSE +21 -0
- package/skills/luckiest-coder-shipping/SKILL.md +148 -0
- package/skills/luckiest-coder-shipping/references/canary.md +44 -0
- package/skills/luckiest-coder-shipping/references/test-and-coverage.md +91 -0
- package/skills/luckiest-coder-subagent-driven-development/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-subagent-driven-development/CHANGELOG.md +28 -0
- package/skills/luckiest-coder-subagent-driven-development/SKILL.md +439 -0
- package/skills/luckiest-coder-subagent-driven-development/implementer-prompt.md +139 -0
- package/skills/luckiest-coder-subagent-driven-development/scripts/review-package +44 -0
- package/skills/luckiest-coder-subagent-driven-development/scripts/sdd-workspace +22 -0
- package/skills/luckiest-coder-subagent-driven-development/scripts/task-brief +40 -0
- package/skills/luckiest-coder-subagent-driven-development/task-reviewer-prompt.md +188 -0
- package/skills/luckiest-coder-tdd/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-tdd/CHANGELOG.md +26 -0
- package/skills/luckiest-coder-tdd/SKILL.md +384 -0
- package/skills/luckiest-coder-tdd/testing-anti-patterns.md +299 -0
- package/skills/luckiest-coder-verification/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-verification/CHANGELOG.md +22 -0
- package/skills/luckiest-coder-verification/SKILL.md +152 -0
- package/skills/luckiest-coder-writing-plans/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-writing-plans/CHANGELOG.md +22 -0
- package/skills/luckiest-coder-writing-plans/SKILL.md +191 -0
- package/skills/luckiest-coder-writing-plans/plan-document-reviewer-prompt.md +49 -0
- package/skills/luckiest-coder-writing-skills/ATTRIBUTION.md +26 -0
- package/skills/luckiest-coder-writing-skills/CHANGELOG.md +31 -0
- package/skills/luckiest-coder-writing-skills/SKILL.md +702 -0
- package/skills/luckiest-coder-writing-skills/anthropic-best-practices.md +1150 -0
- package/skills/luckiest-coder-writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
- package/skills/luckiest-coder-writing-skills/graphviz-conventions.dot +172 -0
- package/skills/luckiest-coder-writing-skills/persuasion-principles.md +187 -0
- package/skills/luckiest-coder-writing-skills/render-graphs.js +168 -0
- package/skills/luckiest-coder-writing-skills/testing-skills-with-subagents.md +384 -0
- package/skills/luckiest-cold-email/ATTRIBUTION.md +36 -0
- package/skills/luckiest-cold-email/CHANGELOG.md +24 -0
- package/skills/luckiest-cold-email/SKILL.md +213 -0
- package/skills/luckiest-cold-email/evals/evals.json +94 -0
- package/skills/luckiest-cold-email/references/benchmarks.md +83 -0
- package/skills/luckiest-cold-email/references/follow-up-sequences.md +81 -0
- package/skills/luckiest-cold-email/references/frameworks.md +90 -0
- package/skills/luckiest-cold-email/references/personalization.md +79 -0
- package/skills/luckiest-cold-email/references/subject-lines.md +53 -0
- package/skills/luckiest-community-marketing/ATTRIBUTION.md +41 -0
- package/skills/luckiest-community-marketing/CHANGELOG.md +35 -0
- package/skills/luckiest-community-marketing/SKILL.md +200 -0
- package/skills/luckiest-community-marketing/evals/evals.json +89 -0
- package/skills/luckiest-competitor-profiling/ATTRIBUTION.md +36 -0
- package/skills/luckiest-competitor-profiling/CHANGELOG.md +24 -0
- package/skills/luckiest-competitor-profiling/SKILL.md +443 -0
- package/skills/luckiest-competitor-profiling/evals/evals.json +85 -0
- package/skills/luckiest-competitor-profiling/references/templates.md +167 -0
- package/skills/luckiest-competitor-profiling/references/tool-reference.md +179 -0
- package/skills/luckiest-competitors/ATTRIBUTION.md +36 -0
- package/skills/luckiest-competitors/CHANGELOG.md +25 -0
- package/skills/luckiest-competitors/SKILL.md +171 -0
- package/skills/luckiest-competitors/evals/evals.json +93 -0
- package/skills/luckiest-competitors/references/content-architecture.md +271 -0
- package/skills/luckiest-competitors/references/templates.md +223 -0
- package/skills/luckiest-content-strategy/ATTRIBUTION.md +39 -0
- package/skills/luckiest-content-strategy/CHANGELOG.md +23 -0
- package/skills/luckiest-content-strategy/SKILL.md +395 -0
- package/skills/luckiest-content-strategy/evals/evals.json +90 -0
- package/skills/luckiest-content-strategy/references/headless-cms.md +194 -0
- package/skills/luckiest-copy-editing/ATTRIBUTION.md +41 -0
- package/skills/luckiest-copy-editing/CHANGELOG.md +31 -0
- package/skills/luckiest-copy-editing/SKILL.md +491 -0
- package/skills/luckiest-copy-editing/evals/evals.json +89 -0
- package/skills/luckiest-copy-editing/references/checklist.md +66 -0
- package/skills/luckiest-copy-editing/references/content-refresh.md +38 -0
- package/skills/luckiest-copy-editing/references/plain-english-alternatives.md +394 -0
- package/skills/luckiest-copywriting/ATTRIBUTION.md +35 -0
- package/skills/luckiest-copywriting/CHANGELOG.md +23 -0
- package/skills/luckiest-copywriting/SKILL.md +296 -0
- package/skills/luckiest-copywriting/evals/evals.json +111 -0
- package/skills/luckiest-copywriting/references/copy-frameworks.md +344 -0
- package/skills/luckiest-copywriting/references/natural-transitions.md +272 -0
- package/skills/luckiest-cro/ATTRIBUTION.md +34 -0
- package/skills/luckiest-cro/CHANGELOG.md +23 -0
- package/skills/luckiest-cro/SKILL.md +215 -0
- package/skills/luckiest-cro/evals/evals.json +111 -0
- package/skills/luckiest-cro/references/experiments.md +248 -0
- package/skills/luckiest-cro/references/form.md +422 -0
- package/skills/luckiest-customer-research/ATTRIBUTION.md +40 -0
- package/skills/luckiest-customer-research/CHANGELOG.md +26 -0
- package/skills/luckiest-customer-research/SKILL.md +293 -0
- package/skills/luckiest-customer-research/evals/evals.json +162 -0
- package/skills/luckiest-customer-research/references/source-guides.md +401 -0
- package/skills/luckiest-directory-submissions/ATTRIBUTION.md +40 -0
- package/skills/luckiest-directory-submissions/CHANGELOG.md +33 -0
- package/skills/luckiest-directory-submissions/SKILL.md +419 -0
- package/skills/luckiest-directory-submissions/evals/evals.json +94 -0
- package/skills/luckiest-directory-submissions/references/directory-list.md +477 -0
- package/skills/luckiest-directory-submissions/references/positioning-variations.md +232 -0
- package/skills/luckiest-directory-submissions/references/submission-tracker-template.csv +266 -0
- package/skills/luckiest-emails/ATTRIBUTION.md +42 -0
- package/skills/luckiest-emails/CHANGELOG.md +33 -0
- package/skills/luckiest-emails/SKILL.md +220 -0
- package/skills/luckiest-emails/evals/evals.json +93 -0
- package/skills/luckiest-emails/references/copy-guidelines.md +120 -0
- package/skills/luckiest-emails/references/email-types.md +515 -0
- package/skills/luckiest-emails/references/sequence-templates.md +168 -0
- package/skills/luckiest-extract-design-system/ATTRIBUTION.md +8 -0
- package/skills/luckiest-extract-design-system/CHANGELOG.md +37 -0
- package/skills/luckiest-extract-design-system/SKILL.md +83 -0
- package/skills/luckiest-free-tools/ATTRIBUTION.md +43 -0
- package/skills/luckiest-free-tools/CHANGELOG.md +25 -0
- package/skills/luckiest-free-tools/SKILL.md +220 -0
- package/skills/luckiest-free-tools/evals/evals.json +90 -0
- package/skills/luckiest-free-tools/references/tool-types.md +217 -0
- package/skills/luckiest-image/ATTRIBUTION.md +43 -0
- package/skills/luckiest-image/CHANGELOG.md +23 -0
- package/skills/luckiest-image/SKILL.md +376 -0
- package/skills/luckiest-image/evals/evals.json +89 -0
- package/skills/luckiest-image/references/ai-image-prompting.md +229 -0
- package/skills/luckiest-launch/ATTRIBUTION.md +40 -0
- package/skills/luckiest-launch/CHANGELOG.md +43 -0
- package/skills/luckiest-launch/SKILL.md +385 -0
- package/skills/luckiest-launch/evals/evals.json +91 -0
- package/skills/luckiest-lead-magnets/ATTRIBUTION.md +35 -0
- package/skills/luckiest-lead-magnets/CHANGELOG.md +23 -0
- package/skills/luckiest-lead-magnets/SKILL.md +331 -0
- package/skills/luckiest-lead-magnets/evals/evals.json +87 -0
- package/skills/luckiest-lead-magnets/references/benchmarks.md +129 -0
- package/skills/luckiest-lead-magnets/references/format-guide.md +196 -0
- package/skills/luckiest-marketing-ideas/ATTRIBUTION.md +39 -0
- package/skills/luckiest-marketing-ideas/CHANGELOG.md +33 -0
- package/skills/luckiest-marketing-ideas/SKILL.md +191 -0
- package/skills/luckiest-marketing-ideas/evals/evals.json +90 -0
- package/skills/luckiest-marketing-ideas/references/ideas-by-category.md +366 -0
- package/skills/luckiest-marketing-plan/ATTRIBUTION.md +40 -0
- package/skills/luckiest-marketing-plan/CHANGELOG.md +23 -0
- package/skills/luckiest-marketing-plan/SKILL.md +285 -0
- package/skills/luckiest-marketing-plan/evals/evals.json +96 -0
- package/skills/luckiest-marketing-plan/references/aarrr-framework.md +180 -0
- package/skills/luckiest-marketing-plan/references/budget-planning.md +168 -0
- package/skills/luckiest-marketing-plan/references/client-types.md +373 -0
- package/skills/luckiest-marketing-plan/references/current-state-rubric.md +255 -0
- package/skills/luckiest-marketing-plan/references/example-quietude.md +972 -0
- package/skills/luckiest-marketing-plan/references/funding-stage-unlocks.md +230 -0
- package/skills/luckiest-marketing-plan/references/growth-patterns.md +148 -0
- package/skills/luckiest-marketing-plan/references/idea-cross-reference.md +265 -0
- package/skills/luckiest-marketing-plan/references/measurement-framework.md +213 -0
- package/skills/luckiest-marketing-plan/references/methodology.md +363 -0
- package/skills/luckiest-marketing-plan/references/ops-stack-mapping.md +197 -0
- package/skills/luckiest-marketing-plan/references/plan-template.md +494 -0
- package/skills/luckiest-marketing-plan/references/team-and-agency-model.md +278 -0
- package/skills/luckiest-marketing-psychology/ATTRIBUTION.md +31 -0
- package/skills/luckiest-marketing-psychology/CHANGELOG.md +32 -0
- package/skills/luckiest-marketing-psychology/SKILL.md +202 -0
- package/skills/luckiest-marketing-psychology/evals.json +88 -0
- package/skills/luckiest-model-router/ATTRIBUTION.md +11 -0
- package/skills/luckiest-model-router/CHANGELOG.md +45 -0
- package/skills/luckiest-model-router/SKILL.md +74 -0
- package/skills/luckiest-model-router/scripts/route.py +145 -0
- package/skills/luckiest-offers/ATTRIBUTION.md +39 -0
- package/skills/luckiest-offers/CHANGELOG.md +23 -0
- package/skills/luckiest-offers/SKILL.md +167 -0
- package/skills/luckiest-offers/references/bonus-stacking.md +150 -0
- package/skills/luckiest-offers/references/examples.md +215 -0
- package/skills/luckiest-offers/references/guarantee-design.md +172 -0
- package/skills/luckiest-offers/references/offer-anatomy.md +203 -0
- package/skills/luckiest-offers/references/offer-formats.md +240 -0
- package/skills/luckiest-offers/references/scarcity-urgency.md +175 -0
- package/skills/luckiest-offers/references/value-equation.md +134 -0
- package/skills/luckiest-onboarding/ATTRIBUTION.md +43 -0
- package/skills/luckiest-onboarding/CHANGELOG.md +31 -0
- package/skills/luckiest-onboarding/SKILL.md +243 -0
- package/skills/luckiest-onboarding/references/experiments.md +258 -0
- package/skills/luckiest-paywalls/ATTRIBUTION.md +39 -0
- package/skills/luckiest-paywalls/CHANGELOG.md +25 -0
- package/skills/luckiest-paywalls/SKILL.md +256 -0
- package/skills/luckiest-paywalls/evals/evals.json +93 -0
- package/skills/luckiest-paywalls/references/experiments.md +164 -0
- package/skills/luckiest-popups/ATTRIBUTION.md +35 -0
- package/skills/luckiest-popups/CHANGELOG.md +27 -0
- package/skills/luckiest-popups/SKILL.md +477 -0
- package/skills/luckiest-popups/evals/evals.json +107 -0
- package/skills/luckiest-pricing/ATTRIBUTION.md +35 -0
- package/skills/luckiest-pricing/CHANGELOG.md +31 -0
- package/skills/luckiest-pricing/SKILL.md +283 -0
- package/skills/luckiest-pricing/evals/evals.json +90 -0
- package/skills/luckiest-pricing/references/research-methods.md +152 -0
- package/skills/luckiest-pricing/references/tier-structure.md +232 -0
- package/skills/luckiest-product-marketing/ATTRIBUTION.md +38 -0
- package/skills/luckiest-product-marketing/CHANGELOG.md +23 -0
- package/skills/luckiest-product-marketing/SKILL.md +256 -0
- package/skills/luckiest-product-marketing/evals/evals.json +85 -0
- package/skills/luckiest-programmatic-seo/ATTRIBUTION.md +35 -0
- package/skills/luckiest-programmatic-seo/CHANGELOG.md +24 -0
- package/skills/luckiest-programmatic-seo/SKILL.md +286 -0
- package/skills/luckiest-programmatic-seo/evals/evals.json +95 -0
- package/skills/luckiest-programmatic-seo/references/playbooks.md +308 -0
- package/skills/luckiest-prompt-rewrite/ATTRIBUTION.md +39 -0
- package/skills/luckiest-prompt-rewrite/CHANGELOG.md +70 -0
- package/skills/luckiest-prompt-rewrite/LICENSE +21 -0
- package/skills/luckiest-prompt-rewrite/SKILL.md +494 -0
- package/skills/luckiest-prompt-rewrite/references/patterns.md +82 -0
- package/skills/luckiest-prompt-rewrite/references/templates.md +454 -0
- package/skills/luckiest-prospecting/ATTRIBUTION.md +29 -0
- package/skills/luckiest-prospecting/CHANGELOG.md +24 -0
- package/skills/luckiest-prospecting/SKILL.md +275 -0
- package/skills/luckiest-prospecting/evals/evals.json +107 -0
- package/skills/luckiest-prospecting/references/b2b-prospecting.md +106 -0
- package/skills/luckiest-prospecting/references/compliance.md +123 -0
- package/skills/luckiest-prospecting/references/data-sources.md +287 -0
- package/skills/luckiest-prospecting/references/local-prospecting.md +165 -0
- package/skills/luckiest-prospecting/references/saas-prospecting.md +123 -0
- package/skills/luckiest-public-relations/ATTRIBUTION.md +34 -0
- package/skills/luckiest-public-relations/CHANGELOG.md +23 -0
- package/skills/luckiest-public-relations/SKILL.md +154 -0
- package/skills/luckiest-public-relations/references/journalist-pitching.md +368 -0
- package/skills/luckiest-public-relations/references/media-outlets.md +207 -0
- package/skills/luckiest-public-relations/references/newsjacking.md +218 -0
- package/skills/luckiest-public-relations/references/press-platforms.md +188 -0
- package/skills/luckiest-referrals/ATTRIBUTION.md +41 -0
- package/skills/luckiest-referrals/CHANGELOG.md +23 -0
- package/skills/luckiest-referrals/SKILL.md +292 -0
- package/skills/luckiest-referrals/evals/evals.json +90 -0
- package/skills/luckiest-referrals/references/affiliate-programs.md +167 -0
- package/skills/luckiest-referrals/references/program-examples.md +143 -0
- package/skills/luckiest-revops/ATTRIBUTION.md +40 -0
- package/skills/luckiest-revops/CHANGELOG.md +24 -0
- package/skills/luckiest-revops/SKILL.md +362 -0
- package/skills/luckiest-revops/evals/evals.json +91 -0
- package/skills/luckiest-revops/references/automation-playbooks.md +290 -0
- package/skills/luckiest-revops/references/lifecycle-definitions.md +278 -0
- package/skills/luckiest-revops/references/routing-rules.md +203 -0
- package/skills/luckiest-revops/references/scoring-models.md +247 -0
- package/skills/luckiest-sales-enablement/ATTRIBUTION.md +39 -0
- package/skills/luckiest-sales-enablement/CHANGELOG.md +23 -0
- package/skills/luckiest-sales-enablement/SKILL.md +379 -0
- package/skills/luckiest-sales-enablement/evals/evals.json +91 -0
- package/skills/luckiest-sales-enablement/references/deck-frameworks.md +263 -0
- package/skills/luckiest-sales-enablement/references/demo-scripts.md +355 -0
- package/skills/luckiest-sales-enablement/references/objection-library.md +270 -0
- package/skills/luckiest-sales-enablement/references/one-pager-templates.md +208 -0
- package/skills/luckiest-schema/ATTRIBUTION.md +39 -0
- package/skills/luckiest-schema/CHANGELOG.md +25 -0
- package/skills/luckiest-schema/SKILL.md +209 -0
- package/skills/luckiest-schema/evals/evals.json +88 -0
- package/skills/luckiest-schema/references/schema-examples.md +398 -0
- package/skills/luckiest-seo-audit/ATTRIBUTION.md +39 -0
- package/skills/luckiest-seo-audit/CHANGELOG.md +23 -0
- package/skills/luckiest-seo-audit/SKILL.md +529 -0
- package/skills/luckiest-seo-audit/evals/evals.json +136 -0
- package/skills/luckiest-seo-audit/references/ai-writing-detection.md +200 -0
- package/skills/luckiest-seo-audit/references/international-seo.md +230 -0
- package/skills/luckiest-session-handoff/ATTRIBUTION.md +14 -0
- package/skills/luckiest-session-handoff/CHANGELOG.md +30 -0
- package/skills/luckiest-session-handoff/SKILL.md +96 -0
- package/skills/luckiest-signup/ATTRIBUTION.md +48 -0
- package/skills/luckiest-signup/CHANGELOG.md +24 -0
- package/skills/luckiest-signup/SKILL.md +382 -0
- package/skills/luckiest-signup/evals/evals.json +88 -0
- package/skills/luckiest-site-architecture/ATTRIBUTION.md +38 -0
- package/skills/luckiest-site-architecture/CHANGELOG.md +23 -0
- package/skills/luckiest-site-architecture/SKILL.md +383 -0
- package/skills/luckiest-site-architecture/evals/evals.json +88 -0
- package/skills/luckiest-site-architecture/references/mermaid-templates.md +216 -0
- package/skills/luckiest-site-architecture/references/navigation-patterns.md +305 -0
- package/skills/luckiest-site-architecture/references/site-type-templates.md +293 -0
- package/skills/luckiest-sms/ATTRIBUTION.md +27 -0
- package/skills/luckiest-sms/CHANGELOG.md +24 -0
- package/skills/luckiest-sms/SKILL.md +400 -0
- package/skills/luckiest-sms/evals/evals.json +100 -0
- package/skills/luckiest-sms/references/compliance.md +202 -0
- package/skills/luckiest-sms/references/platforms.md +318 -0
- package/skills/luckiest-sms/references/sequence-templates.md +282 -0
- package/skills/luckiest-social/ATTRIBUTION.md +27 -0
- package/skills/luckiest-social/CHANGELOG.md +24 -0
- package/skills/luckiest-social/SKILL.md +430 -0
- package/skills/luckiest-social/evals/evals.json +92 -0
- package/skills/luckiest-social/references/listening-sources-template.md +123 -0
- package/skills/luckiest-social/references/listening.md +283 -0
- package/skills/luckiest-social/references/platform-limits.md +110 -0
- package/skills/luckiest-social/references/platforms.md +170 -0
- package/skills/luckiest-social/references/post-templates.md +177 -0
- package/skills/luckiest-social/references/reverse-engineering.md +195 -0
- package/skills/luckiest-social/references/short-form-video.md +237 -0
- package/skills/luckiest-thinker/ATTRIBUTION.md +16 -0
- package/skills/luckiest-thinker/CHANGELOG.md +20 -0
- package/skills/luckiest-thinker/LICENSE +21 -0
- package/skills/luckiest-thinker/SKILL.md +92 -0
- package/skills/luckiest-thinker/references/distribution-calibration.md +106 -0
- package/skills/luckiest-thinker/references/research-foundations.md +214 -0
- package/skills/luckiest-thinker/references/thinking-patterns.md +138 -0
- package/skills/luckiest-trends/.skillignore +10 -0
- package/skills/luckiest-trends/ATTRIBUTION.md +43 -0
- package/skills/luckiest-trends/CHANGELOG.md +86 -0
- package/skills/luckiest-trends/LICENSE +21 -0
- package/skills/luckiest-trends/SKILL.md +194 -0
- package/skills/luckiest-trends/agents/openai.yaml +8 -0
- package/skills/luckiest-trends/references/engine-contract.md +2041 -0
- package/skills/luckiest-trends/references/save-html-brief.md +206 -0
- package/skills/luckiest-trends/scripts/briefing.py +272 -0
- package/skills/luckiest-trends/scripts/build-skill.sh +39 -0
- package/skills/luckiest-trends/scripts/compare.sh +61 -0
- package/skills/luckiest-trends/scripts/evaluate_search_quality.py +573 -0
- package/skills/luckiest-trends/scripts/last30days.py +1467 -0
- package/skills/luckiest-trends/scripts/lib/__init__.py +1 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/arxiv.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/bird_x.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/bluesky.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/categories.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/cjk.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/cluster.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/dates.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/dedupe.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/digg.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/entity_extract.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/env.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/fusion.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/github.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/grounding.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/hackernews.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/hiring_signals.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/html_render.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/http.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/instagram.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/jobs.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/linkedin.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/log.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/normalize.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/permission_preflight.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/perplexity.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/pinterest.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/pipeline.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/planner.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/polymarket.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/preflight.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/providers.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/quality_nudge.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/query.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/reddit.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_arctic.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_enrich.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_keyless.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_listing.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_public.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_rss.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_shreddit.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/relevance.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/render.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/rerank.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/resolve.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/schema.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/signals.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/skill_meta.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/snippet.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/subproc.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/techmeme.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/threads.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/tiktok.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/trustpilot.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/truthsocial.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/ui.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/web_search_keyless.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/xai_x.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/xiaohongshu_api.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/xquik.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/xurl_x.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/__pycache__/youtube_yt.cpython-313.pyc +0 -0
- package/skills/luckiest-trends/scripts/lib/arxiv.py +290 -0
- package/skills/luckiest-trends/scripts/lib/bird_x.py +639 -0
- package/skills/luckiest-trends/scripts/lib/bluesky.py +323 -0
- package/skills/luckiest-trends/scripts/lib/categories.py +283 -0
- package/skills/luckiest-trends/scripts/lib/chrome_cookies.py +414 -0
- package/skills/luckiest-trends/scripts/lib/cjk.py +107 -0
- package/skills/luckiest-trends/scripts/lib/cluster.py +271 -0
- package/skills/luckiest-trends/scripts/lib/competitors.py +198 -0
- package/skills/luckiest-trends/scripts/lib/cookie_extract.py +508 -0
- package/skills/luckiest-trends/scripts/lib/dates.py +169 -0
- package/skills/luckiest-trends/scripts/lib/dedupe.py +145 -0
- package/skills/luckiest-trends/scripts/lib/digg.py +440 -0
- package/skills/luckiest-trends/scripts/lib/entity_extract.py +127 -0
- package/skills/luckiest-trends/scripts/lib/env.py +1073 -0
- package/skills/luckiest-trends/scripts/lib/fanout.py +84 -0
- package/skills/luckiest-trends/scripts/lib/fusion.py +207 -0
- package/skills/luckiest-trends/scripts/lib/github.py +996 -0
- package/skills/luckiest-trends/scripts/lib/grounding.py +331 -0
- package/skills/luckiest-trends/scripts/lib/hackernews.py +357 -0
- package/skills/luckiest-trends/scripts/lib/health.py +92 -0
- package/skills/luckiest-trends/scripts/lib/hiring_signals.py +326 -0
- package/skills/luckiest-trends/scripts/lib/html_publish.py +73 -0
- package/skills/luckiest-trends/scripts/lib/html_render.py +738 -0
- package/skills/luckiest-trends/scripts/lib/http.py +372 -0
- package/skills/luckiest-trends/scripts/lib/instagram.py +521 -0
- package/skills/luckiest-trends/scripts/lib/jobs.py +708 -0
- package/skills/luckiest-trends/scripts/lib/linkedin.py +363 -0
- package/skills/luckiest-trends/scripts/lib/log.py +35 -0
- package/skills/luckiest-trends/scripts/lib/normalize.py +759 -0
- package/skills/luckiest-trends/scripts/lib/permission_preflight.py +242 -0
- package/skills/luckiest-trends/scripts/lib/perplexity.py +745 -0
- package/skills/luckiest-trends/scripts/lib/pinterest.py +154 -0
- package/skills/luckiest-trends/scripts/lib/pipeline.py +1720 -0
- package/skills/luckiest-trends/scripts/lib/planner.py +763 -0
- package/skills/luckiest-trends/scripts/lib/polymarket.py +794 -0
- package/skills/luckiest-trends/scripts/lib/preflight.py +119 -0
- package/skills/luckiest-trends/scripts/lib/providers.py +380 -0
- package/skills/luckiest-trends/scripts/lib/quality_nudge.py +383 -0
- package/skills/luckiest-trends/scripts/lib/query.py +202 -0
- package/skills/luckiest-trends/scripts/lib/reddit.py +731 -0
- package/skills/luckiest-trends/scripts/lib/reddit_arctic.py +92 -0
- package/skills/luckiest-trends/scripts/lib/reddit_enrich.py +322 -0
- package/skills/luckiest-trends/scripts/lib/reddit_keyless.py +329 -0
- package/skills/luckiest-trends/scripts/lib/reddit_listing.py +188 -0
- package/skills/luckiest-trends/scripts/lib/reddit_public.py +275 -0
- package/skills/luckiest-trends/scripts/lib/reddit_rss.py +224 -0
- package/skills/luckiest-trends/scripts/lib/reddit_shreddit.py +184 -0
- package/skills/luckiest-trends/scripts/lib/relevance.py +200 -0
- package/skills/luckiest-trends/scripts/lib/render.py +2130 -0
- package/skills/luckiest-trends/scripts/lib/rerank.py +708 -0
- package/skills/luckiest-trends/scripts/lib/resolve.py +345 -0
- package/skills/luckiest-trends/scripts/lib/safari_cookies.py +193 -0
- package/skills/luckiest-trends/scripts/lib/schema.py +319 -0
- package/skills/luckiest-trends/scripts/lib/setup_wizard.py +843 -0
- package/skills/luckiest-trends/scripts/lib/signals.py +334 -0
- package/skills/luckiest-trends/scripts/lib/skill_meta.py +33 -0
- package/skills/luckiest-trends/scripts/lib/snippet.py +51 -0
- package/skills/luckiest-trends/scripts/lib/subproc.py +116 -0
- package/skills/luckiest-trends/scripts/lib/techmeme.py +239 -0
- package/skills/luckiest-trends/scripts/lib/threads.py +207 -0
- package/skills/luckiest-trends/scripts/lib/tiktok.py +596 -0
- package/skills/luckiest-trends/scripts/lib/transcribe.py +235 -0
- package/skills/luckiest-trends/scripts/lib/trustpilot.py +277 -0
- package/skills/luckiest-trends/scripts/lib/truthsocial.py +162 -0
- package/skills/luckiest-trends/scripts/lib/ui.py +626 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/LICENSE +21 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/bird-search.mjs +147 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/cookies.js +220 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/features.json +17 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/paginate-cursor.js +37 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/query-ids.json +20 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/runtime-features.js +151 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/runtime-query-ids.js +264 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/twitter-client-base.js +129 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/twitter-client-constants.js +50 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/twitter-client-features.js +347 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/twitter-client-search.js +157 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/twitter-client-types.js +2 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/twitter-client-utils.js +511 -0
- package/skills/luckiest-trends/scripts/lib/vendor/bird-search/package.json +13 -0
- package/skills/luckiest-trends/scripts/lib/web_fetch_keyless.py +105 -0
- package/skills/luckiest-trends/scripts/lib/web_search_keyless.py +158 -0
- package/skills/luckiest-trends/scripts/lib/xai_x.py +236 -0
- package/skills/luckiest-trends/scripts/lib/xiaohongshu_api.py +162 -0
- package/skills/luckiest-trends/scripts/lib/xquik.py +404 -0
- package/skills/luckiest-trends/scripts/lib/xurl_x.py +171 -0
- package/skills/luckiest-trends/scripts/lib/youtube_yt.py +1365 -0
- package/skills/luckiest-trends/scripts/setup-keychain.sh +123 -0
- package/skills/luckiest-trends/scripts/setup-pass.sh +132 -0
- package/skills/luckiest-trends/scripts/store.py +1020 -0
- package/skills/luckiest-trends/scripts/test-v1-vs-v2.sh +220 -0
- package/skills/luckiest-trends/scripts/test_device_auth.py +128 -0
- package/skills/luckiest-trends/scripts/verify_v3.py +195 -0
- package/skills/luckiest-trends/scripts/watchlist.py +318 -0
- package/skills/luckiest-video/ATTRIBUTION.md +27 -0
- package/skills/luckiest-video/CHANGELOG.md +23 -0
- package/skills/luckiest-video/SKILL.md +371 -0
- package/skills/luckiest-video/evals/evals.json +88 -0
- package/skills/luckiest-video/references/ai-video-prompting.md +175 -0
- package/skills/remix/SKILL.md +32 -0
- package/commands/luckiest/go.md +0 -45
- package/commands/luckiest/plan.md +0 -55
- /package/commands/{luckiest/charms.md → charms.md} +0 -0
- /package/commands/{luckiest/helpers.md → helpers.md} +0 -0
- /package/commands/{luckiest/home.md → home.md} +0 -0
- /package/commands/{luckiest/leaderboard.md → leaderboard.md} +0 -0
- /package/commands/{luckiest/merge.md → merge.md} +0 -0
- /package/commands/{luckiest/overlaps.md → overlaps.md} +0 -0
- /package/commands/{luckiest/skills.md → skills.md} +0 -0
- /package/commands/{luckiest/start.md → start.md} +0 -0
- /package/commands/{luckiest/status.md → status.md} +0 -0
- /package/commands/{luckiest/updates.md → updates.md} +0 -0
- /package/commands/{luckiest/vouch.md → vouch.md} +0 -0
- /package/commands/{luckiest/wishes.md → wishes.md} +0 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Reddit public ``.json`` search module (demoted to keyless Tier 0).
|
|
2
|
+
|
|
3
|
+
Reddit's public ``.json`` endpoints now return HTTP 403 from most contexts
|
|
4
|
+
(shreddit anti-bot), so this is no longer the primary free path. The keyless
|
|
5
|
+
pipeline (see reddit_keyless.py) still calls ``search`` as a cheap one-shot
|
|
6
|
+
Tier 0 attempt — a residential machine may occasionally get a 200 — before
|
|
7
|
+
falling through to RSS discovery (reddit_rss.py) and shreddit comment
|
|
8
|
+
enrichment (reddit_shreddit.py).
|
|
9
|
+
|
|
10
|
+
``search_reddit_public`` is retained as a compatibility shim that delegates to
|
|
11
|
+
the keyless pipeline, so existing callers (pipeline.py) need no change.
|
|
12
|
+
|
|
13
|
+
Endpoints (Tier 0):
|
|
14
|
+
- Global: https://www.reddit.com/search.json?q={query}&sort=relevance&t=month&limit={limit}
|
|
15
|
+
- Subreddit: https://www.reddit.com/r/{sub}/search.json?q={query}&restrict_sr=on&sort=relevance&t=month
|
|
16
|
+
|
|
17
|
+
Handles 429 rate limits with exponential backoff, HTML anti-bot responses,
|
|
18
|
+
network timeouts, and missing subreddits.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import gzip
|
|
22
|
+
import json
|
|
23
|
+
import sys
|
|
24
|
+
import time
|
|
25
|
+
import urllib.error
|
|
26
|
+
import urllib.parse
|
|
27
|
+
import urllib.request
|
|
28
|
+
from typing import Any, Dict, List, Optional
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
USER_AGENT = (
|
|
32
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
|
33
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
34
|
+
"Chrome/124.0.0.0 Safari/537.36"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Depth-aware limits for thread counts
|
|
38
|
+
DEPTH_LIMITS = {
|
|
39
|
+
"quick": 10,
|
|
40
|
+
"default": 25,
|
|
41
|
+
"deep": 50,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
MAX_RETRIES = 3
|
|
45
|
+
BASE_BACKOFF = 2.0 # seconds
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _log(msg: str):
|
|
49
|
+
"""Log to stderr."""
|
|
50
|
+
sys.stderr.write(f"[RedditPublic] {msg}\n")
|
|
51
|
+
sys.stderr.flush()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _url_encode(text: str) -> str:
|
|
55
|
+
"""URL-encode a query string."""
|
|
56
|
+
return urllib.parse.quote_plus(text)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _fetch_json(url: str, timeout: int = 15) -> Optional[Dict[str, Any]]:
|
|
60
|
+
"""Fetch JSON from a URL with retry on 429 and error handling.
|
|
61
|
+
|
|
62
|
+
Returns parsed JSON dict, or None on unrecoverable failure.
|
|
63
|
+
"""
|
|
64
|
+
headers = {
|
|
65
|
+
"User-Agent": USER_AGENT,
|
|
66
|
+
"Accept": "application/json",
|
|
67
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
68
|
+
"Accept-Encoding": "gzip, deflate",
|
|
69
|
+
"Connection": "keep-alive",
|
|
70
|
+
}
|
|
71
|
+
req = urllib.request.Request(url, headers=headers)
|
|
72
|
+
|
|
73
|
+
for attempt in range(MAX_RETRIES):
|
|
74
|
+
try:
|
|
75
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
76
|
+
content_type = resp.headers.get("Content-Type", "")
|
|
77
|
+
if "json" not in content_type and "text/html" in content_type:
|
|
78
|
+
_log(f"Anti-bot HTML response (Content-Type: {content_type})")
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
raw = resp.read()
|
|
82
|
+
if resp.headers.get("Content-Encoding", "").lower() == "gzip":
|
|
83
|
+
raw = gzip.decompress(raw)
|
|
84
|
+
body = raw.decode("utf-8")
|
|
85
|
+
return json.loads(body)
|
|
86
|
+
|
|
87
|
+
except urllib.error.HTTPError as e:
|
|
88
|
+
if e.code == 429:
|
|
89
|
+
delay = BASE_BACKOFF * (2 ** attempt)
|
|
90
|
+
retry_after = None
|
|
91
|
+
if hasattr(e, "headers"):
|
|
92
|
+
retry_after = e.headers.get("Retry-After")
|
|
93
|
+
if retry_after:
|
|
94
|
+
try:
|
|
95
|
+
delay = float(retry_after)
|
|
96
|
+
except ValueError:
|
|
97
|
+
pass
|
|
98
|
+
_log(f"429 rate limited, retry {attempt + 1}/{MAX_RETRIES} after {delay:.1f}s")
|
|
99
|
+
if attempt < MAX_RETRIES - 1:
|
|
100
|
+
time.sleep(delay)
|
|
101
|
+
continue
|
|
102
|
+
# Last attempt exhausted
|
|
103
|
+
_log("429 retries exhausted")
|
|
104
|
+
return None
|
|
105
|
+
elif e.code == 404:
|
|
106
|
+
_log(f"404 not found: {url}")
|
|
107
|
+
return None
|
|
108
|
+
elif e.code == 403:
|
|
109
|
+
_log(f"403 forbidden: {url}")
|
|
110
|
+
return None
|
|
111
|
+
else:
|
|
112
|
+
_log(f"HTTP {e.code}: {e.reason}")
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
except (urllib.error.URLError, OSError, TimeoutError) as e:
|
|
116
|
+
_log(f"Network error: {e}")
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
except json.JSONDecodeError as e:
|
|
120
|
+
_log(f"JSON decode error: {e}")
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _parse_posts(data: Optional[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
127
|
+
"""Parse Reddit listing JSON into normalized post dicts."""
|
|
128
|
+
if not data:
|
|
129
|
+
return []
|
|
130
|
+
|
|
131
|
+
children = data.get("data", {}).get("children", [])
|
|
132
|
+
posts = []
|
|
133
|
+
|
|
134
|
+
for child in children:
|
|
135
|
+
if child.get("kind") != "t3":
|
|
136
|
+
continue
|
|
137
|
+
post = child.get("data", {})
|
|
138
|
+
permalink = str(post.get("permalink", "")).strip()
|
|
139
|
+
if not permalink or "/comments/" not in permalink:
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
score = int(post.get("score", 0) or 0)
|
|
143
|
+
num_comments = int(post.get("num_comments", 0) or 0)
|
|
144
|
+
selftext = str(post.get("selftext", ""))
|
|
145
|
+
author = str(post.get("author", "[deleted]"))
|
|
146
|
+
created_utc = post.get("created_utc")
|
|
147
|
+
|
|
148
|
+
# Parse date
|
|
149
|
+
date_str = None
|
|
150
|
+
if created_utc:
|
|
151
|
+
try:
|
|
152
|
+
from datetime import datetime, timezone
|
|
153
|
+
dt = datetime.fromtimestamp(float(created_utc), tz=timezone.utc)
|
|
154
|
+
date_str = dt.strftime("%Y-%m-%d")
|
|
155
|
+
except (ValueError, TypeError, OSError):
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
posts.append({
|
|
159
|
+
"id": "", # Will be assigned after dedup
|
|
160
|
+
"title": str(post.get("title", "")).strip(),
|
|
161
|
+
"url": f"https://www.reddit.com{permalink}",
|
|
162
|
+
"score": score,
|
|
163
|
+
"num_comments": num_comments,
|
|
164
|
+
"subreddit": str(post.get("subreddit", "")).strip(),
|
|
165
|
+
"created_utc": float(created_utc) if created_utc else None,
|
|
166
|
+
"author": author if author not in ("[deleted]", "[removed]") else "[deleted]",
|
|
167
|
+
"selftext": selftext[:500] if selftext else "",
|
|
168
|
+
# Normalized fields matching ScrapeCreators output
|
|
169
|
+
"date": date_str,
|
|
170
|
+
"engagement": {
|
|
171
|
+
"score": score,
|
|
172
|
+
"num_comments": num_comments,
|
|
173
|
+
"upvote_ratio": post.get("upvote_ratio"),
|
|
174
|
+
},
|
|
175
|
+
"relevance": _compute_relevance(score, num_comments),
|
|
176
|
+
"why_relevant": "Reddit public search",
|
|
177
|
+
"metadata": {},
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
return posts
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _compute_relevance(score: int, num_comments: int) -> float:
|
|
184
|
+
"""Estimate relevance from engagement signals."""
|
|
185
|
+
score_component = min(1.0, max(0.0, score / 500.0))
|
|
186
|
+
comments_component = min(1.0, max(0.0, num_comments / 200.0))
|
|
187
|
+
return round((score_component * 0.6) + (comments_component * 0.4), 3)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def search(
|
|
191
|
+
query: str,
|
|
192
|
+
depth: str = "default",
|
|
193
|
+
subreddit: Optional[str] = None,
|
|
194
|
+
timeout: int = 15,
|
|
195
|
+
) -> List[Dict[str, Any]]:
|
|
196
|
+
"""Search Reddit via the public JSON endpoint.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
query: Search query string
|
|
200
|
+
depth: 'quick', 'default', or 'deep' — controls result limit
|
|
201
|
+
subreddit: Optional subreddit name (without r/) for scoped search
|
|
202
|
+
timeout: HTTP timeout in seconds
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
List of normalized post dicts. Empty list on any failure.
|
|
206
|
+
"""
|
|
207
|
+
limit = DEPTH_LIMITS.get(depth, DEPTH_LIMITS["default"])
|
|
208
|
+
encoded_query = _url_encode(query)
|
|
209
|
+
|
|
210
|
+
if subreddit:
|
|
211
|
+
sub = subreddit.removeprefix("r/").strip()
|
|
212
|
+
url = (
|
|
213
|
+
f"https://www.reddit.com/r/{sub}/search.json"
|
|
214
|
+
f"?q={encoded_query}&restrict_sr=on&sort=relevance&t=month&limit={limit}&raw_json=1"
|
|
215
|
+
)
|
|
216
|
+
else:
|
|
217
|
+
url = (
|
|
218
|
+
f"https://www.reddit.com/search.json"
|
|
219
|
+
f"?q={encoded_query}&sort=relevance&t=month&limit={limit}&raw_json=1"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
data = _fetch_json(url, timeout=timeout)
|
|
223
|
+
posts = _parse_posts(data)
|
|
224
|
+
|
|
225
|
+
# Dedupe by URL and assign IDs
|
|
226
|
+
seen_urls = set()
|
|
227
|
+
unique = []
|
|
228
|
+
for post in posts:
|
|
229
|
+
if post["url"] not in seen_urls:
|
|
230
|
+
seen_urls.add(post["url"])
|
|
231
|
+
unique.append(post)
|
|
232
|
+
|
|
233
|
+
for i, post in enumerate(unique):
|
|
234
|
+
post["id"] = f"R{i + 1}"
|
|
235
|
+
|
|
236
|
+
return unique[:limit]
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def search_reddit_public(
|
|
240
|
+
topic: str,
|
|
241
|
+
from_date: str,
|
|
242
|
+
to_date: str,
|
|
243
|
+
depth: str = "default",
|
|
244
|
+
subreddits: Optional[List[str]] = None,
|
|
245
|
+
dedicated_subreddits: Optional[List[str]] = None,
|
|
246
|
+
) -> List[Dict[str, Any]]:
|
|
247
|
+
"""High-level free Reddit search + enrichment (keyless).
|
|
248
|
+
|
|
249
|
+
Thin compatibility shim over the keyless pipeline: the legacy ``.json``
|
|
250
|
+
search/enrichment endpoints now return HTTP 403, so this delegates to
|
|
251
|
+
``reddit_keyless.search_and_enrich`` (dedicated-sub listings + RSS discovery
|
|
252
|
+
→ shreddit comment enrichment; no ``.json`` search). The name and signature
|
|
253
|
+
are preserved so ``pipeline.py`` and other callers need no change and the
|
|
254
|
+
ScrapeCreators backup still engages when this returns empty.
|
|
255
|
+
|
|
256
|
+
The module-level ``search`` / ``_parse_posts`` helpers remain as a
|
|
257
|
+
standalone ``.json`` search utility (own test coverage), no longer wired
|
|
258
|
+
into the keyless production path.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
topic: Search topic
|
|
262
|
+
from_date: Start date (YYYY-MM-DD)
|
|
263
|
+
to_date: End date (YYYY-MM-DD)
|
|
264
|
+
depth: 'quick', 'default', or 'deep'
|
|
265
|
+
subreddits: Optional list of subreddit names (without r/) for targeted search
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
List of normalized item dicts matching ScrapeCreators output format.
|
|
269
|
+
Empty list on total failure (so SC backup can engage).
|
|
270
|
+
"""
|
|
271
|
+
from . import reddit_keyless
|
|
272
|
+
return reddit_keyless.search_and_enrich(
|
|
273
|
+
topic, from_date, to_date, depth=depth, subreddits=subreddits,
|
|
274
|
+
dedicated_subreddits=dedicated_subreddits,
|
|
275
|
+
)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Keyless Reddit discovery via public RSS/Atom feeds.
|
|
2
|
+
|
|
3
|
+
Reddit's ``.json`` search endpoints now return HTTP 403 (shreddit anti-bot).
|
|
4
|
+
RSS feeds still serve HTTP 200 with no API key, so this module uses them for
|
|
5
|
+
post discovery, replacing ``reddit_public.search`` as the free search path.
|
|
6
|
+
|
|
7
|
+
Two feed families are combined and deduped:
|
|
8
|
+
- search: /search.rss?q=... and /r/{sub}/search.rss?q=...&restrict_sr=on
|
|
9
|
+
- listing: /r/{sub}/{top,hot}.rss?t=month
|
|
10
|
+
|
|
11
|
+
RSS entries carry no engagement score, so ``score``/``num_comments`` start at 0
|
|
12
|
+
and are backfilled during shreddit enrichment (see reddit_shreddit.py). Output
|
|
13
|
+
dicts match the normalized shape emitted by ``reddit_public._parse_posts`` so
|
|
14
|
+
downstream code (pipeline, renderer) is unaffected.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
import xml.etree.ElementTree as ET
|
|
19
|
+
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeoutError
|
|
20
|
+
from datetime import datetime, timezone
|
|
21
|
+
from typing import Any, Dict, List, Optional
|
|
22
|
+
from urllib.parse import quote_plus
|
|
23
|
+
|
|
24
|
+
from . import http
|
|
25
|
+
from .relevance import token_overlap_relevance
|
|
26
|
+
|
|
27
|
+
ATOM = "{http://www.w3.org/2005/Atom}"
|
|
28
|
+
|
|
29
|
+
# Mirror reddit_public depth-aware limits so the two free paths behave alike.
|
|
30
|
+
DEPTH_LIMITS = {
|
|
31
|
+
"quick": 10,
|
|
32
|
+
"default": 25,
|
|
33
|
+
"deep": 50,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Listing sorts pulled per subreddit (in addition to search), for volume.
|
|
37
|
+
LISTING_SORTS = {
|
|
38
|
+
"quick": ["top"],
|
|
39
|
+
"default": ["top", "hot"],
|
|
40
|
+
"deep": ["top", "hot", "new"],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
MAX_WORKERS = 4
|
|
44
|
+
FEED_TIMEOUT = 15
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _log(msg: str) -> None:
|
|
48
|
+
sys.stderr.write(f"[RedditRSS] {msg}\n")
|
|
49
|
+
sys.stderr.flush()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _iso_to_date(value: Optional[str]) -> Optional[str]:
|
|
53
|
+
"""Parse an ISO-8601 timestamp (e.g. 2026-05-20T18:48:31+00:00) to YYYY-MM-DD."""
|
|
54
|
+
if not value:
|
|
55
|
+
return None
|
|
56
|
+
try:
|
|
57
|
+
dt = datetime.fromisoformat(value.strip())
|
|
58
|
+
return dt.date().isoformat()
|
|
59
|
+
except (ValueError, TypeError):
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _iso_to_epoch(value: Optional[str]) -> Optional[float]:
|
|
64
|
+
if not value:
|
|
65
|
+
return None
|
|
66
|
+
try:
|
|
67
|
+
dt = datetime.fromisoformat(value.strip())
|
|
68
|
+
if dt.tzinfo is None:
|
|
69
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
70
|
+
return dt.timestamp()
|
|
71
|
+
except (ValueError, TypeError):
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _subreddit_from(category: str, url: str) -> str:
|
|
76
|
+
"""Derive subreddit name from the entry category or, failing that, the URL."""
|
|
77
|
+
if category:
|
|
78
|
+
return category
|
|
79
|
+
# URL form: https://www.reddit.com/r/{sub}/comments/{id}/...
|
|
80
|
+
parts = url.split("/r/", 1)
|
|
81
|
+
if len(parts) == 2:
|
|
82
|
+
return parts[1].split("/", 1)[0]
|
|
83
|
+
return ""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _parse_feed(xml_text: str, query: str = "") -> List[Dict[str, Any]]:
|
|
87
|
+
"""Parse an Atom feed string into normalized post dicts. Never raises."""
|
|
88
|
+
if not xml_text:
|
|
89
|
+
return []
|
|
90
|
+
try:
|
|
91
|
+
root = ET.fromstring(xml_text)
|
|
92
|
+
except ET.ParseError as e:
|
|
93
|
+
_log(f"feed parse error: {e}")
|
|
94
|
+
return []
|
|
95
|
+
|
|
96
|
+
posts: List[Dict[str, Any]] = []
|
|
97
|
+
for entry in root.iter(f"{ATOM}entry"):
|
|
98
|
+
link_el = entry.find(f"{ATOM}link")
|
|
99
|
+
url = link_el.get("href", "").strip() if link_el is not None else ""
|
|
100
|
+
if not url or "/comments/" not in url:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
title_el = entry.find(f"{ATOM}title")
|
|
104
|
+
title = (title_el.text or "").strip() if title_el is not None else ""
|
|
105
|
+
|
|
106
|
+
author = ""
|
|
107
|
+
author_el = entry.find(f"{ATOM}author/{ATOM}name")
|
|
108
|
+
if author_el is not None and author_el.text:
|
|
109
|
+
author = author_el.text.strip().removeprefix("/u/").removeprefix("u/")
|
|
110
|
+
if author in ("[deleted]", "[removed]", ""):
|
|
111
|
+
author = "[deleted]"
|
|
112
|
+
|
|
113
|
+
cat_el = entry.find(f"{ATOM}category")
|
|
114
|
+
category = cat_el.get("term", "").strip() if cat_el is not None else ""
|
|
115
|
+
subreddit = _subreddit_from(category, url)
|
|
116
|
+
|
|
117
|
+
updated_el = entry.find(f"{ATOM}updated")
|
|
118
|
+
updated = (updated_el.text or "").strip() if updated_el is not None else ""
|
|
119
|
+
|
|
120
|
+
content_el = entry.find(f"{ATOM}content")
|
|
121
|
+
selftext = ""
|
|
122
|
+
if content_el is not None and content_el.text:
|
|
123
|
+
# Strip the simplest HTML; renderer only needs an excerpt.
|
|
124
|
+
import re as _re
|
|
125
|
+
selftext = _re.sub(r"<[^>]+>", " ", content_el.text)
|
|
126
|
+
selftext = _re.sub(r"\s+", " ", selftext).strip()[:500]
|
|
127
|
+
|
|
128
|
+
relevance = round(token_overlap_relevance(query, title), 3) if query else 0.0
|
|
129
|
+
|
|
130
|
+
posts.append({
|
|
131
|
+
"id": "", # assigned after dedup
|
|
132
|
+
"title": title,
|
|
133
|
+
"url": url,
|
|
134
|
+
"score": 0, # backfilled by shreddit enrichment
|
|
135
|
+
"num_comments": 0, # backfilled by shreddit enrichment
|
|
136
|
+
"subreddit": subreddit,
|
|
137
|
+
"created_utc": _iso_to_epoch(updated),
|
|
138
|
+
"author": author,
|
|
139
|
+
"selftext": selftext,
|
|
140
|
+
"date": _iso_to_date(updated),
|
|
141
|
+
"engagement": {
|
|
142
|
+
"score": 0,
|
|
143
|
+
"num_comments": 0,
|
|
144
|
+
"upvote_ratio": None,
|
|
145
|
+
},
|
|
146
|
+
"relevance": relevance,
|
|
147
|
+
"why_relevant": "Reddit RSS",
|
|
148
|
+
"metadata": {},
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
return posts
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _build_urls(query: str, depth: str, subreddits: Optional[List[str]]) -> List[str]:
|
|
155
|
+
"""Build the keyless RSS feed URLs to fan out across."""
|
|
156
|
+
q = quote_plus(query)
|
|
157
|
+
urls: List[str] = [
|
|
158
|
+
f"https://www.reddit.com/search.rss?q={q}&sort=relevance&t=month"
|
|
159
|
+
]
|
|
160
|
+
for raw_sub in (subreddits or []):
|
|
161
|
+
sub = raw_sub.removeprefix("r/").strip()
|
|
162
|
+
if not sub:
|
|
163
|
+
continue
|
|
164
|
+
urls.append(
|
|
165
|
+
f"https://www.reddit.com/r/{sub}/search.rss"
|
|
166
|
+
f"?q={q}&restrict_sr=on&sort=relevance&t=month"
|
|
167
|
+
)
|
|
168
|
+
for sort in LISTING_SORTS.get(depth, LISTING_SORTS["default"]):
|
|
169
|
+
urls.append(f"https://www.reddit.com/r/{sub}/{sort}.rss?t=month")
|
|
170
|
+
return urls
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _fetch_feed(url: str, query: str) -> List[Dict[str, Any]]:
|
|
174
|
+
"""Fetch and parse one feed. Never raises."""
|
|
175
|
+
try:
|
|
176
|
+
text = http.reddit_keyless_get_text(url, timeout=FEED_TIMEOUT, accept="application/atom+xml")
|
|
177
|
+
return _parse_feed(text, query) if text else []
|
|
178
|
+
except Exception as e: # defensive: a single bad feed must not sink the run
|
|
179
|
+
_log(f"feed fetch failed for {url}: {e}")
|
|
180
|
+
return []
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def search_rss(
|
|
184
|
+
query: str,
|
|
185
|
+
depth: str = "default",
|
|
186
|
+
subreddits: Optional[List[str]] = None,
|
|
187
|
+
) -> List[Dict[str, Any]]:
|
|
188
|
+
"""Discover Reddit posts for a query via keyless RSS feeds.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
query: Search query string
|
|
192
|
+
depth: 'quick', 'default', or 'deep' — controls result limit and feeds
|
|
193
|
+
subreddits: Optional pre-resolved subreddit names (without r/) to target
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
List of normalized post dicts (deduped by URL, capped by depth),
|
|
197
|
+
with placeholder scores to be backfilled during enrichment.
|
|
198
|
+
Empty list on any failure.
|
|
199
|
+
"""
|
|
200
|
+
limit = DEPTH_LIMITS.get(depth, DEPTH_LIMITS["default"])
|
|
201
|
+
urls = _build_urls(query, depth, subreddits)
|
|
202
|
+
|
|
203
|
+
all_posts: List[Dict[str, Any]] = []
|
|
204
|
+
workers = min(MAX_WORKERS, len(urls)) or 1
|
|
205
|
+
with ThreadPoolExecutor(max_workers=workers) as executor:
|
|
206
|
+
futures = {executor.submit(_fetch_feed, url, query): url for url in urls}
|
|
207
|
+
for future in futures:
|
|
208
|
+
try:
|
|
209
|
+
all_posts.extend(future.result(timeout=FEED_TIMEOUT + 5))
|
|
210
|
+
except (Exception, FuturesTimeoutError) as e:
|
|
211
|
+
_log(f"feed future failed: {e}")
|
|
212
|
+
|
|
213
|
+
# Dedupe by URL (first occurrence wins).
|
|
214
|
+
seen: set = set()
|
|
215
|
+
unique: List[Dict[str, Any]] = []
|
|
216
|
+
for post in all_posts:
|
|
217
|
+
if post["url"] not in seen:
|
|
218
|
+
seen.add(post["url"])
|
|
219
|
+
unique.append(post)
|
|
220
|
+
|
|
221
|
+
for i, post in enumerate(unique):
|
|
222
|
+
post["id"] = f"R{i + 1}"
|
|
223
|
+
|
|
224
|
+
return unique[:limit]
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Keyless Reddit comment enrichment via shreddit /svc endpoints.
|
|
2
|
+
|
|
3
|
+
Reddit's ``{thread}.json`` endpoint now returns HTTP 403. The shreddit partial
|
|
4
|
+
endpoint ``/svc/shreddit/comments/r/{sub}/t3_{id}`` still serves HTTP 200 HTML
|
|
5
|
+
with no API key, embedding each comment as a ``<shreddit-comment>`` custom
|
|
6
|
+
element whose start-tag attributes carry ``score`` / ``author`` / ``created`` /
|
|
7
|
+
``permalink``, and whose body lives in a ``<div id="{thingId}-post-rtjson-content">``
|
|
8
|
+
block. This module parses that markup into top comments, matching the
|
|
9
|
+
``top_comments`` / ``comment_insights`` shape produced by ``reddit_enrich`` so
|
|
10
|
+
the renderer is unaffected.
|
|
11
|
+
|
|
12
|
+
Limitation: the comments endpoint carries the real comment count
|
|
13
|
+
(``total-comments``) but not the post's upvote score, so post-level ``score``
|
|
14
|
+
cannot be recovered keylessly here (ScrapeCreators backup still provides it).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import html as _html
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from typing import Any, Dict, List, Optional
|
|
22
|
+
|
|
23
|
+
from . import http
|
|
24
|
+
from . import reddit_enrich
|
|
25
|
+
|
|
26
|
+
# Up to N posts enriched per run, by depth (mirrors reddit_public.ENRICH_LIMITS).
|
|
27
|
+
ENRICH_LIMITS = {
|
|
28
|
+
"quick": 3,
|
|
29
|
+
"default": 5,
|
|
30
|
+
"deep": 8,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Max comments returned per post (independent of how many posts get enriched).
|
|
34
|
+
MAX_COMMENTS = 10
|
|
35
|
+
|
|
36
|
+
SVC_TIMEOUT = 12
|
|
37
|
+
|
|
38
|
+
# Match the exact <shreddit-comment> element start tag, not <shreddit-comment-tree>
|
|
39
|
+
# or <shreddit-comment-tree-stats> (lookahead requires whitespace or '>').
|
|
40
|
+
_COMMENT_START = re.compile(r"<shreddit-comment(?=[\s>])[^>]*>")
|
|
41
|
+
_TOTAL_COMMENTS = re.compile(r'total-comments="(\d+)"')
|
|
42
|
+
_PARA = re.compile(r"<p[^>]*>(.*?)</p>", re.S)
|
|
43
|
+
_TAG = re.compile(r"<[^>]+>")
|
|
44
|
+
_WS = re.compile(r"\s+")
|
|
45
|
+
_NEXT_RTJSON = re.compile(r'id="t1_[A-Za-z0-9]+-(?:comment|post)-rtjson-content"')
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _log(msg: str) -> None:
|
|
49
|
+
sys.stderr.write(f"[RedditShreddit] {msg}\n")
|
|
50
|
+
sys.stderr.flush()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def extract_post_ref(url: str) -> Optional[tuple]:
|
|
54
|
+
"""Return (subreddit, post_id) from a Reddit thread URL, or None."""
|
|
55
|
+
m = re.search(r"/r/([^/]+)/comments/([A-Za-z0-9]+)", url or "")
|
|
56
|
+
if not m:
|
|
57
|
+
return None
|
|
58
|
+
return m.group(1), m.group(2)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _svc_url(subreddit: str, post_id: str) -> str:
|
|
62
|
+
# sort=top guarantees Reddit front-loads the highest-scored comments on the
|
|
63
|
+
# first page, so the true top comments are captured even on huge threads
|
|
64
|
+
# (we still re-sort by score locally as a backstop).
|
|
65
|
+
return (
|
|
66
|
+
f"https://www.reddit.com/svc/shreddit/comments/r/{subreddit}/t3_{post_id}"
|
|
67
|
+
f"?sort=top"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _attr(tag: str, name: str) -> str:
|
|
72
|
+
m = re.search(rf'\b{name}="([^"]*)"', tag)
|
|
73
|
+
return _html.unescape(m.group(1)) if m else ""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _iso_to_date(value: str) -> Optional[str]:
|
|
77
|
+
if not value:
|
|
78
|
+
return None
|
|
79
|
+
try:
|
|
80
|
+
return datetime.fromisoformat(value.strip()).date().isoformat()
|
|
81
|
+
except (ValueError, TypeError):
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _body_for(html_text: str, thing_id: str) -> str:
|
|
86
|
+
"""Extract a comment's text body, anchored on its unique thingId.
|
|
87
|
+
|
|
88
|
+
The body div id embeds the comment's thingId, so this assigns body→comment
|
|
89
|
+
correctly even for nested replies. The slice is bounded by the next
|
|
90
|
+
comment's rtjson anchor to avoid swallowing child-comment text.
|
|
91
|
+
"""
|
|
92
|
+
if not thing_id:
|
|
93
|
+
return ""
|
|
94
|
+
anchor = f'id="{thing_id}-post-rtjson-content"'
|
|
95
|
+
idx = html_text.find(anchor)
|
|
96
|
+
if idx == -1:
|
|
97
|
+
return ""
|
|
98
|
+
window = html_text[idx + len(anchor): idx + len(anchor) + 8000]
|
|
99
|
+
nxt = _NEXT_RTJSON.search(window)
|
|
100
|
+
if nxt:
|
|
101
|
+
window = window[: nxt.start()]
|
|
102
|
+
paras = _PARA.findall(window)
|
|
103
|
+
if not paras:
|
|
104
|
+
return ""
|
|
105
|
+
text = " ".join(_TAG.sub("", p) for p in paras)
|
|
106
|
+
return _WS.sub(" ", _html.unescape(text)).strip()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def parse_comments(html_text: str, limit: int = MAX_COMMENTS) -> List[Dict[str, Any]]:
|
|
110
|
+
"""Parse <shreddit-comment> elements into scored comment dicts (sorted desc)."""
|
|
111
|
+
comments: List[Dict[str, Any]] = []
|
|
112
|
+
for m in _COMMENT_START.finditer(html_text or ""):
|
|
113
|
+
tag = m.group(0)
|
|
114
|
+
author = _attr(tag, "author") or "[deleted]"
|
|
115
|
+
if author in ("[deleted]", "[removed]"):
|
|
116
|
+
continue
|
|
117
|
+
thing_id = _attr(tag, "thingId")
|
|
118
|
+
body = _body_for(html_text, thing_id)
|
|
119
|
+
if not body or body in ("[deleted]", "[removed]"):
|
|
120
|
+
continue
|
|
121
|
+
try:
|
|
122
|
+
score = int(_attr(tag, "score") or 0)
|
|
123
|
+
except ValueError:
|
|
124
|
+
score = 0
|
|
125
|
+
permalink = _attr(tag, "permalink")
|
|
126
|
+
comments.append({
|
|
127
|
+
"score": score,
|
|
128
|
+
"author": author,
|
|
129
|
+
"body": body[:300],
|
|
130
|
+
"excerpt": body[:200],
|
|
131
|
+
"permalink": permalink,
|
|
132
|
+
"date": _iso_to_date(_attr(tag, "created")),
|
|
133
|
+
"url": f"https://reddit.com{permalink}" if permalink else "",
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
comments.sort(key=lambda c: c.get("score", 0), reverse=True)
|
|
137
|
+
return comments[:limit]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _total_comments(html_text: str) -> Optional[int]:
|
|
141
|
+
m = _TOTAL_COMMENTS.search(html_text or "")
|
|
142
|
+
return int(m.group(1)) if m else None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def fetch_comments(
|
|
146
|
+
post_url: str,
|
|
147
|
+
timeout: int = SVC_TIMEOUT,
|
|
148
|
+
) -> Dict[str, Any]:
|
|
149
|
+
"""Fetch and parse top comments for a Reddit post via the shreddit endpoint.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
post_url: Reddit thread URL (…/r/{sub}/comments/{id}/…)
|
|
153
|
+
timeout: HTTP timeout in seconds
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Dict with 'top_comments' (list, reddit_enrich shape), 'comment_insights'
|
|
157
|
+
(list[str]), and 'num_comments' (int or None). Empty/None on any
|
|
158
|
+
failure — never raises, so the caller can fall through to SC backup.
|
|
159
|
+
"""
|
|
160
|
+
ref = extract_post_ref(post_url)
|
|
161
|
+
if not ref:
|
|
162
|
+
return {"top_comments": [], "comment_insights": [], "num_comments": None}
|
|
163
|
+
sub, post_id = ref
|
|
164
|
+
|
|
165
|
+
html_text = http.reddit_keyless_get_text(_svc_url(sub, post_id), timeout=timeout, accept="text/html")
|
|
166
|
+
if not html_text:
|
|
167
|
+
return {"top_comments": [], "comment_insights": [], "num_comments": None}
|
|
168
|
+
|
|
169
|
+
comments = parse_comments(html_text, limit=MAX_COMMENTS)
|
|
170
|
+
insights = reddit_enrich.extract_comment_insights(comments)
|
|
171
|
+
return {
|
|
172
|
+
"top_comments": [
|
|
173
|
+
{
|
|
174
|
+
"score": c["score"],
|
|
175
|
+
"date": c["date"],
|
|
176
|
+
"author": c["author"],
|
|
177
|
+
"excerpt": c["excerpt"],
|
|
178
|
+
"url": c["url"],
|
|
179
|
+
}
|
|
180
|
+
for c in comments
|
|
181
|
+
],
|
|
182
|
+
"comment_insights": insights,
|
|
183
|
+
"num_comments": _total_comments(html_text),
|
|
184
|
+
}
|