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,235 @@
|
|
|
1
|
+
"""Caption-free transcription: compress -> chunk -> provider-fallback.
|
|
2
|
+
|
|
3
|
+
When a video/audio item has no captions, this turns the media into text via a
|
|
4
|
+
Whisper API. Source-agnostic: the input is a media URL or local path, the output
|
|
5
|
+
is transcript text. The pipeline:
|
|
6
|
+
|
|
7
|
+
1. Acquire audio (yt-dlp for a URL, or use a local file as-is).
|
|
8
|
+
2. Re-encode to a low-bitrate mono stream so most clips fit under the provider
|
|
9
|
+
upload limit.
|
|
10
|
+
3. If still over the limit, split into bounded-duration chunks.
|
|
11
|
+
4. Transcribe each chunk through an ordered provider list (Groq free tier
|
|
12
|
+
first, OpenAI paid backstop), with per-chunk fallback, then join.
|
|
13
|
+
|
|
14
|
+
Never raises. Returns a typed :class:`TranscriptResult`; missing prerequisites
|
|
15
|
+
(no ffmpeg, no provider key) yield a degraded result with a reason, consumed by
|
|
16
|
+
the source-health layer, rather than a crash.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import shutil
|
|
23
|
+
import tempfile
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from typing import Optional
|
|
26
|
+
|
|
27
|
+
from . import env, health, subproc
|
|
28
|
+
|
|
29
|
+
# Whisper's documented upload ceiling. We compress to stay under it and chunk
|
|
30
|
+
# when a single clip still exceeds it.
|
|
31
|
+
MAX_UPLOAD_BYTES = 25 * 1024 * 1024
|
|
32
|
+
CHUNK_SECONDS = 600 # 10-minute chunks when splitting is required
|
|
33
|
+
|
|
34
|
+
_PROVIDER_ENDPOINTS = {
|
|
35
|
+
"groq": "https://api.groq.com/openai/v1/audio/transcriptions",
|
|
36
|
+
"openai": "https://api.openai.com/v1/audio/transcriptions",
|
|
37
|
+
}
|
|
38
|
+
_PROVIDER_MODELS = {
|
|
39
|
+
"groq": "whisper-large-v3",
|
|
40
|
+
"openai": "whisper-1",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class TranscriptResult:
|
|
46
|
+
text: str = ""
|
|
47
|
+
ok: bool = False
|
|
48
|
+
reason: str = ""
|
|
49
|
+
provider: str = ""
|
|
50
|
+
chunks: int = 0
|
|
51
|
+
health: Optional[health.SourceHealth] = field(default=None)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def is_available(config: dict) -> bool:
|
|
55
|
+
"""True when ffmpeg is present AND at least one Whisper provider key is set."""
|
|
56
|
+
return bool(shutil.which("ffmpeg")) and bool(env.transcription_providers(config))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def transcribe_media(
|
|
60
|
+
source: str,
|
|
61
|
+
config: dict,
|
|
62
|
+
timeout: float = 120.0,
|
|
63
|
+
) -> TranscriptResult:
|
|
64
|
+
"""Transcribe a media URL or local path. Never raises.
|
|
65
|
+
|
|
66
|
+
Returns a degraded result (ok=False, reason set) when prerequisites are
|
|
67
|
+
missing or every provider fails, so callers can report the gap honestly.
|
|
68
|
+
"""
|
|
69
|
+
if not shutil.which("ffmpeg"):
|
|
70
|
+
return _degraded("ffmpeg not installed", health.MISSING)
|
|
71
|
+
providers = env.transcription_providers(config)
|
|
72
|
+
if not providers:
|
|
73
|
+
return _degraded(
|
|
74
|
+
"no transcription provider key (set GROQ_API_KEY or OPENAI_API_KEY)",
|
|
75
|
+
health.MISSING,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
workdir = tempfile.mkdtemp(prefix="l30d-transcribe-")
|
|
79
|
+
try:
|
|
80
|
+
audio_path = _acquire_audio(source, workdir, timeout=timeout)
|
|
81
|
+
if not audio_path:
|
|
82
|
+
return _degraded("could not acquire/compress audio", health.ERROR)
|
|
83
|
+
|
|
84
|
+
chunk_paths = _chunk_audio(audio_path, workdir)
|
|
85
|
+
if not chunk_paths:
|
|
86
|
+
return _degraded("audio chunking produced no segments", health.ERROR)
|
|
87
|
+
|
|
88
|
+
texts: list[str] = []
|
|
89
|
+
used_provider = ""
|
|
90
|
+
for chunk in chunk_paths:
|
|
91
|
+
chunk_text, provider = _transcribe_chunk(chunk, providers, timeout=timeout)
|
|
92
|
+
if chunk_text is None:
|
|
93
|
+
return _degraded(
|
|
94
|
+
f"all providers failed on a chunk ({len(texts)}/{len(chunk_paths)} done)",
|
|
95
|
+
health.ERROR,
|
|
96
|
+
)
|
|
97
|
+
texts.append(chunk_text)
|
|
98
|
+
used_provider = provider or used_provider
|
|
99
|
+
|
|
100
|
+
joined = "\n".join(t.strip() for t in texts if t.strip())
|
|
101
|
+
if not joined:
|
|
102
|
+
return _degraded("transcription produced empty text", health.DEGRADED)
|
|
103
|
+
return TranscriptResult(
|
|
104
|
+
text=joined,
|
|
105
|
+
ok=True,
|
|
106
|
+
provider=used_provider,
|
|
107
|
+
chunks=len(chunk_paths),
|
|
108
|
+
health=health.SourceHealth(name="transcribe", state=health.OK),
|
|
109
|
+
)
|
|
110
|
+
finally:
|
|
111
|
+
shutil.rmtree(workdir, ignore_errors=True)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _degraded(reason: str, state: str) -> TranscriptResult:
|
|
115
|
+
return TranscriptResult(
|
|
116
|
+
ok=False,
|
|
117
|
+
reason=reason,
|
|
118
|
+
health=health.SourceHealth(name="transcribe", state=state, reason=reason),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _acquire_audio(source: str, workdir: str, timeout: float) -> Optional[str]:
|
|
123
|
+
"""Produce a compressed mono/16kHz/low-bitrate audio file, or None.
|
|
124
|
+
|
|
125
|
+
For a URL, extract audio with yt-dlp; for a local path, transcode it. The
|
|
126
|
+
re-encode keeps most clips under the upload ceiling.
|
|
127
|
+
"""
|
|
128
|
+
raw = source
|
|
129
|
+
if source.startswith("http"):
|
|
130
|
+
raw = os.path.join(workdir, "raw.m4a")
|
|
131
|
+
if not _run([
|
|
132
|
+
"yt-dlp", "-f", "bestaudio", "-o", raw, "--no-playlist", source
|
|
133
|
+
], timeout=timeout):
|
|
134
|
+
return None
|
|
135
|
+
if not os.path.exists(raw):
|
|
136
|
+
return None
|
|
137
|
+
elif not os.path.exists(source):
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
out = os.path.join(workdir, "audio.mp3")
|
|
141
|
+
# Mono, 16kHz, 32kbps keeps speech intelligible while shrinking the file.
|
|
142
|
+
if not _run([
|
|
143
|
+
"ffmpeg", "-y", "-i", raw, "-ac", "1", "-ar", "16000", "-b:a", "32k", out
|
|
144
|
+
], timeout=timeout):
|
|
145
|
+
return None
|
|
146
|
+
return out if os.path.exists(out) else None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _chunk_audio(audio_path: str, workdir: str) -> list[str]:
|
|
150
|
+
"""Return [audio_path] when small enough, else ffmpeg-segmented chunk paths."""
|
|
151
|
+
try:
|
|
152
|
+
size = os.path.getsize(audio_path)
|
|
153
|
+
except OSError:
|
|
154
|
+
return []
|
|
155
|
+
if size <= MAX_UPLOAD_BYTES:
|
|
156
|
+
return [audio_path]
|
|
157
|
+
|
|
158
|
+
pattern = os.path.join(workdir, "chunk_%03d.mp3")
|
|
159
|
+
if not _run([
|
|
160
|
+
"ffmpeg", "-y", "-i", audio_path, "-f", "segment",
|
|
161
|
+
"-segment_time", str(CHUNK_SECONDS), "-c", "copy", pattern
|
|
162
|
+
]):
|
|
163
|
+
# Fall back to the single (oversized) file; the provider may still accept it.
|
|
164
|
+
return [audio_path]
|
|
165
|
+
chunks = sorted(
|
|
166
|
+
os.path.join(workdir, f) for f in os.listdir(workdir) if f.startswith("chunk_")
|
|
167
|
+
)
|
|
168
|
+
return chunks or [audio_path]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _transcribe_chunk(
|
|
172
|
+
path: str,
|
|
173
|
+
providers: list[tuple[str, str]],
|
|
174
|
+
timeout: float,
|
|
175
|
+
) -> tuple[Optional[str], str]:
|
|
176
|
+
"""Try each provider in order; return (text, provider) or (None, '')."""
|
|
177
|
+
for name, key in providers:
|
|
178
|
+
try:
|
|
179
|
+
text = _post_audio(name, path, key, timeout=timeout)
|
|
180
|
+
except Exception: # noqa: BLE001 - any provider failure -> try the next
|
|
181
|
+
text = None
|
|
182
|
+
if text is not None:
|
|
183
|
+
return text, name
|
|
184
|
+
return None, ""
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _post_audio(provider: str, path: str, api_key: str, timeout: float) -> Optional[str]:
|
|
188
|
+
"""POST one audio file to a Whisper-compatible endpoint; return text or None."""
|
|
189
|
+
import json
|
|
190
|
+
import urllib.request
|
|
191
|
+
|
|
192
|
+
endpoint = _PROVIDER_ENDPOINTS[provider]
|
|
193
|
+
model = _PROVIDER_MODELS[provider]
|
|
194
|
+
boundary = "----l30dTranscribeBoundary"
|
|
195
|
+
with open(path, "rb") as fh:
|
|
196
|
+
audio = fh.read()
|
|
197
|
+
|
|
198
|
+
parts: list[bytes] = []
|
|
199
|
+
parts.append(f"--{boundary}\r\n".encode())
|
|
200
|
+
parts.append(b'Content-Disposition: form-data; name="model"\r\n\r\n')
|
|
201
|
+
parts.append(f"{model}\r\n".encode())
|
|
202
|
+
parts.append(f"--{boundary}\r\n".encode())
|
|
203
|
+
parts.append(
|
|
204
|
+
b'Content-Disposition: form-data; name="file"; filename="audio.mp3"\r\n'
|
|
205
|
+
b"Content-Type: audio/mpeg\r\n\r\n"
|
|
206
|
+
)
|
|
207
|
+
parts.append(audio)
|
|
208
|
+
parts.append(f"\r\n--{boundary}--\r\n".encode())
|
|
209
|
+
body = b"".join(parts)
|
|
210
|
+
|
|
211
|
+
req = urllib.request.Request(
|
|
212
|
+
endpoint,
|
|
213
|
+
data=body,
|
|
214
|
+
headers={
|
|
215
|
+
"Authorization": f"Bearer {api_key}",
|
|
216
|
+
"Content-Type": f"multipart/form-data; boundary={boundary}",
|
|
217
|
+
},
|
|
218
|
+
method="POST",
|
|
219
|
+
)
|
|
220
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
221
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
222
|
+
return data.get("text")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _run(command: list[str], timeout: float = 120.0) -> bool:
|
|
226
|
+
"""Run a subprocess; return True on exit 0, False on any failure. No raise.
|
|
227
|
+
|
|
228
|
+
Uses subproc.run_with_timeout so a timed-out yt-dlp/ffmpeg is killed at the
|
|
229
|
+
process-group level (os.setsid/killpg) instead of orphaning child trees.
|
|
230
|
+
"""
|
|
231
|
+
try:
|
|
232
|
+
result = subproc.run_with_timeout(command, timeout=int(timeout))
|
|
233
|
+
except (subproc.SubprocTimeout, FileNotFoundError, OSError):
|
|
234
|
+
return False
|
|
235
|
+
return result.returncode == 0
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Trustpilot brand-sentiment source for last30days.
|
|
2
|
+
|
|
3
|
+
Shells out to ``trustpilot-pp-cli`` to surface a company's TrustScore and
|
|
4
|
+
Trustpilot's own AI review summary for brand/company topics. Trustpilot has no
|
|
5
|
+
API key, but it sits behind AWS WAF: the CLI harvests an ``aws-waf-token`` via
|
|
6
|
+
a one-time headless Chrome launch (~10s), then replays it over plain HTTP until
|
|
7
|
+
it expires.
|
|
8
|
+
|
|
9
|
+
Activation gate: only available when ``trustpilot-pp-cli`` is on PATH.
|
|
10
|
+
``pipeline.available_sources`` checks ``shutil.which`` before including
|
|
11
|
+
``trustpilot``.
|
|
12
|
+
|
|
13
|
+
Default-on safety (three gates):
|
|
14
|
+
1. Brand-shape gate. The CLI is invoked only when the topic resolves to a
|
|
15
|
+
company/brand -- a domain-like token, or a short (<=2-word) capitalized
|
|
16
|
+
proper noun. Generic phrases ("AI coding agents", "agent memory") and
|
|
17
|
+
longer multi-word phrases never call the CLI, so Trustpilot stays quiet --
|
|
18
|
+
and never harvests Chrome -- on non-company topics.
|
|
19
|
+
2. Browser opt-out. Automated contexts (cron, CI, the eval harness) can set
|
|
20
|
+
``LAST30DAYS_TRUSTPILOT_NO_BROWSER`` to disable the source entirely, so a
|
|
21
|
+
headless run never spawns the cookie harvest.
|
|
22
|
+
3. Graceful degradation. Any CLI failure (no Chrome, expired cookie that
|
|
23
|
+
cannot re-harvest, timeout) degrades to empty results, never an error.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import re
|
|
31
|
+
import shutil
|
|
32
|
+
from typing import Any, Dict, List, Optional
|
|
33
|
+
|
|
34
|
+
from . import dates, log, subproc
|
|
35
|
+
from .relevance import token_overlap_relevance
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
CLI_BIN = "trustpilot-pp-cli"
|
|
39
|
+
|
|
40
|
+
SEARCH_TIMEOUT = 75 # generous: a cold run may harvest a WAF cookie (~10s).
|
|
41
|
+
|
|
42
|
+
NO_BROWSER_ENV = "LAST30DAYS_TRUSTPILOT_NO_BROWSER"
|
|
43
|
+
|
|
44
|
+
# Domain-like token, e.g. "chownow.com", "nothing.tech".
|
|
45
|
+
_DOMAIN_RE = re.compile(r"\b[a-z0-9][a-z0-9-]*\.(com|io|co|net|org|app|ai|dev|gg|tech|shop|store)\b")
|
|
46
|
+
|
|
47
|
+
# Generic tokens that disqualify a short capitalized phrase from being a brand.
|
|
48
|
+
_GENERIC_TOKENS = {
|
|
49
|
+
"ai", "best", "top", "vs", "review", "reviews", "guide", "tutorial",
|
|
50
|
+
"how", "what", "why", "agents", "agent", "memory", "tips", "news",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Single-word programming languages, frameworks, runtimes, OSes, and dev tools.
|
|
54
|
+
# A bare capitalized "Python"/"React"/"Docker" query is overwhelmingly about the
|
|
55
|
+
# technology, not a company's customer reviews -- letting it through would both
|
|
56
|
+
# trigger the Chrome harvest and risk surfacing an unrelated company that shares
|
|
57
|
+
# the name. A user who genuinely wants the company can use its domain
|
|
58
|
+
# (e.g. "docker.com"), which still passes via the domain branch.
|
|
59
|
+
_TECH_TOKENS = {
|
|
60
|
+
"python", "javascript", "typescript", "java", "rust", "ruby", "php",
|
|
61
|
+
"kotlin", "scala", "golang", "swift", "elixir", "erlang", "haskell",
|
|
62
|
+
"react", "vue", "angular", "svelte", "django", "flask", "rails", "spring",
|
|
63
|
+
"node", "nodejs", "deno", "bun", "express", "nextjs", "nuxt",
|
|
64
|
+
"linux", "ubuntu", "debian", "fedora", "windows", "macos", "android",
|
|
65
|
+
"docker", "kubernetes", "k8s", "terraform", "ansible", "nginx",
|
|
66
|
+
"redis", "postgres", "postgresql", "mysql", "sqlite", "mongodb", "kafka",
|
|
67
|
+
"graphql", "webpack", "vite", "rust", "wasm",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _log(msg: str) -> None:
|
|
72
|
+
log.source_log("Trustpilot", msg, tty_only=False)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _is_available() -> bool:
|
|
76
|
+
"""True when the trustpilot-pp-cli binary is on PATH."""
|
|
77
|
+
return shutil.which(CLI_BIN) is not None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _truthy(value: Any) -> bool:
|
|
81
|
+
return str(value or "").strip().lower() in ("1", "true", "yes", "on")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _harvest_allowed(config: Optional[Dict[str, Any]]) -> bool:
|
|
85
|
+
"""False when the browser opt-out is set (automated/headless contexts).
|
|
86
|
+
|
|
87
|
+
Reads the opt-out from the merged config AND directly from the process
|
|
88
|
+
environment. The env fallback is load-bearing: ``config`` is assembled from
|
|
89
|
+
an allowlist in ``env.get_config``, so a fallback here guarantees the
|
|
90
|
+
documented kill-switch works even when the key is not propagated into
|
|
91
|
+
config (e.g. a bare ``LAST30DAYS_TRUSTPILOT_NO_BROWSER=1`` in cron/CI).
|
|
92
|
+
"""
|
|
93
|
+
if config and _truthy(config.get(NO_BROWSER_ENV)):
|
|
94
|
+
return False
|
|
95
|
+
if _truthy(os.environ.get(NO_BROWSER_ENV)):
|
|
96
|
+
return False
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def is_brand_shaped(topic: str) -> bool:
|
|
101
|
+
"""True when the topic looks like a company/brand Trustpilot can resolve.
|
|
102
|
+
|
|
103
|
+
A domain-like token always qualifies. Otherwise the topic must be a short
|
|
104
|
+
(<=2-word) capitalized proper noun with no generic tokens -- this lets
|
|
105
|
+
"ChowNow", "Nothing Phone", and "OpenAI" through while keeping "AI coding
|
|
106
|
+
agents", "agent memory", and "Golden State Warriors" out.
|
|
107
|
+
"""
|
|
108
|
+
if not topic or not topic.strip():
|
|
109
|
+
return False
|
|
110
|
+
text = topic.strip()
|
|
111
|
+
if _DOMAIN_RE.search(text.lower()):
|
|
112
|
+
return True
|
|
113
|
+
words = text.split()
|
|
114
|
+
if len(words) > 2:
|
|
115
|
+
return False
|
|
116
|
+
if any(w.lower() in _GENERIC_TOKENS or w.lower() in _TECH_TOKENS for w in words):
|
|
117
|
+
return False
|
|
118
|
+
# At least one token must look like a proper noun (leading capital).
|
|
119
|
+
return any(w[:1].isupper() for w in words)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _company_identifier(topic: str) -> str:
|
|
123
|
+
"""Pick the identifier to hand the CLI: a domain token if present, else the
|
|
124
|
+
cleaned topic string."""
|
|
125
|
+
m = _DOMAIN_RE.search(topic.lower())
|
|
126
|
+
if m:
|
|
127
|
+
return m.group(0)
|
|
128
|
+
return topic.strip()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _build_info_args(identifier: str) -> List[str]:
|
|
132
|
+
return [CLI_BIN, "info", identifier, "--agent"]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _run_cli(cmd: List[str], timeout: int) -> Dict[str, Any]:
|
|
136
|
+
"""Invoke trustpilot-pp-cli and parse the JSON object. Never raises."""
|
|
137
|
+
if not _is_available():
|
|
138
|
+
return {"error": f"{CLI_BIN} not on PATH"}
|
|
139
|
+
try:
|
|
140
|
+
result = subproc.run_with_timeout(cmd, timeout=timeout)
|
|
141
|
+
except subproc.SubprocTimeout as exc:
|
|
142
|
+
_log(f"Timeout: {exc}")
|
|
143
|
+
return {"error": str(exc)}
|
|
144
|
+
except FileNotFoundError as exc:
|
|
145
|
+
_log(f"Binary missing: {exc}")
|
|
146
|
+
return {"error": str(exc)}
|
|
147
|
+
except OSError as exc:
|
|
148
|
+
_log(f"Spawn failed: {exc}")
|
|
149
|
+
return {"error": str(exc)}
|
|
150
|
+
|
|
151
|
+
if result.returncode != 0:
|
|
152
|
+
snippet = (result.stderr or "").strip().splitlines()[:1]
|
|
153
|
+
first = snippet[0] if snippet else f"exit {result.returncode}"
|
|
154
|
+
_log(f"CLI exit {result.returncode}: {first}")
|
|
155
|
+
return {"error": first}
|
|
156
|
+
|
|
157
|
+
stdout = result.stdout or ""
|
|
158
|
+
if not stdout.strip():
|
|
159
|
+
return {}
|
|
160
|
+
try:
|
|
161
|
+
data = json.loads(stdout)
|
|
162
|
+
except json.JSONDecodeError as exc:
|
|
163
|
+
_log(f"JSON decode failed: {exc}")
|
|
164
|
+
return {"error": f"json decode: {exc}"}
|
|
165
|
+
return data if isinstance(data, dict) else {}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def search_trustpilot(
|
|
169
|
+
topic: str,
|
|
170
|
+
from_date: str,
|
|
171
|
+
to_date: str,
|
|
172
|
+
depth: str = "default",
|
|
173
|
+
config: Optional[Dict[str, Any]] = None,
|
|
174
|
+
) -> Dict[str, Any]:
|
|
175
|
+
"""Look up a company's Trustpilot sentiment, gated on a brand-shaped topic.
|
|
176
|
+
|
|
177
|
+
Returns ``{"results": [info_dict]}`` for a resolved company, or
|
|
178
|
+
``{"results": []}`` when the topic is not brand-shaped, the browser opt-out
|
|
179
|
+
is set, or the CLI fails.
|
|
180
|
+
"""
|
|
181
|
+
if not is_brand_shaped(topic):
|
|
182
|
+
return {"results": []}
|
|
183
|
+
if not _is_available():
|
|
184
|
+
return {"results": [], "error": f"{CLI_BIN} not on PATH"}
|
|
185
|
+
if not _harvest_allowed(config):
|
|
186
|
+
_log("skipped: browser opt-out set")
|
|
187
|
+
return {"results": []}
|
|
188
|
+
identifier = _company_identifier(topic)
|
|
189
|
+
_log(f"info '{identifier}'")
|
|
190
|
+
data = _run_cli(_build_info_args(identifier), timeout=SEARCH_TIMEOUT)
|
|
191
|
+
if "error" in data or not data:
|
|
192
|
+
return {"results": []}
|
|
193
|
+
return {"results": [data]}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _coerce_float(value: Any) -> Optional[float]:
|
|
197
|
+
try:
|
|
198
|
+
return float(value)
|
|
199
|
+
except (TypeError, ValueError):
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _coerce_int(value: Any) -> Optional[int]:
|
|
204
|
+
try:
|
|
205
|
+
return int(value)
|
|
206
|
+
except (TypeError, ValueError):
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def parse_trustpilot_response(
|
|
211
|
+
response: Dict[str, Any],
|
|
212
|
+
query: str = "",
|
|
213
|
+
) -> List[Dict[str, Any]]:
|
|
214
|
+
"""Parse a Trustpilot ``info`` envelope into a single normalized item.
|
|
215
|
+
|
|
216
|
+
The AI summary is the body (it already balances positive and negative
|
|
217
|
+
sentiment). TrustScore and review count feed engagement and metadata.
|
|
218
|
+
Returns dicts ready for ``normalize._normalize_trustpilot``.
|
|
219
|
+
"""
|
|
220
|
+
raw = response.get("results") if isinstance(response, dict) else None
|
|
221
|
+
if not isinstance(raw, list) or not raw:
|
|
222
|
+
return []
|
|
223
|
+
info = raw[0]
|
|
224
|
+
if not isinstance(info, dict):
|
|
225
|
+
return []
|
|
226
|
+
|
|
227
|
+
resolved_name = str(info.get("name") or info.get("displayName") or "").strip()
|
|
228
|
+
ai_summary = str(info.get("aiSummary") or info.get("summary") or "").strip()
|
|
229
|
+
trust_score = _coerce_float(info.get("trustScore") or info.get("score"))
|
|
230
|
+
review_count = _coerce_int(
|
|
231
|
+
info.get("reviewCount") or info.get("numberOfReviews") or info.get("total")
|
|
232
|
+
)
|
|
233
|
+
url = str(info.get("url") or "").strip()
|
|
234
|
+
domain = str(info.get("domain") or info.get("identifyingName") or "").strip()
|
|
235
|
+
if not url and domain:
|
|
236
|
+
url = f"https://www.trustpilot.com/review/{domain}"
|
|
237
|
+
|
|
238
|
+
# Require substantive content from the company record itself; do not
|
|
239
|
+
# fabricate an item from the query alone when the CLI returned nothing.
|
|
240
|
+
if not resolved_name and not ai_summary and trust_score is None and review_count is None:
|
|
241
|
+
return []
|
|
242
|
+
|
|
243
|
+
name = resolved_name or query.strip()
|
|
244
|
+
|
|
245
|
+
title = f"{name} on Trustpilot" if name else "Trustpilot reviews"
|
|
246
|
+
if trust_score is not None:
|
|
247
|
+
title = f"{name}: TrustScore {trust_score}" if name else title
|
|
248
|
+
|
|
249
|
+
engagement: Dict[str, float | int] = {}
|
|
250
|
+
if review_count is not None:
|
|
251
|
+
engagement["reviews"] = review_count
|
|
252
|
+
if trust_score is not None:
|
|
253
|
+
engagement["trustScore"] = trust_score
|
|
254
|
+
|
|
255
|
+
relevance = token_overlap_relevance(query, name) if (query and name) else 0.7
|
|
256
|
+
|
|
257
|
+
why = "Trustpilot brand sentiment"
|
|
258
|
+
if trust_score is not None and review_count is not None:
|
|
259
|
+
why = f"Trustpilot: TrustScore {trust_score} across {review_count} reviews"
|
|
260
|
+
elif trust_score is not None:
|
|
261
|
+
why = f"Trustpilot: TrustScore {trust_score}"
|
|
262
|
+
|
|
263
|
+
return [
|
|
264
|
+
{
|
|
265
|
+
"id": domain or name or "trustpilot",
|
|
266
|
+
"title": title,
|
|
267
|
+
"url": url,
|
|
268
|
+
"summary": ai_summary,
|
|
269
|
+
"name": name,
|
|
270
|
+
"trustScore": trust_score,
|
|
271
|
+
"reviewCount": review_count,
|
|
272
|
+
"date": dates.get_date_range(1)[0],
|
|
273
|
+
"engagement": engagement,
|
|
274
|
+
"relevance": round(min(1.0, max(0.4, relevance)), 2),
|
|
275
|
+
"why_relevant": why,
|
|
276
|
+
}
|
|
277
|
+
]
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Truth Social search via Mastodon-compatible API (requires bearer token).
|
|
2
|
+
|
|
3
|
+
Uses truthsocial.com/api/v2/search endpoint.
|
|
4
|
+
Requires TRUTHSOCIAL_TOKEN env var (bearer token from browser dev tools).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import math
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from . import http, log
|
|
13
|
+
|
|
14
|
+
TRUTHSOCIAL_SEARCH_URL = "https://truthsocial.com/api/v2/search"
|
|
15
|
+
|
|
16
|
+
DEPTH_CONFIG = {
|
|
17
|
+
"quick": 15,
|
|
18
|
+
"default": 30,
|
|
19
|
+
"deep": 60,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _log(msg: str):
|
|
24
|
+
log.source_log("TruthSocial", msg, tty_only=False)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _strip_html(html: str) -> str:
|
|
28
|
+
"""Strip HTML tags from Truth Social post content."""
|
|
29
|
+
text = re.sub(r'<br\s*/?>', '\n', html)
|
|
30
|
+
text = re.sub(r'<[^>]+>', '', text)
|
|
31
|
+
return text.strip()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _extract_core_subject(topic: str) -> str:
|
|
35
|
+
"""Extract core subject from verbose query for Truth Social search."""
|
|
36
|
+
from .query import SOCIAL_NOISE, extract_core_subject
|
|
37
|
+
return extract_core_subject(topic, noise=SOCIAL_NOISE)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _parse_date(status: Dict[str, Any]) -> Optional[str]:
|
|
41
|
+
"""Parse date from Mastodon status to YYYY-MM-DD.
|
|
42
|
+
|
|
43
|
+
Mastodon uses ISO 8601 format in created_at field.
|
|
44
|
+
"""
|
|
45
|
+
val = status.get("created_at")
|
|
46
|
+
if val and isinstance(val, str) and len(val) >= 10:
|
|
47
|
+
return val[:10]
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def search_truthsocial(
|
|
52
|
+
topic: str,
|
|
53
|
+
from_date: str,
|
|
54
|
+
to_date: str,
|
|
55
|
+
depth: str = "default",
|
|
56
|
+
config: Optional[Dict[str, Any]] = None,
|
|
57
|
+
) -> Dict[str, Any]:
|
|
58
|
+
"""Search Truth Social via Mastodon-compatible API.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
topic: Search topic
|
|
62
|
+
from_date: Start date (YYYY-MM-DD)
|
|
63
|
+
to_date: End date (YYYY-MM-DD)
|
|
64
|
+
depth: 'quick', 'default', or 'deep'
|
|
65
|
+
config: Config dict with TRUTHSOCIAL_TOKEN
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Dict with 'statuses' list from Mastodon API response.
|
|
69
|
+
"""
|
|
70
|
+
config = config or {}
|
|
71
|
+
token = config.get("TRUTHSOCIAL_TOKEN", "")
|
|
72
|
+
|
|
73
|
+
if not token:
|
|
74
|
+
return {"statuses": [], "error": "Truth Social token not configured"}
|
|
75
|
+
|
|
76
|
+
count = DEPTH_CONFIG.get(depth, DEPTH_CONFIG["default"])
|
|
77
|
+
core_topic = _extract_core_subject(topic)
|
|
78
|
+
|
|
79
|
+
_log(f"Searching for '{core_topic}' (depth={depth}, limit={count})")
|
|
80
|
+
|
|
81
|
+
from urllib.parse import urlencode
|
|
82
|
+
params = {
|
|
83
|
+
"q": core_topic,
|
|
84
|
+
"type": "statuses",
|
|
85
|
+
"limit": str(min(count, 40)),
|
|
86
|
+
}
|
|
87
|
+
url = f"{TRUTHSOCIAL_SEARCH_URL}?{urlencode(params)}"
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
response = http.request(
|
|
91
|
+
"GET", url,
|
|
92
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
93
|
+
timeout=30,
|
|
94
|
+
)
|
|
95
|
+
except http.HTTPError as e:
|
|
96
|
+
if e.status_code == 401:
|
|
97
|
+
_log("Token expired")
|
|
98
|
+
return {"statuses": [], "error": "Truth Social token expired"}
|
|
99
|
+
elif e.status_code == 403:
|
|
100
|
+
_log("Access denied (Cloudflare)")
|
|
101
|
+
return {"statuses": [], "error": "Truth Social access denied (Cloudflare)"}
|
|
102
|
+
elif e.status_code == 429:
|
|
103
|
+
_log("Rate limited")
|
|
104
|
+
return {"statuses": [], "error": "Truth Social rate limited"}
|
|
105
|
+
else:
|
|
106
|
+
_log(f"Search failed: {e}")
|
|
107
|
+
return {"statuses": [], "error": f"Truth Social search failed: {e.status_code}"}
|
|
108
|
+
except Exception as e:
|
|
109
|
+
_log(f"Search failed: {e}")
|
|
110
|
+
return {"statuses": [], "error": str(e)}
|
|
111
|
+
|
|
112
|
+
statuses = response.get("statuses", [])
|
|
113
|
+
_log(f"Found {len(statuses)} posts")
|
|
114
|
+
return response
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def parse_truthsocial_response(response: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
118
|
+
"""Parse Mastodon API response into normalized item dicts.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
List of item dicts ready for normalization.
|
|
122
|
+
"""
|
|
123
|
+
statuses = response.get("statuses", [])
|
|
124
|
+
items = []
|
|
125
|
+
|
|
126
|
+
for i, status in enumerate(statuses):
|
|
127
|
+
content_html = status.get("content") or ""
|
|
128
|
+
text = _strip_html(content_html)
|
|
129
|
+
|
|
130
|
+
account = status.get("account") or {}
|
|
131
|
+
handle = account.get("acct") or account.get("username") or ""
|
|
132
|
+
display_name = account.get("display_name") or handle
|
|
133
|
+
|
|
134
|
+
url = status.get("url") or ""
|
|
135
|
+
|
|
136
|
+
likes = status.get("favourites_count") or 0
|
|
137
|
+
reposts = status.get("reblogs_count") or 0
|
|
138
|
+
replies = status.get("replies_count") or 0
|
|
139
|
+
|
|
140
|
+
date_str = _parse_date(status)
|
|
141
|
+
|
|
142
|
+
# Relevance: position-based (search results are ranked by relevance)
|
|
143
|
+
rank_score = max(0.3, 1.0 - (i * 0.02))
|
|
144
|
+
engagement_boost = min(0.2, math.log1p(likes + reposts) / 40)
|
|
145
|
+
relevance = min(1.0, rank_score * 0.7 + engagement_boost + 0.1)
|
|
146
|
+
|
|
147
|
+
items.append({
|
|
148
|
+
"handle": handle,
|
|
149
|
+
"display_name": display_name,
|
|
150
|
+
"text": text,
|
|
151
|
+
"url": url,
|
|
152
|
+
"date": date_str,
|
|
153
|
+
"engagement": {
|
|
154
|
+
"likes": likes,
|
|
155
|
+
"reposts": reposts,
|
|
156
|
+
"replies": replies,
|
|
157
|
+
},
|
|
158
|
+
"relevance": round(relevance, 2),
|
|
159
|
+
"why_relevant": f"Truth Social: @{handle}: {text[:60]}" if text else f"Truth Social: {handle}",
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
return items
|