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,329 @@
|
|
|
1
|
+
"""Keyless Reddit pipeline: free discovery + comment enrichment.
|
|
2
|
+
|
|
3
|
+
``search.json`` is permanently 403/429 keyless, so it is not used. Discovery
|
|
4
|
+
runs on the surfaces that still serve data without a key, then enrichment runs
|
|
5
|
+
on whatever was discovered:
|
|
6
|
+
|
|
7
|
+
Dedicated lane entity-home subreddits (e.g. r/Kanye) pulled in full via the
|
|
8
|
+
shreddit listing partials (top+hot+new, real scores), kept
|
|
9
|
+
whole — floor-exempt — because the sub IS the topic.
|
|
10
|
+
RSS lane reddit_rss breadth (incl. global keyword search) + broad-sub
|
|
11
|
+
listing partials for real upvote scores. Relevance-floored.
|
|
12
|
+
Enrichment shreddit comment + count enrichment (reddit_shreddit) for the
|
|
13
|
+
top-ranked posts (author + score + text + permalink).
|
|
14
|
+
|
|
15
|
+
Returns ``[]`` (never raises) so ``pipeline.py`` can fall through to the
|
|
16
|
+
ScrapeCreators backup when every keyless lane comes up empty.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import concurrent.futures
|
|
20
|
+
import math
|
|
21
|
+
import sys
|
|
22
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
23
|
+
from typing import Any, Dict, List, Optional
|
|
24
|
+
|
|
25
|
+
from collections import Counter
|
|
26
|
+
|
|
27
|
+
from . import reddit_rss, reddit_shreddit, reddit_listing, reddit_arctic
|
|
28
|
+
# Scores are backfilled from popular derived subreddits, so an engagement-first
|
|
29
|
+
# final sort buries on-topic RSS hits under viral off-topic posts. A relevance
|
|
30
|
+
# floor + relevance-first final ranking keeps the section on-topic. Thresholds
|
|
31
|
+
# are shared with the keyed path (reddit.py) via relevance.py.
|
|
32
|
+
from .relevance import RELEVANCE_FLOOR, MIN_ON_TOPIC
|
|
33
|
+
|
|
34
|
+
ENRICH_LIMITS = reddit_shreddit.ENRICH_LIMITS
|
|
35
|
+
ENRICH_BUDGET = 45 # seconds total across all enrichment threads
|
|
36
|
+
MAX_ENRICH_WORKERS = 4
|
|
37
|
+
MAX_DERIVED_SUBS = 5 # subreddits derived from RSS results for score backfill
|
|
38
|
+
# Dedicated subreddits (the entity's home, e.g. r/Kanye for "Kanye West") are
|
|
39
|
+
# wholly on-topic, so pull top+hot+new — the top-of-month listing alone misses
|
|
40
|
+
# fresh threads — and keep every item (floor-exempt).
|
|
41
|
+
DEDICATED_SORTS = ["top", "hot", "new"]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _relevance_rank_key(post: Dict[str, Any]) -> float:
|
|
45
|
+
"""Rank by relevance first, with a bounded engagement bonus as tiebreaker.
|
|
46
|
+
|
|
47
|
+
Mirrors reddit.py: the log-scaled bonus (capped at 0.25) orders
|
|
48
|
+
similarly-relevant posts by discussion volume but is too small to lift an
|
|
49
|
+
off-topic post (relevance ~0) above an on-topic one.
|
|
50
|
+
"""
|
|
51
|
+
eng = post.get("engagement", {})
|
|
52
|
+
total = (eng.get("score", 0) or 0) + (eng.get("num_comments", 0) or 0)
|
|
53
|
+
return (post.get("relevance") or 0.0) + min(0.25, math.log10(total + 1) / 20.0)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _log(msg: str) -> None:
|
|
57
|
+
sys.stderr.write(f"[RedditKeyless] {msg}\n")
|
|
58
|
+
sys.stderr.flush()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _top_subreddits(posts: List[Dict[str, Any]], limit: int = MAX_DERIVED_SUBS) -> List[str]:
|
|
62
|
+
"""Most frequent subreddits across discovered posts (for score backfill)."""
|
|
63
|
+
counts = Counter(p.get("subreddit", "") for p in posts if p.get("subreddit"))
|
|
64
|
+
return [sub for sub, _ in counts.most_common(limit)]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _apply_scores(post: Dict[str, Any], scored: Dict[str, int]) -> None:
|
|
68
|
+
post["score"] = scored["score"]
|
|
69
|
+
post["num_comments"] = scored["num_comments"]
|
|
70
|
+
post.setdefault("engagement", {})["score"] = scored["score"]
|
|
71
|
+
post["engagement"]["num_comments"] = scored["num_comments"]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _discover(
|
|
75
|
+
topic: str,
|
|
76
|
+
depth: str,
|
|
77
|
+
subreddits: Optional[List[str]],
|
|
78
|
+
dedicated_subreddits: Optional[List[str]] = None,
|
|
79
|
+
) -> List[Dict[str, Any]]:
|
|
80
|
+
# Dedicated lane: the entity's home subs are wholly on-topic. Pull
|
|
81
|
+
# top+hot+new (real scores from the listing) and mark them floor-exempt so
|
|
82
|
+
# an on-topic post whose title lacks the entity name is never dropped.
|
|
83
|
+
dedicated_posts: List[Dict[str, Any]] = []
|
|
84
|
+
if dedicated_subreddits:
|
|
85
|
+
dedicated_posts = reddit_listing.fetch_listings(
|
|
86
|
+
dedicated_subreddits, depth=depth, query=topic, sorts=DEDICATED_SORTS
|
|
87
|
+
)
|
|
88
|
+
for p in dedicated_posts:
|
|
89
|
+
p["dedicated"] = True
|
|
90
|
+
_log(f"Dedicated lane: {len(dedicated_posts)} posts from {dedicated_subreddits}")
|
|
91
|
+
|
|
92
|
+
# search.json is permanently 403/429 keyless (no Tier 0). Discovery is RSS
|
|
93
|
+
# breadth (incl. global keyword search) + broad-sub listing partials for
|
|
94
|
+
# real upvote scores.
|
|
95
|
+
rss_posts = reddit_rss.search_rss(topic, depth=depth, subreddits=subreddits)
|
|
96
|
+
|
|
97
|
+
if subreddits:
|
|
98
|
+
# Targeted run: the caller chose these subreddits, so their listing cards
|
|
99
|
+
# are on-topic — include them as scored discovery AND as a score source.
|
|
100
|
+
listing_posts = reddit_listing.fetch_listings(subreddits, depth=depth, query=topic)
|
|
101
|
+
score_source = listing_posts
|
|
102
|
+
else:
|
|
103
|
+
# Bare global run: subreddits derived from noisy RSS results are NOT
|
|
104
|
+
# reliably on-topic, so their listings are used ONLY to backfill scores
|
|
105
|
+
# onto the keyword-matched RSS posts — never merged as discovery, which
|
|
106
|
+
# would flood results with high-upvote but irrelevant posts.
|
|
107
|
+
listing_posts = []
|
|
108
|
+
derived = _top_subreddits(rss_posts)
|
|
109
|
+
score_source = reddit_listing.fetch_listings(derived, depth=depth, query=topic)
|
|
110
|
+
_log(
|
|
111
|
+
f"Tier 1 (RSS) {len(rss_posts)} posts; "
|
|
112
|
+
f"{'listing discovery ' + str(len(listing_posts)) if subreddits else 'score-only'}; "
|
|
113
|
+
f"{len(score_source)} scored cards"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Score lookup by post id, from the scored listing cards.
|
|
117
|
+
score_map: Dict[str, Dict[str, int]] = {}
|
|
118
|
+
for p in score_source:
|
|
119
|
+
pid = p.get("metadata", {}).get("post_id", "")
|
|
120
|
+
if pid:
|
|
121
|
+
score_map[pid] = {"score": p["score"], "num_comments": p["num_comments"]}
|
|
122
|
+
|
|
123
|
+
# Merge: dedicated-sub posts first (floor-exempt), then scored broad listing
|
|
124
|
+
# posts (targeted only), then RSS breadth backfilled with real scores where
|
|
125
|
+
# the post appears in a listing. First writer wins the dedupe, so a thread
|
|
126
|
+
# in both the dedicated lane and a listing keeps its floor-exempt status.
|
|
127
|
+
merged: List[Dict[str, Any]] = []
|
|
128
|
+
seen: set = set()
|
|
129
|
+
for p in dedicated_posts + listing_posts:
|
|
130
|
+
if p["url"] not in seen:
|
|
131
|
+
seen.add(p["url"])
|
|
132
|
+
merged.append(p)
|
|
133
|
+
for p in rss_posts:
|
|
134
|
+
if p["url"] in seen:
|
|
135
|
+
continue
|
|
136
|
+
pid = reddit_listing._post_id(p["url"])
|
|
137
|
+
if pid in score_map:
|
|
138
|
+
_apply_scores(p, score_map[pid])
|
|
139
|
+
seen.add(p["url"])
|
|
140
|
+
merged.append(p)
|
|
141
|
+
|
|
142
|
+
# Backfill scores for RSS-only posts (no listing card scored them) from the
|
|
143
|
+
# free arctic-shift archive. Posts already scored by a listing keep that
|
|
144
|
+
# live score; arctic only fills the gap, and is best-effort (never raises).
|
|
145
|
+
need = [pid for p in merged
|
|
146
|
+
if not (p.get("engagement", {}).get("score"))
|
|
147
|
+
for pid in [reddit_listing._post_id(p["url"])] if pid]
|
|
148
|
+
if need:
|
|
149
|
+
scores = reddit_arctic.fetch_scores(need)
|
|
150
|
+
filled = 0
|
|
151
|
+
for p in merged:
|
|
152
|
+
if p.get("engagement", {}).get("score"):
|
|
153
|
+
continue
|
|
154
|
+
pid = reddit_listing._post_id(p["url"])
|
|
155
|
+
if pid in scores:
|
|
156
|
+
_apply_scores(p, scores[pid])
|
|
157
|
+
filled += 1
|
|
158
|
+
if filled:
|
|
159
|
+
_log(f"arctic-shift backfilled {filled} post scores")
|
|
160
|
+
return merged
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _enrich_one(post: Dict[str, Any]) -> Dict[str, Any]:
|
|
164
|
+
"""Attach shreddit comments + real comment count. Never raises."""
|
|
165
|
+
try:
|
|
166
|
+
data = reddit_shreddit.fetch_comments(post.get("url", ""))
|
|
167
|
+
if data.get("top_comments"):
|
|
168
|
+
post["top_comments"] = data["top_comments"]
|
|
169
|
+
if data.get("comment_insights"):
|
|
170
|
+
post["comment_insights"] = data["comment_insights"]
|
|
171
|
+
num = data.get("num_comments")
|
|
172
|
+
if num is not None:
|
|
173
|
+
post["num_comments"] = num
|
|
174
|
+
post.setdefault("engagement", {})["num_comments"] = num
|
|
175
|
+
except Exception:
|
|
176
|
+
pass # keep the post with whatever discovery gave us
|
|
177
|
+
return post
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _enrich(posts: List[Dict[str, Any]], depth: str) -> List[Dict[str, Any]]:
|
|
181
|
+
"""Enrich the top N posts with comments under a total time budget."""
|
|
182
|
+
limit = ENRICH_LIMITS.get(depth, ENRICH_LIMITS["default"])
|
|
183
|
+
to_enrich = posts[:limit]
|
|
184
|
+
rest = posts[limit:]
|
|
185
|
+
if not to_enrich:
|
|
186
|
+
return posts
|
|
187
|
+
|
|
188
|
+
result_map: Dict[int, Dict[str, Any]] = {}
|
|
189
|
+
try:
|
|
190
|
+
with ThreadPoolExecutor(max_workers=min(limit, MAX_ENRICH_WORKERS)) as executor:
|
|
191
|
+
futures = {
|
|
192
|
+
executor.submit(_enrich_one, post): i
|
|
193
|
+
for i, post in enumerate(to_enrich)
|
|
194
|
+
}
|
|
195
|
+
done, not_done = concurrent.futures.wait(futures, timeout=ENRICH_BUDGET)
|
|
196
|
+
for future in done:
|
|
197
|
+
idx = futures[future]
|
|
198
|
+
try:
|
|
199
|
+
result_map[idx] = future.result(timeout=0)
|
|
200
|
+
except Exception:
|
|
201
|
+
result_map[idx] = to_enrich[idx]
|
|
202
|
+
for future in not_done:
|
|
203
|
+
idx = futures[future]
|
|
204
|
+
result_map[idx] = to_enrich[idx]
|
|
205
|
+
future.cancel()
|
|
206
|
+
enriched = [result_map[i] for i in range(len(to_enrich))]
|
|
207
|
+
except Exception:
|
|
208
|
+
enriched = to_enrich
|
|
209
|
+
|
|
210
|
+
return enriched + rest
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _slot_priority(topic: str, posts: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
214
|
+
"""Order posts for enrichment slots: entity-matching posts first.
|
|
215
|
+
|
|
216
|
+
Comment slots (ENRICH_LIMITS) are scarce; spending them on high-upvote
|
|
217
|
+
posts that rerank later demotes as entity misses starves the on-topic
|
|
218
|
+
posts the user actually sees (2026-06-06 "OpenClaw vs Hermes" run:
|
|
219
|
+
2,000+ upvote Gemma/GPU threads took every slot, then were demoted to
|
|
220
|
+
zero). Mirror rerank's demotion signal via the shared `_entity_grounded`
|
|
221
|
+
check (head token of the topic's stripped primary entity present in the
|
|
222
|
+
post text) so slots go to posts likely to survive final ranking — keying
|
|
223
|
+
on the same head token keeps the two paths from diverging. Falls back to
|
|
224
|
+
token-overlap relevance when the topic yields no usable primary entity.
|
|
225
|
+
Within each tier the incoming
|
|
226
|
+
(score-first) order is preserved. Never raises; on any failure the
|
|
227
|
+
incoming order is returned unchanged.
|
|
228
|
+
"""
|
|
229
|
+
try:
|
|
230
|
+
from . import relevance, rerank
|
|
231
|
+
|
|
232
|
+
def _post_text(post: Dict[str, Any]) -> str:
|
|
233
|
+
return f"{post.get('title') or ''} {post.get('selftext') or ''}"
|
|
234
|
+
|
|
235
|
+
entity = rerank._primary_entity(topic).lower()
|
|
236
|
+
if entity:
|
|
237
|
+
def _matches(post: Dict[str, Any]) -> bool:
|
|
238
|
+
return rerank._entity_grounded(_post_text(post), entity)
|
|
239
|
+
else:
|
|
240
|
+
prepared = relevance.PreparedQuery(topic)
|
|
241
|
+
|
|
242
|
+
def _matches(post: Dict[str, Any]) -> bool:
|
|
243
|
+
return relevance.token_overlap_relevance(prepared, _post_text(post)) > 0.24
|
|
244
|
+
|
|
245
|
+
matches: List[Dict[str, Any]] = []
|
|
246
|
+
misses: List[Dict[str, Any]] = []
|
|
247
|
+
for post in posts:
|
|
248
|
+
(matches if _matches(post) else misses).append(post)
|
|
249
|
+
return matches + misses
|
|
250
|
+
except Exception:
|
|
251
|
+
return posts
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def search_and_enrich(
|
|
255
|
+
topic: str,
|
|
256
|
+
from_date: str,
|
|
257
|
+
to_date: str,
|
|
258
|
+
depth: str = "default",
|
|
259
|
+
subreddits: Optional[List[str]] = None,
|
|
260
|
+
dedicated_subreddits: Optional[List[str]] = None,
|
|
261
|
+
) -> List[Dict[str, Any]]:
|
|
262
|
+
"""Full keyless Reddit pipeline: discover then enrich.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
topic: Search topic
|
|
266
|
+
from_date: Start date (YYYY-MM-DD)
|
|
267
|
+
to_date: End date (YYYY-MM-DD)
|
|
268
|
+
depth: 'quick', 'default', or 'deep'
|
|
269
|
+
subreddits: Optional pre-resolved broad/category subreddit names (no r/)
|
|
270
|
+
dedicated_subreddits: Optional entity-home subreddit names (no r/) pulled
|
|
271
|
+
in full (top+hot+new) and exempt from the relevance floor.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
List of normalized item dicts matching the reddit_public output shape,
|
|
275
|
+
with top_comments/comment_insights attached on enriched posts.
|
|
276
|
+
Empty list when all keyless tiers fail (so SC backup can engage).
|
|
277
|
+
"""
|
|
278
|
+
posts = _discover(topic, depth, subreddits, dedicated_subreddits)
|
|
279
|
+
if not posts:
|
|
280
|
+
return []
|
|
281
|
+
|
|
282
|
+
# Date filter: keep posts in range or with unknown dates (mirrors reddit_public).
|
|
283
|
+
posts = [
|
|
284
|
+
p for p in posts
|
|
285
|
+
if p.get("date") is None or (from_date <= p["date"] <= to_date)
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
# Relevance floor: strip zero-overlap posts (relevance exactly 0 = no
|
|
289
|
+
# title/body token match at all) when anything relevant remains, so
|
|
290
|
+
# backfilled high-upvote posts from popular subs can't bury on-topic RSS
|
|
291
|
+
# hits. Keep all only when nothing scored above zero.
|
|
292
|
+
before = len(posts)
|
|
293
|
+
# Dedicated-sub posts are floor-exempt: their whole subreddit is the topic,
|
|
294
|
+
# so an on-topic post whose title lacks the entity name must not be dropped.
|
|
295
|
+
on_topic = [p for p in posts if p.get("dedicated") or (p.get("relevance") or 0) >= RELEVANCE_FLOOR]
|
|
296
|
+
if len(on_topic) >= MIN_ON_TOPIC:
|
|
297
|
+
posts = on_topic
|
|
298
|
+
else:
|
|
299
|
+
nonzero = [p for p in posts if p.get("dedicated") or (p.get("relevance") or 0) > 0]
|
|
300
|
+
if nonzero:
|
|
301
|
+
posts = nonzero
|
|
302
|
+
if len(posts) < before:
|
|
303
|
+
_log(f"Relevance floor dropped {before - len(posts)} off-topic posts")
|
|
304
|
+
|
|
305
|
+
# Provisional score-first order so enrichment-slot selection has a stable
|
|
306
|
+
# within-tier order to preserve.
|
|
307
|
+
posts.sort(
|
|
308
|
+
key=lambda p: (
|
|
309
|
+
p.get("engagement", {}).get("score", 0) or 0,
|
|
310
|
+
p.get("relevance", 0) or 0,
|
|
311
|
+
p.get("date") or "",
|
|
312
|
+
),
|
|
313
|
+
reverse=True,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Enrichment slot selection is relevance-aware: entity-matching posts
|
|
317
|
+
# claim the scarce comment slots first (score order preserved within
|
|
318
|
+
# each tier).
|
|
319
|
+
posts = _enrich(_slot_priority(topic, posts), depth)
|
|
320
|
+
|
|
321
|
+
# Final display order ranks relevance-first with a bounded engagement bonus,
|
|
322
|
+
# so an off-topic high-upvote post can't outrank an on-topic one in what the
|
|
323
|
+
# user sees. Enrichment above may have backfilled real comment counts.
|
|
324
|
+
posts.sort(key=_relevance_rank_key, reverse=True)
|
|
325
|
+
|
|
326
|
+
for i, post in enumerate(posts):
|
|
327
|
+
post["id"] = f"R{i + 1}"
|
|
328
|
+
|
|
329
|
+
return posts
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Keyless Reddit listing scrape via shreddit /svc partials — with real scores.
|
|
2
|
+
|
|
3
|
+
The subreddit listing partial
|
|
4
|
+
``/svc/shreddit/community-more-posts/{sort}/?name={sub}[&t={range}]`` serves
|
|
5
|
+
HTTP 200 with no API key and **server-renders each post's upvote score**, which
|
|
6
|
+
neither RSS nor the comments endpoint provides. Each post is a
|
|
7
|
+
``<shreddit-post>`` element whose start-tag attributes carry ``score``,
|
|
8
|
+
``comment-count``, ``post-title``, ``permalink``, ``author``, ``subreddit-name``
|
|
9
|
+
and ``created-timestamp``.
|
|
10
|
+
|
|
11
|
+
This is the keyless source of post-level upvotes. It works for normal users on
|
|
12
|
+
ordinary connections (verified), so reddit_keyless uses it both as a scored
|
|
13
|
+
discovery source and to backfill scores onto RSS-discovered posts.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import html as _html
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeoutError
|
|
21
|
+
from typing import Any, Dict, List, Optional
|
|
22
|
+
|
|
23
|
+
from . import http
|
|
24
|
+
from .relevance import token_overlap_relevance
|
|
25
|
+
|
|
26
|
+
# Listing sorts pulled per subreddit, by depth.
|
|
27
|
+
LISTING_SORTS = {
|
|
28
|
+
"quick": ["top"],
|
|
29
|
+
"default": ["top", "hot"],
|
|
30
|
+
"deep": ["top", "hot", "new"],
|
|
31
|
+
}
|
|
32
|
+
DEPTH_LIMITS = {"quick": 10, "default": 25, "deep": 50}
|
|
33
|
+
TIMEFRAME = "month"
|
|
34
|
+
MAX_WORKERS = 4
|
|
35
|
+
LISTING_TIMEOUT = 15
|
|
36
|
+
|
|
37
|
+
_POST_CARD = re.compile(r"<shreddit-post(?=[\s>])[^>]*>")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _log(msg: str) -> None:
|
|
41
|
+
sys.stderr.write(f"[RedditListing] {msg}\n")
|
|
42
|
+
sys.stderr.flush()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _attr(tag: str, name: str) -> Optional[str]:
|
|
46
|
+
m = re.search(rf'\b{name}="([^"]*)"', tag)
|
|
47
|
+
return _html.unescape(m.group(1)) if m else None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _to_date(value: Optional[str]) -> Optional[str]:
|
|
51
|
+
if not value:
|
|
52
|
+
return None
|
|
53
|
+
try:
|
|
54
|
+
return datetime.fromisoformat(value.strip()).date().isoformat()
|
|
55
|
+
except (ValueError, TypeError):
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _to_epoch(value: Optional[str]) -> Optional[float]:
|
|
60
|
+
if not value:
|
|
61
|
+
return None
|
|
62
|
+
try:
|
|
63
|
+
dt = datetime.fromisoformat(value.strip())
|
|
64
|
+
if dt.tzinfo is None:
|
|
65
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
66
|
+
return dt.timestamp()
|
|
67
|
+
except (ValueError, TypeError):
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _post_id(permalink: str) -> str:
|
|
72
|
+
m = re.search(r"/comments/([A-Za-z0-9]+)", permalink or "")
|
|
73
|
+
return m.group(1) if m else ""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def parse_cards(html_text: str, query: str = "") -> List[Dict[str, Any]]:
|
|
77
|
+
"""Parse <shreddit-post> cards into normalized post dicts with real scores."""
|
|
78
|
+
posts: List[Dict[str, Any]] = []
|
|
79
|
+
for m in _POST_CARD.finditer(html_text or ""):
|
|
80
|
+
tag = m.group(0)
|
|
81
|
+
permalink = _attr(tag, "permalink") or ""
|
|
82
|
+
if "/comments/" not in permalink:
|
|
83
|
+
continue
|
|
84
|
+
try:
|
|
85
|
+
score = int(_attr(tag, "score") or 0)
|
|
86
|
+
except ValueError:
|
|
87
|
+
score = 0
|
|
88
|
+
try:
|
|
89
|
+
num_comments = int(_attr(tag, "comment-count") or 0)
|
|
90
|
+
except ValueError:
|
|
91
|
+
num_comments = 0
|
|
92
|
+
title = _attr(tag, "post-title") or ""
|
|
93
|
+
author = _attr(tag, "author") or "[deleted]"
|
|
94
|
+
subreddit = _attr(tag, "subreddit-name") or ""
|
|
95
|
+
created = _attr(tag, "created-timestamp")
|
|
96
|
+
url = f"https://www.reddit.com{permalink}"
|
|
97
|
+
|
|
98
|
+
posts.append({
|
|
99
|
+
"id": "",
|
|
100
|
+
"title": title,
|
|
101
|
+
"url": url,
|
|
102
|
+
"score": score,
|
|
103
|
+
"num_comments": num_comments,
|
|
104
|
+
"subreddit": subreddit,
|
|
105
|
+
"created_utc": _to_epoch(created),
|
|
106
|
+
"author": author if author not in ("[deleted]", "[removed]") else "[deleted]",
|
|
107
|
+
"selftext": "",
|
|
108
|
+
"date": _to_date(created),
|
|
109
|
+
"engagement": {
|
|
110
|
+
"score": score,
|
|
111
|
+
"num_comments": num_comments,
|
|
112
|
+
"upvote_ratio": None,
|
|
113
|
+
},
|
|
114
|
+
"relevance": round(token_overlap_relevance(query, title), 3) if query else 0.0,
|
|
115
|
+
"why_relevant": "Reddit listing",
|
|
116
|
+
"metadata": {"post_id": _post_id(permalink)},
|
|
117
|
+
})
|
|
118
|
+
return posts
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _listing_url(subreddit: str, sort: str) -> str:
|
|
122
|
+
sub = subreddit.removeprefix("r/").strip()
|
|
123
|
+
url = f"https://www.reddit.com/svc/shreddit/community-more-posts/{sort}/?name={sub}"
|
|
124
|
+
if sort == "top":
|
|
125
|
+
url += f"&t={TIMEFRAME}"
|
|
126
|
+
return url
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _fetch_one(subreddit: str, sort: str, query: str) -> List[Dict[str, Any]]:
|
|
130
|
+
try:
|
|
131
|
+
text = http.reddit_keyless_get_text(_listing_url(subreddit, sort), timeout=LISTING_TIMEOUT,
|
|
132
|
+
accept="text/html")
|
|
133
|
+
return parse_cards(text, query) if text else []
|
|
134
|
+
except Exception as e:
|
|
135
|
+
_log(f"listing fetch failed r/{subreddit} {sort}: {e}")
|
|
136
|
+
return []
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def fetch_listings(
|
|
140
|
+
subreddits: List[str],
|
|
141
|
+
depth: str = "default",
|
|
142
|
+
query: str = "",
|
|
143
|
+
sorts: Optional[List[str]] = None,
|
|
144
|
+
) -> List[Dict[str, Any]]:
|
|
145
|
+
"""Fetch scored post cards across subreddits × sorts.
|
|
146
|
+
|
|
147
|
+
Returns deduped normalized posts (with real scores), unranked/unsliced —
|
|
148
|
+
the caller merges these with other sources, ranks, and slices.
|
|
149
|
+
|
|
150
|
+
``sorts`` overrides the depth-derived sort set. Dedicated-subreddit lanes
|
|
151
|
+
pass ``["top", "hot", "new"]`` so fresh threads (which the top-of-month
|
|
152
|
+
listing misses) are caught with their scores regardless of depth.
|
|
153
|
+
"""
|
|
154
|
+
if not subreddits:
|
|
155
|
+
return []
|
|
156
|
+
sorts = sorts or LISTING_SORTS.get(depth, LISTING_SORTS["default"])
|
|
157
|
+
jobs = [(sub, sort) for sub in subreddits for sort in sorts]
|
|
158
|
+
all_posts: List[Dict[str, Any]] = []
|
|
159
|
+
with ThreadPoolExecutor(max_workers=min(MAX_WORKERS, len(jobs)) or 1) as executor:
|
|
160
|
+
futures = {executor.submit(_fetch_one, sub, sort, query): (sub, sort)
|
|
161
|
+
for sub, sort in jobs}
|
|
162
|
+
for future in futures:
|
|
163
|
+
try:
|
|
164
|
+
all_posts.extend(future.result(timeout=LISTING_TIMEOUT + 5))
|
|
165
|
+
except (Exception, FuturesTimeoutError) as e:
|
|
166
|
+
_log(f"listing future failed: {e}")
|
|
167
|
+
|
|
168
|
+
seen: set = set()
|
|
169
|
+
unique: List[Dict[str, Any]] = []
|
|
170
|
+
for p in all_posts:
|
|
171
|
+
if p["url"] not in seen:
|
|
172
|
+
seen.add(p["url"])
|
|
173
|
+
unique.append(p)
|
|
174
|
+
return unique
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def score_index(subreddits: List[str], depth: str = "default") -> Dict[str, Dict[str, int]]:
|
|
178
|
+
"""Build a {post_id: {score, num_comments}} map from subreddit listings.
|
|
179
|
+
|
|
180
|
+
Used to backfill real scores onto posts discovered via RSS, which carries
|
|
181
|
+
no engagement numbers.
|
|
182
|
+
"""
|
|
183
|
+
index: Dict[str, Dict[str, int]] = {}
|
|
184
|
+
for p in fetch_listings(subreddits, depth=depth):
|
|
185
|
+
pid = p.get("metadata", {}).get("post_id") or _post_id(p["url"])
|
|
186
|
+
if pid:
|
|
187
|
+
index[pid] = {"score": p["score"], "num_comments": p["num_comments"]}
|
|
188
|
+
return index
|