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,1073 @@
|
|
|
1
|
+
"""Environment and API key management for last30days skill."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Literal
|
|
11
|
+
|
|
12
|
+
# Allow override via environment variable for testing
|
|
13
|
+
# Set LAST30DAYS_CONFIG_DIR="" for clean/no-config mode
|
|
14
|
+
# Set LAST30DAYS_CONFIG_DIR="/path/to/dir" for custom config location
|
|
15
|
+
_config_override = os.environ.get('LAST30DAYS_CONFIG_DIR')
|
|
16
|
+
if _config_override == "":
|
|
17
|
+
# Empty string = no config file (clean mode)
|
|
18
|
+
CONFIG_DIR = None
|
|
19
|
+
CONFIG_FILE = None
|
|
20
|
+
elif _config_override:
|
|
21
|
+
CONFIG_DIR = Path(_config_override)
|
|
22
|
+
CONFIG_FILE = CONFIG_DIR / ".env"
|
|
23
|
+
else:
|
|
24
|
+
CONFIG_DIR = Path.home() / ".config" / "last30days"
|
|
25
|
+
CONFIG_FILE = CONFIG_DIR / ".env"
|
|
26
|
+
|
|
27
|
+
# macOS Keychain integration: items stored with this service prefix are picked
|
|
28
|
+
# up automatically on Darwin as the lowest-priority credential source.
|
|
29
|
+
# Example: `security add-generic-password -a "$USER" -s last30days-XAI_API_KEY -w "xai-..."`.
|
|
30
|
+
KEYCHAIN_SERVICE_PREFIX = "last30days-"
|
|
31
|
+
|
|
32
|
+
# Optional non-secret aliases for users who already store API keys under a
|
|
33
|
+
# different Keychain naming convention. Configure as JSON in
|
|
34
|
+
# LAST30DAYS_KEYCHAIN_ALIASES, for example:
|
|
35
|
+
# {"XAI_API_KEY":{"account":"keychain-user","service":"existing-xai-api-key"}}
|
|
36
|
+
# A string value is shorthand for {"service": "..."} with the current user.
|
|
37
|
+
KEYCHAIN_ALIASES_ENV = "LAST30DAYS_KEYCHAIN_ALIASES"
|
|
38
|
+
|
|
39
|
+
# Single source of truth for which credentials the Keychain loader looks up.
|
|
40
|
+
# The setup-keychain.sh helper mirrors this list and is held in sync via
|
|
41
|
+
# tests/test_env_keychain.py::test_keychain_keys_match_setup_script.
|
|
42
|
+
KEYCHAIN_KEYS = (
|
|
43
|
+
"OPENAI_API_KEY", "XAI_API_KEY", "GOOGLE_API_KEY", "GEMINI_API_KEY",
|
|
44
|
+
"GOOGLE_GENAI_API_KEY", "SCRAPECREATORS_API_KEY", "APIFY_API_TOKEN",
|
|
45
|
+
"AUTH_TOKEN", "CT0", "BSKY_HANDLE", "BSKY_APP_PASSWORD",
|
|
46
|
+
"TRUTHSOCIAL_TOKEN", "BRAVE_API_KEY", "EXA_API_KEY", "SERPER_API_KEY",
|
|
47
|
+
"OPENROUTER_API_KEY", "PERPLEXITY_API_KEY", "PARALLEL_API_KEY", "XQUIK_API_KEY",
|
|
48
|
+
"XIAOHONGSHU_API_BASE",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# pass(1) integration: Linux/Unix analog of the Keychain source. Each key in
|
|
52
|
+
# KEYCHAIN_KEYS is looked up at pass path f"{prefix}{KEY}", the direct analog of
|
|
53
|
+
# Keychain's "last30days-<KEY>" service-name convention, so any user stores keys
|
|
54
|
+
# under one namespace without editing code. The prefix is resolved at call time
|
|
55
|
+
# (in get_config) from LAST30DAYS_PASS_PREFIX in the process env or a config
|
|
56
|
+
# file, falling back to this default; included verbatim, so keep the trailing
|
|
57
|
+
# separator. Honors PASSWORD_STORE_DIR.
|
|
58
|
+
DEFAULT_PASS_PATH_PREFIX = "last30days/"
|
|
59
|
+
|
|
60
|
+
AuthSource = Literal["api_key", "none"]
|
|
61
|
+
AuthStatus = Literal["ok", "missing"]
|
|
62
|
+
|
|
63
|
+
AUTH_SOURCE_API_KEY: AuthSource = "api_key"
|
|
64
|
+
AUTH_SOURCE_NONE: AuthSource = "none"
|
|
65
|
+
|
|
66
|
+
AUTH_STATUS_OK: AuthStatus = "ok"
|
|
67
|
+
AUTH_STATUS_MISSING: AuthStatus = "missing"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class OpenAIAuth:
|
|
72
|
+
token: str | None
|
|
73
|
+
source: AuthSource
|
|
74
|
+
status: AuthStatus
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
BrowserCookieMode = Literal["off", "read", "plan_only"]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class ConfigLoadPolicy:
|
|
82
|
+
"""Local-read gates for configuration loading.
|
|
83
|
+
|
|
84
|
+
Bare library calls use the safe default: no browser-cookie extraction and no
|
|
85
|
+
project-scoped config. CLI entry points can opt into narrower behavior after
|
|
86
|
+
parsing command intent.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
browser_cookies: BrowserCookieMode = "off"
|
|
90
|
+
allow_project_config: bool = False
|
|
91
|
+
inspect_ignored_project_config: bool = False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _truthy(value: Any) -> bool:
|
|
95
|
+
if value is None:
|
|
96
|
+
return False
|
|
97
|
+
return str(value).strip().lower() in {"1", "true", "yes", "on"}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _project_config_trusted(policy: ConfigLoadPolicy, file_env: dict[str, Any]) -> bool:
|
|
101
|
+
if policy.allow_project_config:
|
|
102
|
+
return True
|
|
103
|
+
process_value = os.environ.get("LAST30DAYS_TRUST_PROJECT_CONFIG")
|
|
104
|
+
if process_value is not None:
|
|
105
|
+
return _truthy(process_value)
|
|
106
|
+
return _truthy(file_env.get("LAST30DAYS_TRUST_PROJECT_CONFIG"))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _check_file_permissions(path: Path) -> None:
|
|
110
|
+
"""Warn to stderr if a secrets file has overly permissive permissions."""
|
|
111
|
+
if os.name == "nt":
|
|
112
|
+
# Windows reports synthesized POSIX mode bits that do not reflect NTFS ACLs.
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
mode = path.stat().st_mode
|
|
117
|
+
# Check if group or other can read (bits 0o044)
|
|
118
|
+
if mode & 0o044:
|
|
119
|
+
sys.stderr.write(
|
|
120
|
+
f"[last30days] WARNING: {path} is readable by other users. "
|
|
121
|
+
f"Run: chmod 600 {path}\n"
|
|
122
|
+
)
|
|
123
|
+
sys.stderr.flush()
|
|
124
|
+
except OSError as exc:
|
|
125
|
+
sys.stderr.write(f"[last30days] WARNING: could not stat {path}: {exc}\n")
|
|
126
|
+
sys.stderr.flush()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def load_env_file(path: Path) -> dict[str, str]:
|
|
130
|
+
"""Load environment variables from a file."""
|
|
131
|
+
env = {}
|
|
132
|
+
if not path or not path.exists():
|
|
133
|
+
return env
|
|
134
|
+
_check_file_permissions(path)
|
|
135
|
+
|
|
136
|
+
with open(path, 'r') as f:
|
|
137
|
+
for line in f:
|
|
138
|
+
line = line.strip()
|
|
139
|
+
if not line or line.startswith('#'):
|
|
140
|
+
continue
|
|
141
|
+
if '=' in line:
|
|
142
|
+
key, _, value = line.partition('=')
|
|
143
|
+
key = key.strip()
|
|
144
|
+
value = value.strip()
|
|
145
|
+
# Remove quotes if present
|
|
146
|
+
if value and value[0] in ('"', "'") and value[-1] == value[0]:
|
|
147
|
+
value = value[1:-1]
|
|
148
|
+
if key and value:
|
|
149
|
+
env[key] = value
|
|
150
|
+
return env
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _parse_keychain_aliases(raw: str | None) -> dict[str, list[dict[str, str]]]:
|
|
154
|
+
"""Parse non-secret Keychain alias metadata from JSON.
|
|
155
|
+
|
|
156
|
+
Supported forms:
|
|
157
|
+
{"XAI_API_KEY": "existing-xai-api-key"}
|
|
158
|
+
{"XAI_API_KEY": {"service": "existing-xai-api-key", "account": "keychain-user"}}
|
|
159
|
+
{"XAI_API_KEY": [{"service": "primary"}, {"service": "fallback"}]}
|
|
160
|
+
|
|
161
|
+
Invalid entries are ignored so a typo never blocks canonical
|
|
162
|
+
`last30days-<KEY>` lookups; malformed JSON emits a warning.
|
|
163
|
+
"""
|
|
164
|
+
if not raw:
|
|
165
|
+
return {}
|
|
166
|
+
try:
|
|
167
|
+
parsed = json.loads(raw)
|
|
168
|
+
except json.JSONDecodeError as exc:
|
|
169
|
+
sys.stderr.write(
|
|
170
|
+
f"[last30days] WARNING: {KEYCHAIN_ALIASES_ENV} is not valid JSON; "
|
|
171
|
+
f"ignoring Keychain aliases while keeping canonical lookups enabled: {exc}\n"
|
|
172
|
+
)
|
|
173
|
+
sys.stderr.flush()
|
|
174
|
+
return {}
|
|
175
|
+
if not isinstance(parsed, dict):
|
|
176
|
+
return {}
|
|
177
|
+
|
|
178
|
+
allowed = set(KEYCHAIN_KEYS)
|
|
179
|
+
aliases: dict[str, list[dict[str, str]]] = {}
|
|
180
|
+
for key, spec in parsed.items():
|
|
181
|
+
if key not in allowed:
|
|
182
|
+
continue
|
|
183
|
+
specs = spec if isinstance(spec, list) else [spec]
|
|
184
|
+
clean_specs: list[dict[str, str]] = []
|
|
185
|
+
for item in specs:
|
|
186
|
+
if isinstance(item, str):
|
|
187
|
+
service = item.strip()
|
|
188
|
+
account = ""
|
|
189
|
+
elif isinstance(item, dict):
|
|
190
|
+
service = str(item.get("service", "")).strip()
|
|
191
|
+
account = str(item.get("account", "")).strip()
|
|
192
|
+
else:
|
|
193
|
+
continue
|
|
194
|
+
if service:
|
|
195
|
+
clean_specs.append({"service": service, "account": account})
|
|
196
|
+
if clean_specs:
|
|
197
|
+
aliases[key] = clean_specs
|
|
198
|
+
return aliases
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _load_keychain(keys: list[str], aliases: dict[str, list[dict[str, str]]] | None = None) -> dict[str, str]:
|
|
202
|
+
"""Load credentials from macOS Keychain (no-op on other platforms).
|
|
203
|
+
|
|
204
|
+
Each key is looked up as a generic password with service name
|
|
205
|
+
``f"{KEYCHAIN_SERVICE_PREFIX}{key}"`` for the current user. Missing items
|
|
206
|
+
then fall back to optional alias metadata from
|
|
207
|
+
``LAST30DAYS_KEYCHAIN_ALIASES``. Lookup failures are silent — Keychain is
|
|
208
|
+
the lowest-priority source and is meant to be additive over `.env` files
|
|
209
|
+
and process environment.
|
|
210
|
+
"""
|
|
211
|
+
import platform
|
|
212
|
+
if platform.system() != "Darwin":
|
|
213
|
+
return {}
|
|
214
|
+
|
|
215
|
+
import shutil
|
|
216
|
+
security = shutil.which("security")
|
|
217
|
+
if not security:
|
|
218
|
+
return {}
|
|
219
|
+
|
|
220
|
+
import subprocess
|
|
221
|
+
# USER can be unset under sudo, in Docker without --env USER, or in some CI
|
|
222
|
+
# runners; fall back to the OS user record so lookups still match items
|
|
223
|
+
# stored by setup-keychain.sh (which uses $USER).
|
|
224
|
+
user = os.environ.get("USER")
|
|
225
|
+
if not user:
|
|
226
|
+
try:
|
|
227
|
+
import pwd
|
|
228
|
+
except ImportError:
|
|
229
|
+
pwd = None
|
|
230
|
+
|
|
231
|
+
if pwd is not None:
|
|
232
|
+
try:
|
|
233
|
+
user = pwd.getpwuid(os.getuid()).pw_name
|
|
234
|
+
except AttributeError:
|
|
235
|
+
user = "unknown"
|
|
236
|
+
else:
|
|
237
|
+
user = "unknown"
|
|
238
|
+
env: dict[str, str] = {}
|
|
239
|
+
|
|
240
|
+
def lookup(account: str, service: str) -> str:
|
|
241
|
+
try:
|
|
242
|
+
result = subprocess.run(
|
|
243
|
+
[security, "find-generic-password",
|
|
244
|
+
"-a", account,
|
|
245
|
+
"-s", service,
|
|
246
|
+
"-w"],
|
|
247
|
+
capture_output=True, text=True, timeout=5,
|
|
248
|
+
)
|
|
249
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
250
|
+
return ""
|
|
251
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
252
|
+
return result.stdout.strip()
|
|
253
|
+
return ""
|
|
254
|
+
|
|
255
|
+
for key in keys:
|
|
256
|
+
value = lookup(user, f"{KEYCHAIN_SERVICE_PREFIX}{key}")
|
|
257
|
+
if not value and aliases:
|
|
258
|
+
for alias in aliases.get(key, []):
|
|
259
|
+
alias_account = alias.get("account") or user
|
|
260
|
+
value = lookup(alias_account, alias["service"])
|
|
261
|
+
if value:
|
|
262
|
+
break
|
|
263
|
+
if value:
|
|
264
|
+
env[key] = value
|
|
265
|
+
return env
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _load_pass(keys: list[str], prefix: str) -> dict[str, str]:
|
|
269
|
+
"""Load credentials from a pass(1) store (no-op if `pass` is absent).
|
|
270
|
+
|
|
271
|
+
The Linux/Unix analog of the macOS Keychain source. Each env-var name is
|
|
272
|
+
looked up at pass path ``f"{prefix}{key}"`` — mirroring Keychain's
|
|
273
|
+
``last30days-<key>`` service-name convention — so any user stores keys under
|
|
274
|
+
that namespace without editing code (prefix overridable via
|
|
275
|
+
``LAST30DAYS_PASS_PREFIX``). The secret is decrypted in a subprocess and
|
|
276
|
+
read from stdout's first line (pass keeps the secret there; any metadata
|
|
277
|
+
follows) — never written to disk, never logged. Honors ``PASSWORD_STORE_DIR``.
|
|
278
|
+
Missing entries and failures are silent: pass is a lowest-priority, additive
|
|
279
|
+
source like Keychain, so an explicit .env or process-env value still wins.
|
|
280
|
+
"""
|
|
281
|
+
import shutil
|
|
282
|
+
pass_bin = shutil.which("pass")
|
|
283
|
+
if not pass_bin:
|
|
284
|
+
return {}
|
|
285
|
+
|
|
286
|
+
import subprocess
|
|
287
|
+
env: dict[str, str] = {}
|
|
288
|
+
for key in keys:
|
|
289
|
+
try:
|
|
290
|
+
result = subprocess.run(
|
|
291
|
+
[pass_bin, "show", f"{prefix}{key}"],
|
|
292
|
+
capture_output=True, text=True, timeout=5,
|
|
293
|
+
encoding="utf-8", errors="replace",
|
|
294
|
+
)
|
|
295
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
296
|
+
# A timeout (GPG/pinentry hanging) or exec failure isn't a per-key
|
|
297
|
+
# condition — it means the store is unusable right now. Stop instead
|
|
298
|
+
# of paying the timeout once per key; otherwise a locked store would
|
|
299
|
+
# stall every config load by 5s x len(keys). A genuinely missing key
|
|
300
|
+
# returns fast with a non-zero exit and is handled below.
|
|
301
|
+
break
|
|
302
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
303
|
+
env[key] = result.stdout.strip().splitlines()[0]
|
|
304
|
+
return env
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def get_openai_auth(file_env: dict[str, str]) -> OpenAIAuth:
|
|
308
|
+
"""Resolve OpenAI API auth from explicit user-provided API keys."""
|
|
309
|
+
api_key = os.environ.get('OPENAI_API_KEY') or file_env.get('OPENAI_API_KEY')
|
|
310
|
+
if api_key:
|
|
311
|
+
return OpenAIAuth(
|
|
312
|
+
token=api_key,
|
|
313
|
+
source=AUTH_SOURCE_API_KEY,
|
|
314
|
+
status=AUTH_STATUS_OK,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
return OpenAIAuth(
|
|
318
|
+
token=None,
|
|
319
|
+
source=AUTH_SOURCE_NONE,
|
|
320
|
+
status=AUTH_STATUS_MISSING,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _find_project_env() -> Path | None:
|
|
325
|
+
"""Find per-project .env by walking up from cwd.
|
|
326
|
+
|
|
327
|
+
Searches for .claude/last30days.env in each parent directory,
|
|
328
|
+
stopping at the git root, user's home directory, or filesystem root.
|
|
329
|
+
"""
|
|
330
|
+
cwd = Path.cwd()
|
|
331
|
+
for parent in [cwd, *cwd.parents]:
|
|
332
|
+
candidate = parent / '.claude' / 'last30days.env'
|
|
333
|
+
if candidate.exists():
|
|
334
|
+
return candidate
|
|
335
|
+
if (parent / ".git").exists():
|
|
336
|
+
break
|
|
337
|
+
# Stop at filesystem root or home
|
|
338
|
+
if parent == Path.home() or parent == parent.parent:
|
|
339
|
+
break
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def get_config(policy: ConfigLoadPolicy | None = None) -> dict[str, Any]:
|
|
344
|
+
"""Load configuration from multiple sources.
|
|
345
|
+
|
|
346
|
+
Priority (highest wins):
|
|
347
|
+
1. Environment variables (os.environ)
|
|
348
|
+
2. Trusted .claude/last30days.env (per-project config)
|
|
349
|
+
3. ~/.config/last30days/.env (global config)
|
|
350
|
+
4. macOS Keychain items prefixed ``last30days-`` (Darwin only)
|
|
351
|
+
"""
|
|
352
|
+
policy = policy or ConfigLoadPolicy()
|
|
353
|
+
# Load from global config file
|
|
354
|
+
file_env = load_env_file(CONFIG_FILE) if CONFIG_FILE else {}
|
|
355
|
+
|
|
356
|
+
# Load per-project config only when trust comes from process env, global
|
|
357
|
+
# user config, or an explicit policy. A project file cannot grant trust to
|
|
358
|
+
# itself because it is not parsed until after this decision.
|
|
359
|
+
project_config_trusted = _project_config_trusted(policy, file_env)
|
|
360
|
+
project_env_path = _find_project_env() if project_config_trusted else None
|
|
361
|
+
project_env = load_env_file(project_env_path) if project_env_path else {}
|
|
362
|
+
ignored_project_env_path = None
|
|
363
|
+
ignored_project_keys: list[str] = []
|
|
364
|
+
if not project_config_trusted and policy.inspect_ignored_project_config:
|
|
365
|
+
ignored_project_env_path = _find_project_env()
|
|
366
|
+
if ignored_project_env_path:
|
|
367
|
+
ignored_project_keys = sorted(load_env_file(ignored_project_env_path).keys())
|
|
368
|
+
|
|
369
|
+
# Merge file sources: project > global
|
|
370
|
+
merged_env = {**file_env, **project_env}
|
|
371
|
+
|
|
372
|
+
# Keychain is the lowest-priority source (Darwin only; no-op elsewhere).
|
|
373
|
+
# Loaded before openai_auth so OPENAI_API_KEY can come from Keychain too.
|
|
374
|
+
keychain_aliases_raw = os.environ.get(KEYCHAIN_ALIASES_ENV) or merged_env.get(KEYCHAIN_ALIASES_ENV)
|
|
375
|
+
keychain_aliases = _parse_keychain_aliases(keychain_aliases_raw)
|
|
376
|
+
keychain_env = _load_keychain(list(KEYCHAIN_KEYS), keychain_aliases)
|
|
377
|
+
merged_env = {**keychain_env, **merged_env}
|
|
378
|
+
# pass(1) store: Linux/Unix analog of Keychain at convention path
|
|
379
|
+
# {prefix}<KEY>. Decrypts transiently so secrets stay encrypted at rest (no
|
|
380
|
+
# plaintext .env). Lowest priority: Keychain, the config files, and process
|
|
381
|
+
# env all win over it. Two efficiency guards so a user who merely has `pass`
|
|
382
|
+
# on PATH doesn't pay for it: resolve the prefix from the loaded config/env
|
|
383
|
+
# (not import time, so a .env-set LAST30DAYS_PASS_PREFIX is honored), and
|
|
384
|
+
# probe ONLY keys still unset after the higher-priority sources — an empty
|
|
385
|
+
# list short-circuits with no gpg/pinentry calls at all.
|
|
386
|
+
pass_prefix = (
|
|
387
|
+
os.environ.get("LAST30DAYS_PASS_PREFIX")
|
|
388
|
+
or merged_env.get("LAST30DAYS_PASS_PREFIX")
|
|
389
|
+
or DEFAULT_PASS_PATH_PREFIX
|
|
390
|
+
)
|
|
391
|
+
pass_missing = [k for k in KEYCHAIN_KEYS if k not in os.environ and not merged_env.get(k)]
|
|
392
|
+
pass_env = _load_pass(pass_missing, pass_prefix)
|
|
393
|
+
merged_env = {**pass_env, **merged_env}
|
|
394
|
+
|
|
395
|
+
openai_auth = get_openai_auth(merged_env)
|
|
396
|
+
|
|
397
|
+
# Build config: Codex/OpenAI auth + process.env > project .env > global .env
|
|
398
|
+
config = {
|
|
399
|
+
'OPENAI_API_KEY': openai_auth.token,
|
|
400
|
+
'OPENAI_AUTH_SOURCE': openai_auth.source,
|
|
401
|
+
'OPENAI_AUTH_STATUS': openai_auth.status,
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
keys = [
|
|
405
|
+
('XAI_API_KEY', None),
|
|
406
|
+
('GOOGLE_API_KEY', None),
|
|
407
|
+
('GEMINI_API_KEY', None),
|
|
408
|
+
('GOOGLE_GENAI_API_KEY', None),
|
|
409
|
+
('XIAOHONGSHU_API_BASE', None),
|
|
410
|
+
('LAST30DAYS_REASONING_PROVIDER', 'auto'),
|
|
411
|
+
('LAST30DAYS_PLANNER_MODEL', None),
|
|
412
|
+
('LAST30DAYS_RERANK_MODEL', None),
|
|
413
|
+
('LAST30DAYS_X_MODEL', None),
|
|
414
|
+
('LAST30DAYS_X_BACKEND', None),
|
|
415
|
+
('LAST30DAYS_REDDIT_BACKEND', None),
|
|
416
|
+
('LAST30DAYS_REDDIT_SC_MIN_ITEMS', None),
|
|
417
|
+
('LAST30DAYS_STORE', None),
|
|
418
|
+
('LAST30DAYS_MEMORY_DIR', None),
|
|
419
|
+
('OPENAI_MODEL_PIN', None),
|
|
420
|
+
('XAI_MODEL_PIN', None),
|
|
421
|
+
('OPENAI_BASE_URL', None),
|
|
422
|
+
('XAI_BASE_URL', None),
|
|
423
|
+
('SCRAPECREATORS_API_KEY', None),
|
|
424
|
+
('APIFY_API_TOKEN', None),
|
|
425
|
+
('AUTH_TOKEN', None),
|
|
426
|
+
('CT0', None),
|
|
427
|
+
('BSKY_HANDLE', None),
|
|
428
|
+
('BSKY_APP_PASSWORD', None),
|
|
429
|
+
('BSKY_SEARCH_HOST', None),
|
|
430
|
+
('TRUTHSOCIAL_TOKEN', None),
|
|
431
|
+
('BRAVE_API_KEY', None),
|
|
432
|
+
('EXA_API_KEY', None),
|
|
433
|
+
('SERPER_API_KEY', None),
|
|
434
|
+
('OPENROUTER_API_KEY', None),
|
|
435
|
+
('PERPLEXITY_API_KEY', None),
|
|
436
|
+
('LAST30DAYS_PERPLEXITY_MODE', 'sonar'),
|
|
437
|
+
('LAST30DAYS_PERPLEXITY_MODEL', None),
|
|
438
|
+
('LAST30DAYS_PERPLEXITY_MAX_RESULTS', None),
|
|
439
|
+
('LAST30DAYS_PERPLEXITY_SEARCH_CONTEXT_SIZE', None),
|
|
440
|
+
('LAST30DAYS_PERPLEXITY_SEARCH_MODE', None),
|
|
441
|
+
('LAST30DAYS_PERPLEXITY_DOMAIN_FILTER', None),
|
|
442
|
+
('LAST30DAYS_PERPLEXITY_LANGUAGE_FILTER', None),
|
|
443
|
+
('LAST30DAYS_PERPLEXITY_COUNTRY', None),
|
|
444
|
+
('LAST30DAYS_PERPLEXITY_RECENCY_FILTER', None),
|
|
445
|
+
('LAST30DAYS_PERPLEXITY_REASONING_EFFORT', None),
|
|
446
|
+
('LAST30DAYS_PERPLEXITY_DEEP_TIMEOUT_SECONDS', '600'),
|
|
447
|
+
('PARALLEL_API_KEY', None),
|
|
448
|
+
('XQUIK_API_KEY', None),
|
|
449
|
+
# Host-native search signal: set by the SKILL.md agent-host path when the
|
|
450
|
+
# invoking runtime has its own (better) web-search tool, so the engine's
|
|
451
|
+
# keyless search floor stays off there. Defaults unset -> floor allowed.
|
|
452
|
+
('LAST30DAYS_NATIVE_SEARCH', None),
|
|
453
|
+
# Optional SearXNG instance for the keyless-search fallback rung.
|
|
454
|
+
('LAST30DAYS_SEARXNG_URL', None),
|
|
455
|
+
# Truthy -> disable Trustpilot's headless-Chrome WAF-cookie harvest in
|
|
456
|
+
# automated contexts (cron/CI/eval). Read by trustpilot._harvest_allowed.
|
|
457
|
+
('LAST30DAYS_TRUSTPILOT_NO_BROWSER', None),
|
|
458
|
+
('FROM_BROWSER', None),
|
|
459
|
+
('LAST30DAYS_TRUST_PROJECT_CONFIG', None),
|
|
460
|
+
('SETUP_COMPLETE', None),
|
|
461
|
+
('INCLUDE_SOURCES', ''),
|
|
462
|
+
('EXCLUDE_SOURCES', ''),
|
|
463
|
+
('LAST30DAYS_DEFAULT_SEARCH', ''),
|
|
464
|
+
('LAST30DAYS_YOUTUBE_SSH_HOST', None),
|
|
465
|
+
('LAST30DAYS_TRANSCRIPT_TIMEOUT', None),
|
|
466
|
+
(KEYCHAIN_ALIASES_ENV, None),
|
|
467
|
+
# Whisper transcription provider for caption-free audio/video. Groq's
|
|
468
|
+
# free tier is preferred; OPENAI_API_KEY is the paid backstop (already
|
|
469
|
+
# resolved above via openai_auth).
|
|
470
|
+
('GROQ_API_KEY', None),
|
|
471
|
+
('LAST30DAYS_YT_SUB_LANGS', 'en,es,pt'),
|
|
472
|
+
]
|
|
473
|
+
|
|
474
|
+
for key, default in keys:
|
|
475
|
+
config[key] = os.environ.get(key) or merged_env.get(key, default)
|
|
476
|
+
|
|
477
|
+
# Backward-compat: ScrapeCreators' own examples and tutorials use the
|
|
478
|
+
# SCRAPE_CREATORS_API_KEY spelling (with underscore between SCRAPE and
|
|
479
|
+
# CREATORS). Accept that form too so users who follow the vendor's docs
|
|
480
|
+
# don't silently end up with has_scrapecreators=False. Canonical name
|
|
481
|
+
# wins when both are set.
|
|
482
|
+
if not config.get('SCRAPECREATORS_API_KEY'):
|
|
483
|
+
legacy = os.environ.get('SCRAPE_CREATORS_API_KEY') or merged_env.get('SCRAPE_CREATORS_API_KEY')
|
|
484
|
+
if legacy:
|
|
485
|
+
config['SCRAPECREATORS_API_KEY'] = legacy
|
|
486
|
+
|
|
487
|
+
# Multi-key rotation: comma-separated SCRAPECREATORS_API_KEY round-robins
|
|
488
|
+
# via random.choice per run. Originally added in #268, accidentally dropped
|
|
489
|
+
# in v3.0.6, restored here.
|
|
490
|
+
sc_key_raw = config.get('SCRAPECREATORS_API_KEY') or ''
|
|
491
|
+
if ',' in sc_key_raw:
|
|
492
|
+
import random
|
|
493
|
+
sc_keys = [k.strip() for k in sc_key_raw.split(',') if k.strip()]
|
|
494
|
+
config['SCRAPECREATORS_API_KEY'] = random.choice(sc_keys) if sc_keys else ''
|
|
495
|
+
|
|
496
|
+
# Track which config source was used (highest-priority file source wins
|
|
497
|
+
# the label; keychain is only reported when nothing else is configured).
|
|
498
|
+
if project_env_path:
|
|
499
|
+
config['_CONFIG_SOURCE'] = f'project:{project_env_path}'
|
|
500
|
+
elif CONFIG_FILE and CONFIG_FILE.exists():
|
|
501
|
+
config['_CONFIG_SOURCE'] = f'global:{CONFIG_FILE}'
|
|
502
|
+
elif keychain_env:
|
|
503
|
+
config['_CONFIG_SOURCE'] = 'keychain'
|
|
504
|
+
elif pass_env:
|
|
505
|
+
config['_CONFIG_SOURCE'] = 'pass'
|
|
506
|
+
else:
|
|
507
|
+
config['_CONFIG_SOURCE'] = 'env_only'
|
|
508
|
+
if ignored_project_env_path:
|
|
509
|
+
config['_IGNORED_PROJECT_CONFIG'] = str(ignored_project_env_path)
|
|
510
|
+
config['_IGNORED_PROJECT_CONFIG_KEYS'] = ignored_project_keys
|
|
511
|
+
config['_BROWSER_COOKIE_MODE'] = policy.browser_cookies
|
|
512
|
+
config['_BROWSER_COOKIE_BROWSERS'] = cookie_extraction_browsers(config)
|
|
513
|
+
|
|
514
|
+
if policy.browser_cookies == "read":
|
|
515
|
+
browser_creds = extract_browser_credentials(config)
|
|
516
|
+
for key, value in browser_creds.items():
|
|
517
|
+
if not config.get(key):
|
|
518
|
+
config[key] = value
|
|
519
|
+
config[f"_{key}_SOURCE"] = "browser"
|
|
520
|
+
|
|
521
|
+
return config
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
# ---------------------------------------------------------------------------
|
|
525
|
+
# Browser cookie extraction
|
|
526
|
+
# ---------------------------------------------------------------------------
|
|
527
|
+
|
|
528
|
+
COOKIE_DOMAINS: dict[str, dict[str, Any]] = {
|
|
529
|
+
"x": {
|
|
530
|
+
"domain": ".x.com",
|
|
531
|
+
"cookies": ["auth_token", "ct0"],
|
|
532
|
+
"mapping": {"auth_token": "AUTH_TOKEN", "ct0": "CT0"},
|
|
533
|
+
},
|
|
534
|
+
"truthsocial": {
|
|
535
|
+
"domain": ".truthsocial.com",
|
|
536
|
+
"cookies": ["_session_id"],
|
|
537
|
+
"mapping": {"_session_id": "TRUTHSOCIAL_TOKEN"},
|
|
538
|
+
},
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def cookie_extraction_browsers(config: dict[str, Any]) -> list[str]:
|
|
543
|
+
"""Browsers to try for cookie extraction, honoring FROM_BROWSER.
|
|
544
|
+
|
|
545
|
+
Default (FROM_BROWSER unset): no browser-cookie reads. The Chromium family
|
|
546
|
+
(Chrome, Brave, Edge, Vivaldi, Opera, Arc, Chromium) is available only when
|
|
547
|
+
explicitly selected because reading their cookies on macOS requires the
|
|
548
|
+
browser's Safe Storage Keychain key, which triggers a system password prompt
|
|
549
|
+
that cannot be reliably suppressed. On Windows only Firefox cookie
|
|
550
|
+
extraction is supported; Chrome and Edge use DPAPI-encrypted cookie stores
|
|
551
|
+
that are not yet supported.
|
|
552
|
+
|
|
553
|
+
- ``FROM_BROWSER=<name>`` - a single browser (e.g. ``firefox``, ``brave``,
|
|
554
|
+
``edge``, ``arc``).
|
|
555
|
+
- ``FROM_BROWSER=firefox,safari`` - a comma-separated explicit browser list.
|
|
556
|
+
- ``FROM_BROWSER=auto`` - also try every Chromium browser (user accepts the
|
|
557
|
+
Keychain dialog when needed).
|
|
558
|
+
- ``FROM_BROWSER=off`` - returns [] (extraction disabled).
|
|
559
|
+
|
|
560
|
+
Returning the browser list from one place keeps the setup wizard and the
|
|
561
|
+
steady-state path on the same policy, so neither surprises the user with an
|
|
562
|
+
unrequested Keychain prompt.
|
|
563
|
+
"""
|
|
564
|
+
silent_browsers = ["firefox", "safari"]
|
|
565
|
+
chromium_browsers = ["chrome", "brave", "edge", "vivaldi", "opera", "arc", "chromium"]
|
|
566
|
+
known_browsers = silent_browsers + chromium_browsers
|
|
567
|
+
from_browser = (config.get("FROM_BROWSER") or "").strip().lower()
|
|
568
|
+
if not from_browser:
|
|
569
|
+
return []
|
|
570
|
+
if from_browser == "off":
|
|
571
|
+
return []
|
|
572
|
+
if from_browser == "auto":
|
|
573
|
+
return silent_browsers + chromium_browsers
|
|
574
|
+
if "," in from_browser:
|
|
575
|
+
requested = [b.strip() for b in from_browser.split(",") if b.strip()]
|
|
576
|
+
resolved = [b for b in requested if b in known_browsers]
|
|
577
|
+
unknown = [b for b in requested if b not in known_browsers]
|
|
578
|
+
if unknown:
|
|
579
|
+
sys.stderr.write(
|
|
580
|
+
"[last30days] WARNING: FROM_BROWSER ignored unrecognized browser(s): "
|
|
581
|
+
f"{', '.join(unknown)} (known: {', '.join(known_browsers)})\n"
|
|
582
|
+
)
|
|
583
|
+
sys.stderr.flush()
|
|
584
|
+
return resolved
|
|
585
|
+
if from_browser in known_browsers:
|
|
586
|
+
return [from_browser]
|
|
587
|
+
# Non-empty, not off/auto, not a known browser, not a list: unrecognized.
|
|
588
|
+
# Warn rather than fail silently so a typo (FROM_BROWSER=chrme) is visible
|
|
589
|
+
# instead of looking like "no cookies found".
|
|
590
|
+
sys.stderr.write(
|
|
591
|
+
f"[last30days] WARNING: FROM_BROWSER='{from_browser}' is not a recognized "
|
|
592
|
+
f"browser; no cookies will be read (known: {', '.join(known_browsers)}, "
|
|
593
|
+
"or 'auto'/'off')\n"
|
|
594
|
+
)
|
|
595
|
+
sys.stderr.flush()
|
|
596
|
+
return []
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def extract_browser_credentials(config: dict[str, Any]) -> dict[str, str]:
|
|
601
|
+
"""Extract auth cookies from local browsers.
|
|
602
|
+
|
|
603
|
+
Browser selection (and the Chrome-prompt caveat) is handled by
|
|
604
|
+
``cookie_extraction_browsers``; this function just runs the extraction for
|
|
605
|
+
each configured cookie domain.
|
|
606
|
+
"""
|
|
607
|
+
browsers = cookie_extraction_browsers(config)
|
|
608
|
+
if not browsers:
|
|
609
|
+
return {}
|
|
610
|
+
try:
|
|
611
|
+
from . import cookie_extract
|
|
612
|
+
except ImportError:
|
|
613
|
+
return {}
|
|
614
|
+
extracted: dict[str, str] = {}
|
|
615
|
+
for _service, spec in COOKIE_DOMAINS.items():
|
|
616
|
+
if all(config.get(env_key) for env_key in spec["mapping"].values()):
|
|
617
|
+
continue
|
|
618
|
+
for browser in browsers:
|
|
619
|
+
try:
|
|
620
|
+
cookies = cookie_extract.extract_cookies(browser, spec["domain"], spec["cookies"])
|
|
621
|
+
except Exception:
|
|
622
|
+
continue
|
|
623
|
+
if cookies:
|
|
624
|
+
for cookie_name, env_key in spec["mapping"].items():
|
|
625
|
+
if cookie_name in cookies and not config.get(env_key):
|
|
626
|
+
extracted[env_key] = cookies[cookie_name]
|
|
627
|
+
break # Found cookies for this service, stop trying browsers
|
|
628
|
+
return extracted
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def get_x_source_with_method(config: dict[str, Any]) -> tuple[str | None, str]:
|
|
632
|
+
"""Return (source, method) for X search, where method describes the auth origin."""
|
|
633
|
+
if config.get("XAI_API_KEY"):
|
|
634
|
+
return "xai", "xai"
|
|
635
|
+
if config.get("AUTH_TOKEN") and config.get("CT0"):
|
|
636
|
+
method = config.get("_AUTH_TOKEN_SOURCE", "env")
|
|
637
|
+
return "bird", method
|
|
638
|
+
# Fall back to xurl CLI (official X API v2, OAuth2, free developer app)
|
|
639
|
+
from . import xurl_x
|
|
640
|
+
if xurl_x.is_available():
|
|
641
|
+
return "xurl", "oauth2"
|
|
642
|
+
return None, "none"
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def config_exists(policy: ConfigLoadPolicy | None = None) -> bool:
|
|
646
|
+
"""Check if any configuration source exists."""
|
|
647
|
+
policy = policy or ConfigLoadPolicy()
|
|
648
|
+
file_env = load_env_file(CONFIG_FILE) if CONFIG_FILE and CONFIG_FILE.exists() else {}
|
|
649
|
+
if _project_config_trusted(policy, file_env) and _find_project_env():
|
|
650
|
+
return True
|
|
651
|
+
if CONFIG_FILE:
|
|
652
|
+
return CONFIG_FILE.exists()
|
|
653
|
+
return False
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def get_reddit_source(config: dict[str, Any]) -> str | None:
|
|
657
|
+
"""Determine which Reddit backend to use.
|
|
658
|
+
|
|
659
|
+
Returns: 'scrapecreators' or None
|
|
660
|
+
"""
|
|
661
|
+
if config.get('SCRAPECREATORS_API_KEY'):
|
|
662
|
+
return 'scrapecreators'
|
|
663
|
+
return None
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
# Default X backend priority. The first available backend is the primary X
|
|
667
|
+
# source; the rest are ordered failover backups, tried only if the one before
|
|
668
|
+
# returns nothing or errors. There is one X source ("x"); these are its
|
|
669
|
+
# interchangeable backends, never run in parallel.
|
|
670
|
+
# xai — xAI/Grok live search (XAI_API_KEY)
|
|
671
|
+
# bird — X GraphQL scrape via the user's browser cookies (AUTH_TOKEN/CT0)
|
|
672
|
+
# xurl — official X API v2 (xurl CLI, OAuth2)
|
|
673
|
+
# xquik — key-based REST X search (XQUIK_API_KEY); keyless of browser cookies
|
|
674
|
+
_X_BACKEND_ORDER = ("xai", "bird", "xurl", "xquik")
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def _x_backend_available(backend: str, config: dict[str, Any], has_bird_creds: bool) -> bool:
|
|
678
|
+
if backend == 'xai':
|
|
679
|
+
return bool(config.get('XAI_API_KEY'))
|
|
680
|
+
if backend == 'bird':
|
|
681
|
+
from . import bird_x
|
|
682
|
+
return has_bird_creds and bird_x.is_bird_installed()
|
|
683
|
+
if backend == 'xurl':
|
|
684
|
+
from . import xurl_x
|
|
685
|
+
return xurl_x.is_available()
|
|
686
|
+
if backend == 'xquik':
|
|
687
|
+
return is_xquik_available(config)
|
|
688
|
+
return False
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def x_backend_chain(config: dict[str, Any]) -> list[str]:
|
|
692
|
+
"""Ordered list of available X backends.
|
|
693
|
+
|
|
694
|
+
``chain[0]`` is the default X source; the remaining entries are failover
|
|
695
|
+
backups, used only when the one before yields no items or errors. There is
|
|
696
|
+
exactly one X source — these are its backends, never fetched in parallel.
|
|
697
|
+
|
|
698
|
+
A ``LAST30DAYS_X_BACKEND`` pin forces a single backend (no failover): the
|
|
699
|
+
user explicitly chose it. Browser-cookie probing is intentionally avoided
|
|
700
|
+
(automatic Keychain access causes popups); bird counts as available only
|
|
701
|
+
when AUTH_TOKEN and CT0 are present explicitly.
|
|
702
|
+
"""
|
|
703
|
+
from . import bird_x
|
|
704
|
+
has_bird_creds = bool(config.get('AUTH_TOKEN') and config.get('CT0'))
|
|
705
|
+
if has_bird_creds:
|
|
706
|
+
bird_x.set_credentials(config.get('AUTH_TOKEN'), config.get('CT0'))
|
|
707
|
+
|
|
708
|
+
preferred = (config.get('LAST30DAYS_X_BACKEND') or '').lower()
|
|
709
|
+
if preferred in _X_BACKEND_ORDER:
|
|
710
|
+
return [preferred] if _x_backend_available(preferred, config, has_bird_creds) else []
|
|
711
|
+
|
|
712
|
+
return [b for b in _X_BACKEND_ORDER if _x_backend_available(b, config, has_bird_creds)]
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def get_x_source(config: dict[str, Any]) -> str | None:
|
|
716
|
+
"""The default (primary) X backend, or None if no X source is available.
|
|
717
|
+
|
|
718
|
+
Thin wrapper over ``x_backend_chain`` returning the first/primary backend;
|
|
719
|
+
callers that want failover should use ``x_backend_chain`` directly.
|
|
720
|
+
"""
|
|
721
|
+
chain = x_backend_chain(config)
|
|
722
|
+
return chain[0] if chain else None
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def x_pending_browser_auth(config: dict[str, Any]) -> bool:
|
|
726
|
+
"""True when X is not available now but ``FROM_BROWSER`` will authenticate it at run time.
|
|
727
|
+
|
|
728
|
+
``--diagnose`` / ``--preflight`` load config in ``plan_only`` mode, which
|
|
729
|
+
deliberately skips browser-cookie extraction (no Keychain popup,
|
|
730
|
+
``reads_values: false``). As a result ``get_x_source`` returns None and X is
|
|
731
|
+
dropped from ``available_sources`` even though a normal run would extract the
|
|
732
|
+
same cookies and authenticate X fine. This predicate reports that
|
|
733
|
+
"available pending browser auth" state without reading a single cookie — it
|
|
734
|
+
keys only on the already-resolved browser list (``cookie_extraction_browsers``
|
|
735
|
+
derives it from ``FROM_BROWSER`` alone, no secrets), bird being installed, and
|
|
736
|
+
X having a cookie-domain mapping. Side-effect free, so the safe-inspection
|
|
737
|
+
contract of diagnose/preflight is preserved.
|
|
738
|
+
|
|
739
|
+
Returns False whenever X is already available outright (static AUTH_TOKEN/CT0,
|
|
740
|
+
or xAI/xurl/xquik backend), and in ``read`` mode (a real run has already
|
|
741
|
+
extracted creds, so its status must be unchanged — never "pending").
|
|
742
|
+
"""
|
|
743
|
+
# Already available via a static backend (bird creds, xAI, xurl, xquik).
|
|
744
|
+
if get_x_source(config):
|
|
745
|
+
return False
|
|
746
|
+
# Only meaningful in inspection modes that skip extraction; a real ``read``
|
|
747
|
+
# run has already attempted extraction and must report its true state.
|
|
748
|
+
if config.get('_BROWSER_COOKIE_MODE') == 'read':
|
|
749
|
+
return False
|
|
750
|
+
if 'x' not in COOKIE_DOMAINS:
|
|
751
|
+
return False
|
|
752
|
+
if not cookie_extraction_browsers(config):
|
|
753
|
+
return False
|
|
754
|
+
from . import bird_x
|
|
755
|
+
return bird_x.is_bird_installed()
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def is_ytdlp_available() -> bool:
|
|
759
|
+
"""Check if yt-dlp is installed for YouTube search."""
|
|
760
|
+
from . import youtube_yt
|
|
761
|
+
return youtube_yt.is_ytdlp_installed()
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def is_youtube_comments_available(config: dict[str, Any]) -> bool:
|
|
765
|
+
"""Check if YouTube comment enrichment is available.
|
|
766
|
+
|
|
767
|
+
Default-on when SCRAPECREATORS_API_KEY is set — the same key-only backup
|
|
768
|
+
tier as the YouTube transcript fallback (``is_youtube_sc_available``). Cost
|
|
769
|
+
is bounded by ``enrich_with_comments(max_videos=3)`` (~3 credits per run).
|
|
770
|
+
Suppress via ``EXCLUDE_SOURCES=youtube_comments``.
|
|
771
|
+
|
|
772
|
+
Note: TikTok/Instagram comments remain explicit ``INCLUDE_SOURCES`` opt-ins
|
|
773
|
+
(see ``is_tiktok_comments_available``); only YouTube comments are default-on.
|
|
774
|
+
"""
|
|
775
|
+
if not config.get('SCRAPECREATORS_API_KEY'):
|
|
776
|
+
return False
|
|
777
|
+
return 'youtube_comments' not in _parse_exclude_sources(config)
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def is_tiktok_comments_available(config: dict[str, Any]) -> bool:
|
|
781
|
+
"""Check if TikTok comment enrichment is available.
|
|
782
|
+
|
|
783
|
+
Requires SCRAPECREATORS_API_KEY AND tiktok_comments in INCLUDE_SOURCES.
|
|
784
|
+
Mirrors the youtube_comments opt-in pattern.
|
|
785
|
+
"""
|
|
786
|
+
if not config.get('SCRAPECREATORS_API_KEY'):
|
|
787
|
+
return False
|
|
788
|
+
include = _parse_include_sources(config)
|
|
789
|
+
return 'tiktok_comments' in include
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def is_youtube_sc_available(config: dict[str, Any]) -> bool:
|
|
793
|
+
"""Check if ScrapeCreators YouTube search fallback is available.
|
|
794
|
+
|
|
795
|
+
Used when yt-dlp is not installed or fails.
|
|
796
|
+
"""
|
|
797
|
+
return bool(config.get('SCRAPECREATORS_API_KEY'))
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def is_hackernews_available() -> bool:
|
|
801
|
+
"""Check if Hacker News source is available.
|
|
802
|
+
|
|
803
|
+
Always returns True - HN uses free Algolia API, no key needed.
|
|
804
|
+
"""
|
|
805
|
+
return True
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def is_native_search(config: dict[str, Any]) -> bool:
|
|
809
|
+
"""Whether the invoking host has its own (better) native web search.
|
|
810
|
+
|
|
811
|
+
Defined by capability, not host identity: the SKILL.md agent-host path sets
|
|
812
|
+
``LAST30DAYS_NATIVE_SEARCH`` when the runtime actually has a native web-search
|
|
813
|
+
tool (e.g. Claude Code's WebSearch). When true, the engine's keyless search
|
|
814
|
+
floor is suppressed so a worse free search never preempts the model's own.
|
|
815
|
+
Defaults False (unset), so headless/cron and hosts without native search fall
|
|
816
|
+
to the keyless floor.
|
|
817
|
+
"""
|
|
818
|
+
raw = config.get('LAST30DAYS_NATIVE_SEARCH')
|
|
819
|
+
if raw is None:
|
|
820
|
+
return False
|
|
821
|
+
return str(raw).strip().lower() in ('1', 'true', 'yes', 'on')
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def keyless_web_allowed(config: dict[str, Any]) -> bool:
|
|
825
|
+
"""Whether the engine may use its keyless web-search floor for this run.
|
|
826
|
+
|
|
827
|
+
Allowed only when the host does NOT have native search. Independent of
|
|
828
|
+
whether a paid key is set (the grounding dispatcher prefers paid first and
|
|
829
|
+
falls to keyless on empty/error for non-native runs).
|
|
830
|
+
"""
|
|
831
|
+
return not is_native_search(config)
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def transcription_providers(config: dict[str, Any]) -> list[tuple[str, str]]:
|
|
835
|
+
"""Ordered (name, api_key) Whisper providers for caption-free transcription.
|
|
836
|
+
|
|
837
|
+
Groq (free tier) first, OpenAI (paid) as the backstop. Empty when neither
|
|
838
|
+
key is set, in which case transcription degrades rather than runs.
|
|
839
|
+
"""
|
|
840
|
+
providers: list[tuple[str, str]] = []
|
|
841
|
+
if config.get('GROQ_API_KEY'):
|
|
842
|
+
providers.append(('groq', config['GROQ_API_KEY']))
|
|
843
|
+
if config.get('OPENAI_API_KEY'):
|
|
844
|
+
providers.append(('openai', config['OPENAI_API_KEY']))
|
|
845
|
+
return providers
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def is_bluesky_available(config: dict[str, Any]) -> bool:
|
|
849
|
+
"""Check if Bluesky source is available.
|
|
850
|
+
|
|
851
|
+
Requires BSKY_HANDLE and BSKY_APP_PASSWORD (app password from bsky.app/settings).
|
|
852
|
+
"""
|
|
853
|
+
return bool(config.get('BSKY_HANDLE') and config.get('BSKY_APP_PASSWORD'))
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def is_truthsocial_available(config: dict[str, Any]) -> bool:
|
|
857
|
+
"""Check if Truth Social source is available.
|
|
858
|
+
|
|
859
|
+
Requires TRUTHSOCIAL_TOKEN (bearer token from browser dev tools).
|
|
860
|
+
"""
|
|
861
|
+
return bool(config.get('TRUTHSOCIAL_TOKEN'))
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def is_polymarket_available() -> bool:
|
|
865
|
+
"""Check if Polymarket source is available.
|
|
866
|
+
|
|
867
|
+
Always returns True - Gamma API is free, no key needed.
|
|
868
|
+
"""
|
|
869
|
+
return True
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def is_tiktok_available(config: dict[str, Any]) -> bool:
|
|
873
|
+
"""Check if TikTok source is available (ScrapeCreators or legacy Apify).
|
|
874
|
+
|
|
875
|
+
Returns True if SCRAPECREATORS_API_KEY or APIFY_API_TOKEN is set.
|
|
876
|
+
"""
|
|
877
|
+
return bool(config.get('SCRAPECREATORS_API_KEY') or config.get('APIFY_API_TOKEN'))
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
def get_tiktok_token(config: dict[str, Any]) -> str:
|
|
881
|
+
"""Get TikTok API token, preferring ScrapeCreators over legacy Apify."""
|
|
882
|
+
return config.get('SCRAPECREATORS_API_KEY') or config.get('APIFY_API_TOKEN') or ''
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def _parse_include_sources(config: dict[str, Any]) -> set[str]:
|
|
886
|
+
"""Parse INCLUDE_SOURCES config value into a set of lowercase source names."""
|
|
887
|
+
raw = config.get('INCLUDE_SOURCES') or ''
|
|
888
|
+
return {s.strip().lower() for s in raw.split(',') if s.strip()}
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def _parse_exclude_sources(config: dict[str, Any]) -> set[str]:
|
|
892
|
+
"""Parse EXCLUDE_SOURCES config value into a set of lowercase source names."""
|
|
893
|
+
raw = config.get('EXCLUDE_SOURCES') or ''
|
|
894
|
+
return {s.strip().lower() for s in raw.split(',') if s.strip()}
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
def is_threads_available(config: dict[str, Any]) -> bool:
|
|
898
|
+
"""Check if Threads source is available.
|
|
899
|
+
|
|
900
|
+
Returns True when SCRAPECREATORS_API_KEY is set. Threads runs alongside
|
|
901
|
+
TikTok and Instagram as part of the SC family — same key, same per-call
|
|
902
|
+
cost shape, so the same default-on rule applies. Suppress via
|
|
903
|
+
EXCLUDE_SOURCES=threads.
|
|
904
|
+
"""
|
|
905
|
+
return bool(config.get('SCRAPECREATORS_API_KEY'))
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
def is_instagram_available(config: dict[str, Any]) -> bool:
|
|
909
|
+
"""Check if Instagram source is available (ScrapeCreators).
|
|
910
|
+
|
|
911
|
+
Returns True if SCRAPECREATORS_API_KEY is set.
|
|
912
|
+
Instagram uses the same key as TikTok.
|
|
913
|
+
"""
|
|
914
|
+
return bool(config.get('SCRAPECREATORS_API_KEY'))
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
def get_instagram_token(config: dict[str, Any]) -> str:
|
|
918
|
+
"""Get Instagram API token (same ScrapeCreators key as TikTok)."""
|
|
919
|
+
return config.get('SCRAPECREATORS_API_KEY') or ''
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
def get_xiaohongshu_api_base(config: dict[str, Any]) -> str:
|
|
923
|
+
"""Get Xiaohongshu HTTP API base URL.
|
|
924
|
+
|
|
925
|
+
Defaults to host.docker.internal so OpenClaw Docker can reach host service.
|
|
926
|
+
"""
|
|
927
|
+
return (config.get('XIAOHONGSHU_API_BASE') or "http://host.docker.internal:18060").rstrip("/")
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
def is_xiaohongshu_available(config: dict[str, Any]) -> bool:
|
|
931
|
+
"""Check whether Xiaohongshu HTTP API is reachable and logged in."""
|
|
932
|
+
# Import here to avoid heavy imports at module load.
|
|
933
|
+
from . import http
|
|
934
|
+
|
|
935
|
+
base = get_xiaohongshu_api_base(config)
|
|
936
|
+
try:
|
|
937
|
+
# Keep health probe snappy, but allow one retry for transient hiccups.
|
|
938
|
+
health = http.get(f"{base}/health", timeout=3, retries=2)
|
|
939
|
+
if not isinstance(health, dict):
|
|
940
|
+
return False
|
|
941
|
+
if not health.get("success"):
|
|
942
|
+
return False
|
|
943
|
+
|
|
944
|
+
# Login probe can be slower on some deployments (browser/session checks),
|
|
945
|
+
# so use a slightly longer timeout to avoid false negatives.
|
|
946
|
+
login = http.get(f"{base}/api/v1/login/status", timeout=8, retries=2)
|
|
947
|
+
is_logged_in = (
|
|
948
|
+
login.get("data", {}).get("is_logged_in")
|
|
949
|
+
if isinstance(login, dict) else False
|
|
950
|
+
)
|
|
951
|
+
return bool(is_logged_in)
|
|
952
|
+
except (OSError, http.HTTPError):
|
|
953
|
+
return False
|
|
954
|
+
except Exception as exc:
|
|
955
|
+
sys.stderr.write(
|
|
956
|
+
f"[last30days] WARNING: unexpected error checking Xiaohongshu: "
|
|
957
|
+
f"{type(exc).__name__}: {exc}\n"
|
|
958
|
+
)
|
|
959
|
+
sys.stderr.flush()
|
|
960
|
+
return False
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
# Backward compat alias
|
|
964
|
+
is_apify_available = is_tiktok_available
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def get_x_source_status(config: dict[str, Any], probe: bool = False) -> dict[str, Any]:
|
|
968
|
+
"""Get detailed X source status for UI decisions.
|
|
969
|
+
|
|
970
|
+
Args:
|
|
971
|
+
probe: when True, run a cheap 1-tweet bird probe and downgrade
|
|
972
|
+
``bird_authenticated`` to False when X clearly returns nothing,
|
|
973
|
+
so ``--diagnose`` reflects runtime reality instead of static
|
|
974
|
+
credential presence. A transient timeout leaves the status
|
|
975
|
+
unchanged (fail open).
|
|
976
|
+
|
|
977
|
+
Returns:
|
|
978
|
+
Dict with keys: source, bird_installed, bird_authenticated,
|
|
979
|
+
bird_username, xai_available, can_install_bird
|
|
980
|
+
"""
|
|
981
|
+
from . import bird_x
|
|
982
|
+
|
|
983
|
+
if config.get('AUTH_TOKEN') and config.get('CT0'):
|
|
984
|
+
bird_x.set_credentials(config.get('AUTH_TOKEN'), config.get('CT0'))
|
|
985
|
+
bird_status = bird_x.get_bird_status()
|
|
986
|
+
xai_available = bool(config.get('XAI_API_KEY'))
|
|
987
|
+
|
|
988
|
+
# Report the TRUE auth lane (browser / env / keychain) rather than the static
|
|
989
|
+
# "env AUTH_TOKEN" label — tokens usually come from live browser cookies, and
|
|
990
|
+
# mislabeling the lane sent past debugging down a 30-minute wrong path.
|
|
991
|
+
if bird_status["authenticated"]:
|
|
992
|
+
lane = config.get('_AUTH_TOKEN_SOURCE') or 'env'
|
|
993
|
+
bird_status["username"] = f"{lane} AUTH_TOKEN"
|
|
994
|
+
|
|
995
|
+
# Optional runtime probe: don't show X green when it's effectively dead.
|
|
996
|
+
if probe and bird_status["authenticated"]:
|
|
997
|
+
if bird_x.probe_works() is False:
|
|
998
|
+
bird_status["authenticated"] = False
|
|
999
|
+
bird_status["username"] = "probe failed (no working X auth)"
|
|
1000
|
+
|
|
1001
|
+
# Xquik: the key-based X source used when bird's cookie auth isn't available.
|
|
1002
|
+
# Probe so --diagnose reports the true state — funded, or configured-but-
|
|
1003
|
+
# unpaid (402) — instead of false-green on mere key presence.
|
|
1004
|
+
xquik_available = is_xquik_available(config)
|
|
1005
|
+
xquik_working: bool | None = None
|
|
1006
|
+
xquik_status = ""
|
|
1007
|
+
if xquik_available:
|
|
1008
|
+
if probe:
|
|
1009
|
+
from . import xquik
|
|
1010
|
+
xquik_working = xquik.probe_works(get_xquik_token(config))
|
|
1011
|
+
xquik_status = xquik.probe_reason()
|
|
1012
|
+
else:
|
|
1013
|
+
xquik_status = "configured (not probed)"
|
|
1014
|
+
|
|
1015
|
+
# Determine active source. bird (browser cookies) and xAI win when present;
|
|
1016
|
+
# when neither is available, xquik is the active X source. A probe that
|
|
1017
|
+
# clearly failed (False) means xquik is not actually usable.
|
|
1018
|
+
if bird_status["authenticated"]:
|
|
1019
|
+
source = 'bird'
|
|
1020
|
+
elif xai_available:
|
|
1021
|
+
source = 'xai'
|
|
1022
|
+
else:
|
|
1023
|
+
from . import xurl_x as _xurl_check
|
|
1024
|
+
if _xurl_check.is_available():
|
|
1025
|
+
source = 'xurl'
|
|
1026
|
+
elif xquik_available and xquik_working is not False:
|
|
1027
|
+
source = 'xquik'
|
|
1028
|
+
else:
|
|
1029
|
+
source = None
|
|
1030
|
+
|
|
1031
|
+
from . import xurl_x as _xurl_x
|
|
1032
|
+
return {
|
|
1033
|
+
"source": source,
|
|
1034
|
+
"bird_installed": bird_status["installed"],
|
|
1035
|
+
"bird_authenticated": bird_status["authenticated"],
|
|
1036
|
+
"bird_username": bird_status["username"],
|
|
1037
|
+
"xai_available": xai_available,
|
|
1038
|
+
"xurl_available": _xurl_x.is_available(),
|
|
1039
|
+
"xquik_available": xquik_available,
|
|
1040
|
+
"xquik_working": xquik_working,
|
|
1041
|
+
"xquik_status": xquik_status,
|
|
1042
|
+
"can_install_bird": bird_status["can_install"],
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
# Pinterest
|
|
1047
|
+
def is_pinterest_available(config: dict[str, Any]) -> bool:
|
|
1048
|
+
"""Check if Pinterest source is available.
|
|
1049
|
+
|
|
1050
|
+
Returns True when SCRAPECREATORS_API_KEY is set AND 'pinterest' is in
|
|
1051
|
+
INCLUDE_SOURCES (or requested_sources at the pipeline level). Pinterest
|
|
1052
|
+
is opt-in because not every topic benefits from visual pin results.
|
|
1053
|
+
"""
|
|
1054
|
+
return bool(config.get('SCRAPECREATORS_API_KEY'))
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
def get_pinterest_token(config: dict[str, Any]) -> str:
|
|
1058
|
+
"""Get Pinterest API token (same ScrapeCreators key as TikTok/Instagram)."""
|
|
1059
|
+
return config.get('SCRAPECREATORS_API_KEY') or ''
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
# Xquik
|
|
1063
|
+
def is_xquik_available(config: dict[str, Any]) -> bool:
|
|
1064
|
+
"""Check if Xquik X search source is available.
|
|
1065
|
+
|
|
1066
|
+
Requires XQUIK_API_KEY (API key from xquik.com).
|
|
1067
|
+
"""
|
|
1068
|
+
return bool(config.get('XQUIK_API_KEY'))
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
def get_xquik_token(config: dict[str, Any]) -> str:
|
|
1072
|
+
"""Get Xquik API key."""
|
|
1073
|
+
return config.get('XQUIK_API_KEY') or ''
|