luckiest-co 1.0.1 → 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 +6 -4
- package/commands/{luckiest/finish.md → finish.md} +2 -0
- package/commands/go.md +53 -0
- package/commands/plan.md +66 -0
- package/package.json +3 -2
- 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,843 @@
|
|
|
1
|
+
"""First-run setup wizard for last30days.
|
|
2
|
+
|
|
3
|
+
Detects first run, performs auto-setup (cookie extraction + yt-dlp check),
|
|
4
|
+
and writes configuration. The actual wizard UI is SKILL.md-driven (the LLM
|
|
5
|
+
presents it), but this module provides the detection and setup actions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, Optional, Tuple
|
|
16
|
+
from urllib.error import HTTPError, URLError
|
|
17
|
+
from urllib.request import Request, urlopen
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def is_first_run(config: Dict[str, Any]) -> bool:
|
|
23
|
+
"""Return True if the setup wizard has not been completed.
|
|
24
|
+
|
|
25
|
+
Checks for SETUP_COMPLETE in the config dict. If it's not set
|
|
26
|
+
(None or empty string), the user hasn't gone through setup yet.
|
|
27
|
+
"""
|
|
28
|
+
return not config.get("SETUP_COMPLETE")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def run_auto_setup(config: Dict[str, Any], *, allow_browser_cookies: bool = False) -> Dict[str, Any]:
|
|
32
|
+
"""Perform the auto-setup actions.
|
|
33
|
+
|
|
34
|
+
- Optionally runs cookie extraction for all registered domains, trying the
|
|
35
|
+
browsers from ``env.cookie_extraction_browsers()``. Browser reads are off
|
|
36
|
+
unless ``allow_browser_cookies`` is true.
|
|
37
|
+
- Checks if yt-dlp is installed
|
|
38
|
+
- Best-effort install of digg-pp-cli (Printing Press library)
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Dict with keys:
|
|
42
|
+
cookies_found: {source_name: browser_name} for each source where cookies were found
|
|
43
|
+
ytdlp_installed: bool
|
|
44
|
+
ytdlp_action: already_installed | installed | install_failed | no_homebrew
|
|
45
|
+
digg_installed: bool (True when the engine can resolve digg-pp-cli on PATH)
|
|
46
|
+
digg_action: already_installed | installed | installed_off_path | install_failed | no_npx
|
|
47
|
+
env_written: bool (always False here — caller writes config separately)
|
|
48
|
+
ytdlp_stderr: present when ytdlp_action is install_failed
|
|
49
|
+
digg_stderr: present when digg_action is install_failed
|
|
50
|
+
digg_path: present when digg_action is installed_off_path (binary on disk, not on PATH)
|
|
51
|
+
"""
|
|
52
|
+
from .env import COOKIE_DOMAINS, cookie_extraction_browsers
|
|
53
|
+
|
|
54
|
+
cookies_found: Dict[str, str] = {}
|
|
55
|
+
|
|
56
|
+
if allow_browser_cookies:
|
|
57
|
+
from . import cookie_extract
|
|
58
|
+
|
|
59
|
+
cookie_config = dict(config)
|
|
60
|
+
if not (cookie_config.get("FROM_BROWSER") or "").strip():
|
|
61
|
+
cookie_config["FROM_BROWSER"] = "firefox,safari"
|
|
62
|
+
browsers = cookie_extraction_browsers(cookie_config)
|
|
63
|
+
|
|
64
|
+
for source_name, spec in COOKIE_DOMAINS.items():
|
|
65
|
+
domain = spec["domain"]
|
|
66
|
+
cookie_names = spec["cookies"]
|
|
67
|
+
|
|
68
|
+
for browser in browsers:
|
|
69
|
+
try:
|
|
70
|
+
result = cookie_extract.extract_cookies_with_source(browser, domain, cookie_names)
|
|
71
|
+
except Exception as exc:
|
|
72
|
+
logger.debug("Cookie extraction failed for %s via %s: %s", source_name, browser, exc)
|
|
73
|
+
continue
|
|
74
|
+
if result is not None and result[0]:
|
|
75
|
+
cookies_found[source_name] = result[1]
|
|
76
|
+
break # Found cookies for this service, stop trying browsers
|
|
77
|
+
|
|
78
|
+
# Check yt-dlp availability and install via Homebrew if missing
|
|
79
|
+
ytdlp_action: str
|
|
80
|
+
if shutil.which("yt-dlp") is not None:
|
|
81
|
+
ytdlp_installed = True
|
|
82
|
+
ytdlp_action = "already_installed"
|
|
83
|
+
elif shutil.which("brew") is not None:
|
|
84
|
+
brew_stderr = ""
|
|
85
|
+
try:
|
|
86
|
+
proc = subprocess.run(
|
|
87
|
+
["brew", "install", "yt-dlp"],
|
|
88
|
+
capture_output=True, text=True, timeout=120,
|
|
89
|
+
)
|
|
90
|
+
if proc.returncode == 0:
|
|
91
|
+
ytdlp_installed = True
|
|
92
|
+
ytdlp_action = "installed"
|
|
93
|
+
else:
|
|
94
|
+
ytdlp_installed = False
|
|
95
|
+
ytdlp_action = "install_failed"
|
|
96
|
+
brew_stderr = proc.stderr
|
|
97
|
+
logger.warning("brew install yt-dlp failed: %s", proc.stderr)
|
|
98
|
+
except Exception as exc:
|
|
99
|
+
ytdlp_installed = False
|
|
100
|
+
ytdlp_action = "install_failed"
|
|
101
|
+
brew_stderr = str(exc)
|
|
102
|
+
logger.warning("brew install yt-dlp exception: %s", exc)
|
|
103
|
+
else:
|
|
104
|
+
ytdlp_installed = False
|
|
105
|
+
ytdlp_action = "no_homebrew"
|
|
106
|
+
|
|
107
|
+
digg_installed, digg_action, digg_stderr, digg_path = _install_digg_cli()
|
|
108
|
+
pp_sources = install_default_pp_sources()
|
|
109
|
+
|
|
110
|
+
results: Dict[str, Any] = {
|
|
111
|
+
"cookies_found": cookies_found,
|
|
112
|
+
"ytdlp_installed": ytdlp_installed,
|
|
113
|
+
"ytdlp_action": ytdlp_action,
|
|
114
|
+
"digg_installed": digg_installed,
|
|
115
|
+
"digg_action": digg_action,
|
|
116
|
+
# Per-CLI status for the additional default-on Printing Press sources
|
|
117
|
+
# (arxiv, techmeme, trustpilot): {source: {installed, action, ...}}.
|
|
118
|
+
"pp_sources": pp_sources,
|
|
119
|
+
"env_written": False,
|
|
120
|
+
}
|
|
121
|
+
if ytdlp_action == "install_failed":
|
|
122
|
+
results["ytdlp_stderr"] = brew_stderr
|
|
123
|
+
if digg_action == "install_failed":
|
|
124
|
+
results["digg_stderr"] = digg_stderr
|
|
125
|
+
if digg_path:
|
|
126
|
+
results["digg_path"] = digg_path
|
|
127
|
+
return results
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# Generous timeout: the install shells out to `npx`, which may download the
|
|
131
|
+
# Printing Press package and build the Go binary over the network.
|
|
132
|
+
DIGG_INSTALL_TIMEOUT = 300
|
|
133
|
+
DIGG_CLI_BIN = "digg-pp-cli"
|
|
134
|
+
# Pin the catalog installer; matches printing-press-library npm 0.1.16 default
|
|
135
|
+
# ($HOME/.local/bin on macOS/Linux).
|
|
136
|
+
PRINTING_PRESS_NPM = "@mvanhorn/printing-press-library@0.1.16"
|
|
137
|
+
DIGG_INSTALL_CMD = f"npx -y {PRINTING_PRESS_NPM} install digg --cli-only"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _digg_bin_candidate_paths() -> list[Path]:
|
|
141
|
+
"""Known install locations for digg-pp-cli (Printing Press library defaults).
|
|
142
|
+
|
|
143
|
+
Order: current installer default (~/.local/bin), legacy Go bins, Windows
|
|
144
|
+
managed dir. ``pipeline.available_sources()`` only activates Digg when
|
|
145
|
+
``shutil.which`` resolves on PATH — probing these dirs is for setup
|
|
146
|
+
verification and honest off-PATH messaging, not engine activation.
|
|
147
|
+
"""
|
|
148
|
+
home = Path.home()
|
|
149
|
+
candidates: list[Path] = [home / ".local" / "bin" / DIGG_CLI_BIN]
|
|
150
|
+
gopath = os.environ.get("GOPATH")
|
|
151
|
+
if gopath:
|
|
152
|
+
candidates.append(Path(gopath) / "bin" / DIGG_CLI_BIN)
|
|
153
|
+
candidates.append(home / "go" / "bin" / DIGG_CLI_BIN)
|
|
154
|
+
if os.name == "nt":
|
|
155
|
+
local_app = os.environ.get("LOCALAPPDATA") or os.environ.get("LocalAppData")
|
|
156
|
+
if local_app:
|
|
157
|
+
candidates.append(
|
|
158
|
+
Path(local_app) / "Programs" / "PrintingPress" / "bin" / f"{DIGG_CLI_BIN}.exe"
|
|
159
|
+
)
|
|
160
|
+
return candidates
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _digg_on_path() -> Optional[str]:
|
|
164
|
+
"""Return digg-pp-cli when the engine would activate Digg (PATH-resolvable)."""
|
|
165
|
+
return shutil.which(DIGG_CLI_BIN)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _digg_off_path_binary() -> Optional[str]:
|
|
169
|
+
"""Return digg-pp-cli path from known install dirs when not on PATH."""
|
|
170
|
+
for candidate in _digg_bin_candidate_paths():
|
|
171
|
+
if candidate.is_file() and os.access(candidate, os.X_OK):
|
|
172
|
+
return str(candidate)
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _digg_bin_dir_hint(digg_path: str) -> str:
|
|
177
|
+
"""Return a copy-pasteable PATH directory for the given binary path."""
|
|
178
|
+
parent = os.path.dirname(os.path.expanduser(digg_path))
|
|
179
|
+
if os.name == "nt":
|
|
180
|
+
# Windows PATH edits use absolute dirs; $HOME is a Unix shell convention.
|
|
181
|
+
return parent
|
|
182
|
+
home = str(Path.home())
|
|
183
|
+
if parent == home:
|
|
184
|
+
return "$HOME"
|
|
185
|
+
prefix = home + os.sep
|
|
186
|
+
if parent.startswith(prefix):
|
|
187
|
+
rel = parent[len(prefix):].replace(os.sep, "/")
|
|
188
|
+
return f"$HOME/{rel}" if rel else "$HOME"
|
|
189
|
+
return parent
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _install_digg_cli() -> Tuple[bool, str, str, str]:
|
|
193
|
+
"""Best-effort install of the digg-pp-cli binary.
|
|
194
|
+
|
|
195
|
+
Mirrors the yt-dlp/brew auto-install: it never raises, and degrades to a
|
|
196
|
+
recommend-only outcome when the installer is unavailable. Uses
|
|
197
|
+
``@mvanhorn/printing-press-library`` (``--cli-only``) — the same catalog
|
|
198
|
+
installer as pp-digg; Hermes/OpenClaw skill wiring is irrelevant here.
|
|
199
|
+
|
|
200
|
+
Returns ``(engine_active, action, stderr, off_path_binary)`` where
|
|
201
|
+
``engine_active`` is True only when ``shutil.which`` resolves the binary
|
|
202
|
+
(matching ``pipeline.available_sources()``). ``action`` is one of:
|
|
203
|
+
already_installed | installed | installed_off_path | install_failed | no_npx
|
|
204
|
+
``stderr`` is populated on ``install_failed``. ``off_path_binary`` is set
|
|
205
|
+
when the binary exists on disk but is not PATH-visible to this process.
|
|
206
|
+
"""
|
|
207
|
+
on_path = _digg_on_path()
|
|
208
|
+
if on_path:
|
|
209
|
+
return True, "already_installed", "", ""
|
|
210
|
+
off_path = _digg_off_path_binary()
|
|
211
|
+
if off_path:
|
|
212
|
+
return False, "installed_off_path", "", off_path
|
|
213
|
+
if shutil.which("npx") is None:
|
|
214
|
+
return False, "no_npx", "", ""
|
|
215
|
+
try:
|
|
216
|
+
proc = subprocess.run(
|
|
217
|
+
["npx", "-y", PRINTING_PRESS_NPM, "install", "digg", "--cli-only"],
|
|
218
|
+
capture_output=True, text=True, timeout=DIGG_INSTALL_TIMEOUT,
|
|
219
|
+
)
|
|
220
|
+
except Exception as exc:
|
|
221
|
+
logger.warning("npx install digg exception: %s", exc)
|
|
222
|
+
return False, "install_failed", str(exc), ""
|
|
223
|
+
if proc.returncode != 0:
|
|
224
|
+
stderr = proc.stderr or f"npx install digg exited {proc.returncode}"
|
|
225
|
+
logger.warning("npx install digg failed (rc=%s): %s", proc.returncode, stderr)
|
|
226
|
+
return False, "install_failed", stderr, ""
|
|
227
|
+
on_path = _digg_on_path()
|
|
228
|
+
if on_path:
|
|
229
|
+
return True, "installed", "", ""
|
|
230
|
+
off_path = _digg_off_path_binary()
|
|
231
|
+
if off_path:
|
|
232
|
+
combined = (proc.stderr or "").strip()
|
|
233
|
+
if combined:
|
|
234
|
+
logger.warning("digg-pp-cli installed off PATH: %s", combined)
|
|
235
|
+
return False, "installed_off_path", combined, off_path
|
|
236
|
+
stderr = proc.stderr or "install completed but digg-pp-cli was not found"
|
|
237
|
+
logger.warning("npx install digg failed verification: %s", stderr)
|
|
238
|
+
return False, "install_failed", stderr, ""
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# Additional default-on Printing Press sources installed the same way as Digg:
|
|
242
|
+
# (engine source key, slug for `install <slug>`, binary name). These activate in
|
|
243
|
+
# ``pipeline.available_sources()`` when ``shutil.which`` resolves the binary.
|
|
244
|
+
# Trustpilot is intentionally NOT here: it is opt-in (INCLUDE_SOURCES=trustpilot)
|
|
245
|
+
# because of its headless-Chrome cookie harvest, so auto-installing its binary
|
|
246
|
+
# for a source that stays off by default would be wasted work. Opting in installs
|
|
247
|
+
# it on demand via `npx ... install trustpilot --cli-only` (see CONFIGURATION.md).
|
|
248
|
+
PP_DEFAULT_SOURCES: list[tuple[str, str, str]] = [
|
|
249
|
+
("arxiv", "arxiv", "arxiv-pp-cli"),
|
|
250
|
+
("techmeme", "techmeme", "techmeme-pp-cli"),
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _pp_bin_candidate_paths(bin_name: str) -> list[Path]:
|
|
255
|
+
"""Known install locations for a Printing Press CLI binary (slug-parameterized
|
|
256
|
+
mirror of ``_digg_bin_candidate_paths``)."""
|
|
257
|
+
home = Path.home()
|
|
258
|
+
candidates: list[Path] = [home / ".local" / "bin" / bin_name]
|
|
259
|
+
gopath = os.environ.get("GOPATH")
|
|
260
|
+
if gopath:
|
|
261
|
+
candidates.append(Path(gopath) / "bin" / bin_name)
|
|
262
|
+
candidates.append(home / "go" / "bin" / bin_name)
|
|
263
|
+
if os.name == "nt":
|
|
264
|
+
local_app = os.environ.get("LOCALAPPDATA") or os.environ.get("LocalAppData")
|
|
265
|
+
if local_app:
|
|
266
|
+
candidates.append(
|
|
267
|
+
Path(local_app) / "Programs" / "PrintingPress" / "bin" / f"{bin_name}.exe"
|
|
268
|
+
)
|
|
269
|
+
return candidates
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _pp_off_path_binary(bin_name: str) -> Optional[str]:
|
|
273
|
+
for candidate in _pp_bin_candidate_paths(bin_name):
|
|
274
|
+
if candidate.is_file() and os.access(candidate, os.X_OK):
|
|
275
|
+
return str(candidate)
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _install_pp_cli(slug: str, bin_name: str) -> Tuple[bool, str, str, str]:
|
|
280
|
+
"""Best-effort install of a Printing Press CLI binary.
|
|
281
|
+
|
|
282
|
+
Slug-parameterized mirror of ``_install_digg_cli``: never raises, degrades
|
|
283
|
+
to recommend-only when the installer is unavailable. Returns
|
|
284
|
+
``(engine_active, action, stderr, off_path_binary)`` with the same action
|
|
285
|
+
taxonomy: already_installed | installed | installed_off_path |
|
|
286
|
+
install_failed | no_npx.
|
|
287
|
+
"""
|
|
288
|
+
on_path = shutil.which(bin_name)
|
|
289
|
+
if on_path:
|
|
290
|
+
return True, "already_installed", "", ""
|
|
291
|
+
off_path = _pp_off_path_binary(bin_name)
|
|
292
|
+
if off_path:
|
|
293
|
+
return False, "installed_off_path", "", off_path
|
|
294
|
+
if shutil.which("npx") is None:
|
|
295
|
+
return False, "no_npx", "", ""
|
|
296
|
+
try:
|
|
297
|
+
proc = subprocess.run(
|
|
298
|
+
["npx", "-y", PRINTING_PRESS_NPM, "install", slug, "--cli-only"],
|
|
299
|
+
capture_output=True, text=True, timeout=DIGG_INSTALL_TIMEOUT,
|
|
300
|
+
)
|
|
301
|
+
except Exception as exc:
|
|
302
|
+
logger.warning("npx install %s exception: %s", slug, exc)
|
|
303
|
+
return False, "install_failed", str(exc), ""
|
|
304
|
+
if proc.returncode != 0:
|
|
305
|
+
stderr = proc.stderr or f"npx install {slug} exited {proc.returncode}"
|
|
306
|
+
logger.warning("npx install %s failed (rc=%s): %s", slug, proc.returncode, stderr)
|
|
307
|
+
return False, "install_failed", stderr, ""
|
|
308
|
+
on_path = shutil.which(bin_name)
|
|
309
|
+
if on_path:
|
|
310
|
+
return True, "installed", "", ""
|
|
311
|
+
off_path = _pp_off_path_binary(bin_name)
|
|
312
|
+
if off_path:
|
|
313
|
+
combined = (proc.stderr or "").strip()
|
|
314
|
+
if combined:
|
|
315
|
+
logger.warning("%s installed off PATH: %s", bin_name, combined)
|
|
316
|
+
return False, "installed_off_path", combined, off_path
|
|
317
|
+
stderr = proc.stderr or f"install completed but {bin_name} was not found"
|
|
318
|
+
logger.warning("npx install %s failed verification: %s", slug, stderr)
|
|
319
|
+
return False, "install_failed", stderr, ""
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def install_default_pp_sources() -> Dict[str, Dict[str, Any]]:
|
|
323
|
+
"""Best-effort install of every additional default-on Printing Press source.
|
|
324
|
+
|
|
325
|
+
Returns ``{source_key: {installed, action, stderr?, path?}}`` so the wizard
|
|
326
|
+
can report per-CLI status alongside Digg without raising on any single
|
|
327
|
+
failure.
|
|
328
|
+
"""
|
|
329
|
+
out: Dict[str, Dict[str, Any]] = {}
|
|
330
|
+
for source_key, slug, bin_name in PP_DEFAULT_SOURCES:
|
|
331
|
+
installed, action, stderr, off_path = _install_pp_cli(slug, bin_name)
|
|
332
|
+
entry: Dict[str, Any] = {"installed": installed, "action": action}
|
|
333
|
+
if action == "install_failed" and stderr:
|
|
334
|
+
entry["stderr"] = stderr
|
|
335
|
+
if off_path:
|
|
336
|
+
entry["path"] = off_path
|
|
337
|
+
out[source_key] = entry
|
|
338
|
+
return out
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _open_secret_append(path: Path):
|
|
342
|
+
"""Open ``path`` for appending as a 0o600 secret file with no readable window.
|
|
343
|
+
|
|
344
|
+
``os.open`` with ``O_CREAT|O_WRONLY|O_APPEND`` and mode ``0o600`` sets
|
|
345
|
+
restrictive permissions at creation (umask can only further restrict, never
|
|
346
|
+
widen, so the file is never world-readable even transiently). An explicit
|
|
347
|
+
``chmod`` afterwards also tightens a pre-existing loose file. This matters
|
|
348
|
+
because the .env stores API keys, cookies, and tokens.
|
|
349
|
+
"""
|
|
350
|
+
fd = os.open(path, os.O_CREAT | os.O_WRONLY | os.O_APPEND, 0o600)
|
|
351
|
+
try:
|
|
352
|
+
os.chmod(path, 0o600)
|
|
353
|
+
except OSError:
|
|
354
|
+
pass
|
|
355
|
+
return os.fdopen(fd, "a", encoding="utf-8")
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _format_env_value(value: str) -> str:
|
|
359
|
+
"""Quote a value so it round-trips through env.load_env_file.
|
|
360
|
+
|
|
361
|
+
env.load_env_file strips a single layer of matching surrounding quotes but
|
|
362
|
+
does NOT process backslash escapes, so we wrap (never escape):
|
|
363
|
+
- plain tokens (no whitespace, no leading quote): returned unchanged;
|
|
364
|
+
- values with whitespace/leading quote and no double-quote: double-quoted;
|
|
365
|
+
- values containing a double-quote but no single-quote: single-quoted;
|
|
366
|
+
- values containing both quote types: returned as-is (best effort; no
|
|
367
|
+
wrapper round-trips through the loader, and tokens never hit this).
|
|
368
|
+
Newlines are not valid in a single-line env value and are stripped.
|
|
369
|
+
"""
|
|
370
|
+
value = value.replace("\r", "").replace("\n", " ")
|
|
371
|
+
needs_quoting = (not value) or value[0] in ("'", '"') or any(c.isspace() for c in value)
|
|
372
|
+
if not needs_quoting:
|
|
373
|
+
return value
|
|
374
|
+
if '"' not in value:
|
|
375
|
+
return f'"{value}"'
|
|
376
|
+
if "'" not in value:
|
|
377
|
+
return f"'{value}'"
|
|
378
|
+
return value
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def write_setup_config(env_path: Path, from_browser: str | None = None) -> bool:
|
|
382
|
+
"""Write SETUP_COMPLETE and FROM_BROWSER to the .env file.
|
|
383
|
+
|
|
384
|
+
Creates the file and parent directories if needed.
|
|
385
|
+
Appends to existing file without overwriting existing keys.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
env_path: Path to the .env file (e.g. ~/.config/last30days/.env)
|
|
389
|
+
from_browser: Browser extraction mode to persist. Pass the browser that
|
|
390
|
+
actually yielded cookies (e.g. "firefox") to fast-path future runs.
|
|
391
|
+
Pass None (default) to NOT pin FROM_BROWSER — the steady-state
|
|
392
|
+
default (Firefox/Safari, no Keychain prompt) then applies. We avoid
|
|
393
|
+
persisting "auto" because it makes every later run probe Chrome and
|
|
394
|
+
re-trigger the Keychain prompt.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
True if config was written successfully, False on error.
|
|
398
|
+
"""
|
|
399
|
+
try:
|
|
400
|
+
env_path = Path(env_path)
|
|
401
|
+
env_path.parent.mkdir(parents=True, exist_ok=True)
|
|
402
|
+
|
|
403
|
+
# Read existing content to avoid overwriting keys
|
|
404
|
+
existing_keys: set = set()
|
|
405
|
+
existing_content = ""
|
|
406
|
+
if env_path.exists():
|
|
407
|
+
existing_content = env_path.read_text(encoding="utf-8")
|
|
408
|
+
for line in existing_content.splitlines():
|
|
409
|
+
stripped = line.strip()
|
|
410
|
+
if stripped and not stripped.startswith("#") and "=" in stripped:
|
|
411
|
+
key = stripped.split("=", 1)[0].strip()
|
|
412
|
+
existing_keys.add(key)
|
|
413
|
+
|
|
414
|
+
lines_to_add = []
|
|
415
|
+
if "SETUP_COMPLETE" not in existing_keys:
|
|
416
|
+
lines_to_add.append("SETUP_COMPLETE=true")
|
|
417
|
+
if from_browser and "FROM_BROWSER" not in existing_keys:
|
|
418
|
+
lines_to_add.append(f"FROM_BROWSER={_format_env_value(from_browser)}")
|
|
419
|
+
|
|
420
|
+
if not lines_to_add:
|
|
421
|
+
return True # Nothing to write, already configured
|
|
422
|
+
|
|
423
|
+
# Create/append as a 0o600 secret file: the .env holds tokens and keys,
|
|
424
|
+
# so it must never be created world-readable.
|
|
425
|
+
with _open_secret_append(env_path) as f:
|
|
426
|
+
if existing_content and not existing_content.endswith("\n"):
|
|
427
|
+
f.write("\n")
|
|
428
|
+
f.write("\n".join(lines_to_add) + "\n")
|
|
429
|
+
|
|
430
|
+
return True
|
|
431
|
+
|
|
432
|
+
except OSError as exc:
|
|
433
|
+
logger.error("Failed to write setup config to %s: %s", env_path, exc)
|
|
434
|
+
return False
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def write_api_key(env_path: Path, api_key: str, key_name: str = "SCRAPECREATORS_API_KEY") -> bool:
|
|
438
|
+
"""Append an API key to the .env file as a 0o600 secret.
|
|
439
|
+
|
|
440
|
+
Reuses the same secret-safe write path as ``write_setup_config`` so the
|
|
441
|
+
value lands with restrictive permissions and round-trips through
|
|
442
|
+
``env.load_env_file``. Idempotent: if ``key_name`` is already present in
|
|
443
|
+
the file, nothing is written and the existing value is preserved (we never
|
|
444
|
+
clobber a key the user may have set by hand).
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
env_path: Path to the .env file (e.g. ~/.config/last30days/.env).
|
|
448
|
+
api_key: The raw key value to persist.
|
|
449
|
+
key_name: The env var name to write (default SCRAPECREATORS_API_KEY).
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
True if the key was written or already present, False on error or when
|
|
453
|
+
``api_key`` is empty.
|
|
454
|
+
"""
|
|
455
|
+
if not api_key:
|
|
456
|
+
return False
|
|
457
|
+
try:
|
|
458
|
+
env_path = Path(env_path)
|
|
459
|
+
env_path.parent.mkdir(parents=True, exist_ok=True)
|
|
460
|
+
|
|
461
|
+
existing_content = ""
|
|
462
|
+
if env_path.exists():
|
|
463
|
+
existing_content = env_path.read_text(encoding="utf-8")
|
|
464
|
+
for line in existing_content.splitlines():
|
|
465
|
+
stripped = line.strip()
|
|
466
|
+
if stripped and not stripped.startswith("#") and "=" in stripped:
|
|
467
|
+
if stripped.split("=", 1)[0].strip() == key_name:
|
|
468
|
+
return True # Already configured; do not duplicate
|
|
469
|
+
|
|
470
|
+
line = f"{key_name}={_format_env_value(api_key)}\n"
|
|
471
|
+
with _open_secret_append(env_path) as f:
|
|
472
|
+
if existing_content and not existing_content.endswith("\n"):
|
|
473
|
+
f.write("\n")
|
|
474
|
+
f.write(line)
|
|
475
|
+
|
|
476
|
+
return True
|
|
477
|
+
|
|
478
|
+
except OSError as exc:
|
|
479
|
+
logger.error("Failed to write API key to %s: %s", env_path, exc)
|
|
480
|
+
return False
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def mask_api_key(api_key: str) -> str:
|
|
484
|
+
"""Return a non-secret display form of an API key (prefix + last 4).
|
|
485
|
+
|
|
486
|
+
Used so the key never appears verbatim in stdout the host model captures.
|
|
487
|
+
Short or empty keys collapse to a fixed placeholder.
|
|
488
|
+
"""
|
|
489
|
+
if not api_key or len(api_key) <= 8:
|
|
490
|
+
return "sc_…"
|
|
491
|
+
return f"{api_key[:3]}…{api_key[-4:]}"
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def get_setup_status_text(results: Dict[str, Any]) -> str:
|
|
495
|
+
"""Return a human-readable summary of auto-setup results.
|
|
496
|
+
|
|
497
|
+
Args:
|
|
498
|
+
results: Dict from run_auto_setup()
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
Multi-line status text.
|
|
502
|
+
"""
|
|
503
|
+
lines = []
|
|
504
|
+
lines.append("Setup complete! Here's what I found:")
|
|
505
|
+
lines.append("")
|
|
506
|
+
|
|
507
|
+
cookies_found = results.get("cookies_found", {})
|
|
508
|
+
if cookies_found:
|
|
509
|
+
for source, browser in cookies_found.items():
|
|
510
|
+
lines.append(f" - {source.upper()} cookies found in {browser}")
|
|
511
|
+
else:
|
|
512
|
+
lines.append(" - No browser cookies found for X/Twitter")
|
|
513
|
+
|
|
514
|
+
ytdlp_action = results.get("ytdlp_action", "")
|
|
515
|
+
if ytdlp_action == "installed":
|
|
516
|
+
lines.append(" - Installed yt-dlp via Homebrew")
|
|
517
|
+
elif ytdlp_action == "install_failed":
|
|
518
|
+
lines.append(" - yt-dlp install failed \u2014 run `brew install yt-dlp` manually")
|
|
519
|
+
elif ytdlp_action == "no_homebrew":
|
|
520
|
+
lines.append(" - yt-dlp not found. Install Homebrew first, then: brew install yt-dlp")
|
|
521
|
+
elif ytdlp_action == "already_installed":
|
|
522
|
+
lines.append(" - yt-dlp already installed")
|
|
523
|
+
elif results.get("ytdlp_installed", False):
|
|
524
|
+
lines.append(" - yt-dlp is installed (YouTube search ready)")
|
|
525
|
+
else:
|
|
526
|
+
lines.append(" - yt-dlp not found (install with: brew install yt-dlp)")
|
|
527
|
+
|
|
528
|
+
digg_action = results.get("digg_action", "")
|
|
529
|
+
if digg_action == "installed":
|
|
530
|
+
lines.append(" - Installed Digg CLI (free AI-news clusters source now active)")
|
|
531
|
+
elif digg_action == "already_installed":
|
|
532
|
+
lines.append(" - Digg CLI already installed (AI-news clusters active)")
|
|
533
|
+
elif digg_action == "installed_off_path":
|
|
534
|
+
digg_path = results.get("digg_path", "")
|
|
535
|
+
if digg_path:
|
|
536
|
+
bin_dir = _digg_bin_dir_hint(digg_path)
|
|
537
|
+
lines.append(
|
|
538
|
+
f" - Digg CLI found at {digg_path} but not on PATH — add "
|
|
539
|
+
f"{bin_dir} to PATH and restart your agent session/gateway "
|
|
540
|
+
"for Digg to activate"
|
|
541
|
+
)
|
|
542
|
+
else:
|
|
543
|
+
lines.append(
|
|
544
|
+
" - Digg CLI is installed but not on PATH — add its install "
|
|
545
|
+
"directory to PATH and restart your agent session/gateway for "
|
|
546
|
+
"Digg to activate"
|
|
547
|
+
)
|
|
548
|
+
elif digg_action == "install_failed":
|
|
549
|
+
lines.append(f" - Digg CLI install failed — run `{DIGG_INSTALL_CMD}` manually")
|
|
550
|
+
elif digg_action == "no_npx":
|
|
551
|
+
lines.append(
|
|
552
|
+
" - Digg CLI not installed (free, optional). Install Node/npx, then: "
|
|
553
|
+
f"{DIGG_INSTALL_CMD}"
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
env_written = results.get("env_written", False)
|
|
557
|
+
if env_written:
|
|
558
|
+
lines.append("")
|
|
559
|
+
lines.append("Configuration saved. Future runs will auto-detect your browsers.")
|
|
560
|
+
|
|
561
|
+
return "\n".join(lines)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
# ---------------------------------------------------------------------------
|
|
565
|
+
# OpenClaw server-side setup (no browser, JSON output)
|
|
566
|
+
# ---------------------------------------------------------------------------
|
|
567
|
+
|
|
568
|
+
_OPENCLAW_KEY_NAMES = [
|
|
569
|
+
"SCRAPECREATORS_API_KEY",
|
|
570
|
+
"XAI_API_KEY",
|
|
571
|
+
"BRAVE_API_KEY",
|
|
572
|
+
"EXA_API_KEY",
|
|
573
|
+
"SERPER_API_KEY",
|
|
574
|
+
"OPENAI_API_KEY",
|
|
575
|
+
"AUTH_TOKEN",
|
|
576
|
+
]
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def run_openclaw_setup(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
580
|
+
"""Server-side setup probe: no cookies, tool + key availability, Digg CLI.
|
|
581
|
+
|
|
582
|
+
Best-effort installs digg-pp-cli when npx is available (same as desktop
|
|
583
|
+
``run_auto_setup``). Returns a dict suitable for JSON output to stdout so
|
|
584
|
+
that SKILL.md can present appropriate options to the user.
|
|
585
|
+
"""
|
|
586
|
+
yt_dlp = shutil.which("yt-dlp") is not None
|
|
587
|
+
node = shutil.which("node") is not None
|
|
588
|
+
python3 = shutil.which("python3") is not None
|
|
589
|
+
|
|
590
|
+
digg_installed, digg_action, digg_stderr, digg_path = _install_digg_cli()
|
|
591
|
+
|
|
592
|
+
keys: Dict[str, bool] = {}
|
|
593
|
+
for key_name in _OPENCLAW_KEY_NAMES:
|
|
594
|
+
short = key_name.lower().replace("_api_key", "").replace("_key", "").replace("_token", "")
|
|
595
|
+
# Normalize: AUTH_TOKEN -> auth, SCRAPECREATORS_API_KEY -> scrapecreators
|
|
596
|
+
keys[short] = bool(config.get(key_name))
|
|
597
|
+
|
|
598
|
+
# Determine x_method
|
|
599
|
+
if config.get("XAI_API_KEY"):
|
|
600
|
+
x_method: Optional[str] = "xai"
|
|
601
|
+
elif config.get("AUTH_TOKEN") and config.get("CT0"):
|
|
602
|
+
x_method = "cookies"
|
|
603
|
+
else:
|
|
604
|
+
x_method = None
|
|
605
|
+
|
|
606
|
+
payload: Dict[str, Any] = {
|
|
607
|
+
"yt_dlp": yt_dlp,
|
|
608
|
+
"node": node,
|
|
609
|
+
"python3": python3,
|
|
610
|
+
"digg_cli": digg_installed,
|
|
611
|
+
"digg_action": digg_action,
|
|
612
|
+
"keys": keys,
|
|
613
|
+
"x_method": x_method,
|
|
614
|
+
}
|
|
615
|
+
if digg_path:
|
|
616
|
+
payload["digg_path"] = digg_path
|
|
617
|
+
if digg_action == "install_failed" and digg_stderr:
|
|
618
|
+
payload["digg_stderr"] = digg_stderr
|
|
619
|
+
return payload
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
# ---------------------------------------------------------------------------
|
|
623
|
+
# Device auth flow (GitHub OAuth via ScrapeCreators)
|
|
624
|
+
# ---------------------------------------------------------------------------
|
|
625
|
+
|
|
626
|
+
_DEVICE_BASE = "https://api.scrapecreators.com/v1/github/device"
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def run_device_auth() -> Optional[Tuple[str, str, str, int]]:
|
|
630
|
+
"""Start the device authorization flow.
|
|
631
|
+
|
|
632
|
+
POSTs to the ScrapeCreators device/code endpoint.
|
|
633
|
+
|
|
634
|
+
Returns:
|
|
635
|
+
(device_code, user_code, verification_uri, interval) on success,
|
|
636
|
+
None on failure.
|
|
637
|
+
"""
|
|
638
|
+
try:
|
|
639
|
+
body = json.dumps({}).encode()
|
|
640
|
+
req = Request(f"{_DEVICE_BASE}/code", data=body, method="POST")
|
|
641
|
+
req.add_header("Content-Type", "application/json")
|
|
642
|
+
with urlopen(req, timeout=15) as resp:
|
|
643
|
+
data = json.loads(resp.read())
|
|
644
|
+
except (HTTPError, URLError, OSError) as exc:
|
|
645
|
+
logger.warning("Device auth code request failed: %s", exc)
|
|
646
|
+
return None
|
|
647
|
+
|
|
648
|
+
device_code = data.get("device_code")
|
|
649
|
+
user_code = data.get("user_code")
|
|
650
|
+
verification_uri = data.get("verification_uri")
|
|
651
|
+
interval = data.get("interval", 5)
|
|
652
|
+
|
|
653
|
+
if not device_code or not user_code:
|
|
654
|
+
logger.warning("Device auth returned incomplete response: %s", data)
|
|
655
|
+
return None
|
|
656
|
+
|
|
657
|
+
return (device_code, user_code, verification_uri or "", interval)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def poll_device_auth(
|
|
661
|
+
device_code: str,
|
|
662
|
+
interval: int,
|
|
663
|
+
timeout: int = 300,
|
|
664
|
+
user_code: str = "",
|
|
665
|
+
clipboard_ok: bool = False,
|
|
666
|
+
) -> Optional[str]:
|
|
667
|
+
"""Poll for an access token after the user authorizes the device.
|
|
668
|
+
|
|
669
|
+
Args:
|
|
670
|
+
device_code: The device_code from run_device_auth().
|
|
671
|
+
interval: Polling interval in seconds.
|
|
672
|
+
timeout: Maximum time to poll in seconds.
|
|
673
|
+
user_code: The user code to remind about during polling.
|
|
674
|
+
clipboard_ok: Whether the code was copied to clipboard.
|
|
675
|
+
|
|
676
|
+
Returns:
|
|
677
|
+
access_token on success, None on timeout or failure.
|
|
678
|
+
"""
|
|
679
|
+
import sys
|
|
680
|
+
|
|
681
|
+
started_at = time.time()
|
|
682
|
+
deadline = started_at + timeout
|
|
683
|
+
last_reminder = started_at
|
|
684
|
+
reminder_count = 0
|
|
685
|
+
max_reminders = 4
|
|
686
|
+
reminder_interval = 30 # seconds between reminders
|
|
687
|
+
|
|
688
|
+
while time.time() < deadline:
|
|
689
|
+
time.sleep(interval)
|
|
690
|
+
|
|
691
|
+
# Periodic reminder of the code while waiting
|
|
692
|
+
if (
|
|
693
|
+
user_code
|
|
694
|
+
and reminder_count < max_reminders
|
|
695
|
+
and time.time() - last_reminder >= reminder_interval
|
|
696
|
+
):
|
|
697
|
+
clipboard_hint = " (on your clipboard)" if clipboard_ok else ""
|
|
698
|
+
print(
|
|
699
|
+
f" Still waiting... Your code: {user_code}{clipboard_hint}",
|
|
700
|
+
file=sys.stderr,
|
|
701
|
+
flush=True,
|
|
702
|
+
)
|
|
703
|
+
last_reminder = time.time()
|
|
704
|
+
reminder_count += 1
|
|
705
|
+
|
|
706
|
+
try:
|
|
707
|
+
body = json.dumps({"device_code": device_code}).encode()
|
|
708
|
+
req = Request(f"{_DEVICE_BASE}/token", data=body, method="POST")
|
|
709
|
+
req.add_header("Content-Type", "application/json")
|
|
710
|
+
with urlopen(req, timeout=15) as resp:
|
|
711
|
+
data = json.loads(resp.read())
|
|
712
|
+
except HTTPError as exc:
|
|
713
|
+
if exc.code in (400, 403, 428):
|
|
714
|
+
continue
|
|
715
|
+
logger.warning("Device auth poll error: %s", exc)
|
|
716
|
+
return None
|
|
717
|
+
except (URLError, OSError):
|
|
718
|
+
continue
|
|
719
|
+
|
|
720
|
+
if data.get("access_token"):
|
|
721
|
+
return data["access_token"]
|
|
722
|
+
|
|
723
|
+
error = data.get("error")
|
|
724
|
+
if error == "slow_down":
|
|
725
|
+
interval = min(interval + 2, 30)
|
|
726
|
+
continue
|
|
727
|
+
if error == "authorization_pending":
|
|
728
|
+
continue
|
|
729
|
+
if error in ("expired_token", "access_denied"):
|
|
730
|
+
logger.warning("Device auth failed: %s", error)
|
|
731
|
+
return None
|
|
732
|
+
|
|
733
|
+
return None
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def fetch_api_key(access_token: str) -> Optional[str]:
|
|
737
|
+
"""Fetch the ScrapeCreators API key using the GitHub access token.
|
|
738
|
+
|
|
739
|
+
GETs the device/profile endpoint with Bearer auth.
|
|
740
|
+
|
|
741
|
+
Returns:
|
|
742
|
+
api_key string on success, None on failure.
|
|
743
|
+
"""
|
|
744
|
+
try:
|
|
745
|
+
req = Request(f"{_DEVICE_BASE}/profile")
|
|
746
|
+
req.add_header("Authorization", f"Bearer {access_token}")
|
|
747
|
+
with urlopen(req, timeout=15) as resp:
|
|
748
|
+
data = json.loads(resp.read())
|
|
749
|
+
except (HTTPError, URLError, OSError) as exc:
|
|
750
|
+
logger.warning("Failed to fetch API key: %s", exc)
|
|
751
|
+
return None
|
|
752
|
+
|
|
753
|
+
return data.get("api_key")
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def run_full_device_auth(timeout: int = 300) -> Dict[str, Any]:
|
|
757
|
+
"""Run the complete GitHub device auth flow and return JSON-serializable result.
|
|
758
|
+
|
|
759
|
+
Chains: start device flow -> open browser -> poll -> fetch API key.
|
|
760
|
+
Designed to be called from the CLI and have its stdout parsed by the LLM.
|
|
761
|
+
|
|
762
|
+
Returns:
|
|
763
|
+
Dict with status and relevant fields:
|
|
764
|
+
- {"status": "success", "api_key": "sc_...", "user_code": "ABCD-1234"}
|
|
765
|
+
- {"status": "error", "message": "..."}
|
|
766
|
+
- {"status": "timeout", "user_code": "ABCD-1234"}
|
|
767
|
+
- {"status": "denied"}
|
|
768
|
+
"""
|
|
769
|
+
import webbrowser
|
|
770
|
+
|
|
771
|
+
# Step 1: Start device flow
|
|
772
|
+
result = run_device_auth()
|
|
773
|
+
if result is None:
|
|
774
|
+
return {"status": "error", "message": "Failed to start device auth flow"}
|
|
775
|
+
|
|
776
|
+
device_code, user_code, verification_uri, interval = result
|
|
777
|
+
|
|
778
|
+
import sys
|
|
779
|
+
|
|
780
|
+
# Step 2: Copy code to clipboard BEFORE opening browser
|
|
781
|
+
clipboard_ok = False
|
|
782
|
+
if sys.platform == "darwin":
|
|
783
|
+
try:
|
|
784
|
+
subprocess.run(
|
|
785
|
+
["pbcopy"], input=user_code.encode(), check=True, timeout=5,
|
|
786
|
+
)
|
|
787
|
+
clipboard_ok = True
|
|
788
|
+
except Exception:
|
|
789
|
+
pass # pbcopy unavailable or failed, fall through
|
|
790
|
+
|
|
791
|
+
# Step 3: Show code prominently, then open browser
|
|
792
|
+
clipboard_hint = " (copied to clipboard)" if clipboard_ok else ""
|
|
793
|
+
code_line = f" Your code: {user_code}{clipboard_hint}"
|
|
794
|
+
action_line = " Paste it on the GitHub page that just opened"
|
|
795
|
+
width = max(len(code_line), len(action_line)) + 2
|
|
796
|
+
border = "-" * width
|
|
797
|
+
print(f"\n+{border}+", file=sys.stderr)
|
|
798
|
+
print(f"|{code_line.ljust(width)}|", file=sys.stderr)
|
|
799
|
+
print(f"|{action_line.ljust(width)}|", file=sys.stderr)
|
|
800
|
+
print(f"+{border}+", file=sys.stderr)
|
|
801
|
+
|
|
802
|
+
if verification_uri:
|
|
803
|
+
try:
|
|
804
|
+
webbrowser.open(verification_uri)
|
|
805
|
+
except Exception:
|
|
806
|
+
print(f"Open: {verification_uri}", file=sys.stderr)
|
|
807
|
+
|
|
808
|
+
print("Waiting for authorization...", file=sys.stderr, flush=True)
|
|
809
|
+
|
|
810
|
+
# Step 4: Poll for token (with periodic code reminders)
|
|
811
|
+
access_token = poll_device_auth(
|
|
812
|
+
device_code, interval, timeout=timeout,
|
|
813
|
+
user_code=user_code, clipboard_ok=clipboard_ok,
|
|
814
|
+
)
|
|
815
|
+
if access_token is None:
|
|
816
|
+
return {"status": "timeout", "user_code": user_code, "clipboard_ok": clipboard_ok}
|
|
817
|
+
|
|
818
|
+
# Step 4: Fetch API key
|
|
819
|
+
api_key = fetch_api_key(access_token)
|
|
820
|
+
if api_key is None:
|
|
821
|
+
return {
|
|
822
|
+
"status": "error",
|
|
823
|
+
"message": "Authorized but failed to fetch API key",
|
|
824
|
+
"clipboard_ok": clipboard_ok,
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return {"status": "success", "method": "device", "api_key": api_key, "user_code": user_code, "clipboard_ok": clipboard_ok}
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
# ---------------------------------------------------------------------------
|
|
831
|
+
# Unified GitHub auth
|
|
832
|
+
# ---------------------------------------------------------------------------
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def run_github_auth(timeout: int = 300) -> Dict[str, Any]:
|
|
836
|
+
"""Run the --github setup path via device auth only.
|
|
837
|
+
|
|
838
|
+
Kept as the semantic entry point for GitHub-backed setup; this path must
|
|
839
|
+
not read or forward local GitHub CLI tokens.
|
|
840
|
+
|
|
841
|
+
Returns JSON-serializable dict with status, method, and api_key.
|
|
842
|
+
"""
|
|
843
|
+
return run_full_device_auth(timeout=timeout)
|