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.
Files changed (618) hide show
  1. package/.claude-plugin/marketplace.json +16 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/LICENSE +24 -0
  4. package/bin/install.js +7 -5
  5. package/commands/{luckiest/finish.md → finish.md} +2 -0
  6. package/commands/go.md +53 -0
  7. package/commands/plan.md +66 -0
  8. package/package.json +16 -5
  9. package/skills/brainstorm/ATTRIBUTION.md +27 -0
  10. package/skills/brainstorm/SKILL.md +139 -0
  11. package/skills/luckiest-ab-testing/ATTRIBUTION.md +35 -0
  12. package/skills/luckiest-ab-testing/CHANGELOG.md +27 -0
  13. package/skills/luckiest-ab-testing/SKILL.md +379 -0
  14. package/skills/luckiest-ab-testing/evals/evals.json +118 -0
  15. package/skills/luckiest-ab-testing/references/sample-size-guide.md +290 -0
  16. package/skills/luckiest-ab-testing/references/test-templates.md +284 -0
  17. package/skills/luckiest-ad-creative/ATTRIBUTION.md +35 -0
  18. package/skills/luckiest-ad-creative/CHANGELOG.md +23 -0
  19. package/skills/luckiest-ad-creative/SKILL.md +393 -0
  20. package/skills/luckiest-ad-creative/evals/evals.json +90 -0
  21. package/skills/luckiest-ad-creative/references/generative-tools.md +637 -0
  22. package/skills/luckiest-ad-creative/references/platform-specs.md +213 -0
  23. package/skills/luckiest-ads/ATTRIBUTION.md +36 -0
  24. package/skills/luckiest-ads/CHANGELOG.md +24 -0
  25. package/skills/luckiest-ads/LICENSE +21 -0
  26. package/skills/luckiest-ads/SKILL.md +434 -0
  27. package/skills/luckiest-ads/evals/evals.json +90 -0
  28. package/skills/luckiest-ads/references/ad-copy-templates.md +207 -0
  29. package/skills/luckiest-ads/references/audience-targeting.md +243 -0
  30. package/skills/luckiest-ads/references/conversion-tracking.md +361 -0
  31. package/skills/luckiest-ads/references/platform-setup-checklists.md +277 -0
  32. package/skills/luckiest-advisors/ATTRIBUTION.md +16 -0
  33. package/skills/luckiest-advisors/CHANGELOG.md +99 -0
  34. package/skills/luckiest-advisors/SKILL.md +151 -0
  35. package/skills/luckiest-aeo-grader/ATTRIBUTION.md +19 -0
  36. package/skills/luckiest-aeo-grader/CHANGELOG.md +52 -0
  37. package/skills/luckiest-aeo-grader/SKILL.md +200 -0
  38. package/skills/luckiest-aeo-grader/evals/evals.json +19 -0
  39. package/skills/luckiest-aeo-grader/references/rubric.md +72 -0
  40. package/skills/luckiest-aeo-grader/scripts/scan.py +265 -0
  41. package/skills/luckiest-ai-seo/ATTRIBUTION.md +38 -0
  42. package/skills/luckiest-ai-seo/CHANGELOG.md +23 -0
  43. package/skills/luckiest-ai-seo/SKILL.md +510 -0
  44. package/skills/luckiest-ai-seo/evals/evals.json +90 -0
  45. package/skills/luckiest-ai-seo/references/content-patterns.md +285 -0
  46. package/skills/luckiest-ai-seo/references/content-types.md +71 -0
  47. package/skills/luckiest-ai-seo/references/okf.md +104 -0
  48. package/skills/luckiest-ai-seo/references/platform-ranking-factors.md +154 -0
  49. package/skills/luckiest-analytics/ATTRIBUTION.md +42 -0
  50. package/skills/luckiest-analytics/CHANGELOG.md +36 -0
  51. package/skills/luckiest-analytics/SKILL.md +380 -0
  52. package/skills/luckiest-analytics/evals/evals.json +90 -0
  53. package/skills/luckiest-analytics/references/event-library.md +260 -0
  54. package/skills/luckiest-analytics/references/ga4-implementation.md +300 -0
  55. package/skills/luckiest-analytics/references/gtm-implementation.md +390 -0
  56. package/skills/luckiest-aso/ATTRIBUTION.md +40 -0
  57. package/skills/luckiest-aso/CHANGELOG.md +33 -0
  58. package/skills/luckiest-aso/SKILL.md +327 -0
  59. package/skills/luckiest-aso/evals/evals.json +91 -0
  60. package/skills/luckiest-aso/references/apple-specs.md +107 -0
  61. package/skills/luckiest-aso/references/benchmarks.md +129 -0
  62. package/skills/luckiest-aso/references/google-play-specs.md +131 -0
  63. package/skills/luckiest-aso/references/report-template.md +213 -0
  64. package/skills/luckiest-aso/references/scoring-criteria.md +213 -0
  65. package/skills/luckiest-churn-prevention/ATTRIBUTION.md +41 -0
  66. package/skills/luckiest-churn-prevention/CHANGELOG.md +24 -0
  67. package/skills/luckiest-churn-prevention/SKILL.md +445 -0
  68. package/skills/luckiest-churn-prevention/evals/evals.json +93 -0
  69. package/skills/luckiest-churn-prevention/references/cancel-flow-patterns.md +316 -0
  70. package/skills/luckiest-churn-prevention/references/dunning-playbook.md +408 -0
  71. package/skills/luckiest-co-marketing/ATTRIBUTION.md +38 -0
  72. package/skills/luckiest-co-marketing/CHANGELOG.md +25 -0
  73. package/skills/luckiest-co-marketing/SKILL.md +331 -0
  74. package/skills/luckiest-co-marketing/evals/evals.json +84 -0
  75. package/skills/luckiest-coder/ATTRIBUTION.md +26 -0
  76. package/skills/luckiest-coder/CHANGELOG.md +30 -0
  77. package/skills/luckiest-coder/SKILL.md +77 -0
  78. package/skills/luckiest-coder/references/antigravity-tools.md +23 -0
  79. package/skills/luckiest-coder/references/codex-tools.md +39 -0
  80. package/skills/luckiest-coder/references/pi-tools.md +16 -0
  81. package/skills/luckiest-coder-brainstorming/ATTRIBUTION.md +26 -0
  82. package/skills/luckiest-coder-brainstorming/CHANGELOG.md +31 -0
  83. package/skills/luckiest-coder-brainstorming/SKILL.md +173 -0
  84. package/skills/luckiest-coder-brainstorming/scripts/frame-template.html +213 -0
  85. package/skills/luckiest-coder-brainstorming/scripts/helper.js +168 -0
  86. package/skills/luckiest-coder-brainstorming/scripts/server.cjs +721 -0
  87. package/skills/luckiest-coder-brainstorming/scripts/start-server.sh +209 -0
  88. package/skills/luckiest-coder-brainstorming/scripts/stop-server.sh +120 -0
  89. package/skills/luckiest-coder-brainstorming/spec-document-reviewer-prompt.md +49 -0
  90. package/skills/luckiest-coder-brainstorming/visual-companion.md +291 -0
  91. package/skills/luckiest-coder-consistency/ATTRIBUTION.md +30 -0
  92. package/skills/luckiest-coder-consistency/CHANGELOG.md +26 -0
  93. package/skills/luckiest-coder-consistency/SKILL.md +98 -0
  94. package/skills/luckiest-coder-constitution/ATTRIBUTION.md +28 -0
  95. package/skills/luckiest-coder-constitution/CHANGELOG.md +25 -0
  96. package/skills/luckiest-coder-constitution/SKILL.md +115 -0
  97. package/skills/luckiest-coder-debugging/ATTRIBUTION.md +26 -0
  98. package/skills/luckiest-coder-debugging/CHANGELOG.md +22 -0
  99. package/skills/luckiest-coder-debugging/CREATION-LOG.md +119 -0
  100. package/skills/luckiest-coder-debugging/SKILL.md +309 -0
  101. package/skills/luckiest-coder-debugging/condition-based-waiting-example.ts +158 -0
  102. package/skills/luckiest-coder-debugging/condition-based-waiting.md +115 -0
  103. package/skills/luckiest-coder-debugging/defense-in-depth.md +122 -0
  104. package/skills/luckiest-coder-debugging/find-polluter.sh +63 -0
  105. package/skills/luckiest-coder-debugging/root-cause-tracing.md +169 -0
  106. package/skills/luckiest-coder-debugging/test-academic.md +14 -0
  107. package/skills/luckiest-coder-debugging/test-pressure-1.md +58 -0
  108. package/skills/luckiest-coder-debugging/test-pressure-2.md +68 -0
  109. package/skills/luckiest-coder-debugging/test-pressure-3.md +69 -0
  110. package/skills/luckiest-coder-dispatching-parallel-agents/ATTRIBUTION.md +26 -0
  111. package/skills/luckiest-coder-dispatching-parallel-agents/CHANGELOG.md +23 -0
  112. package/skills/luckiest-coder-dispatching-parallel-agents/SKILL.md +198 -0
  113. package/skills/luckiest-coder-executing-plans/ATTRIBUTION.md +26 -0
  114. package/skills/luckiest-coder-executing-plans/CHANGELOG.md +22 -0
  115. package/skills/luckiest-coder-executing-plans/SKILL.md +83 -0
  116. package/skills/luckiest-coder-finishing-a-branch/ATTRIBUTION.md +26 -0
  117. package/skills/luckiest-coder-finishing-a-branch/CHANGELOG.md +22 -0
  118. package/skills/luckiest-coder-finishing-a-branch/SKILL.md +258 -0
  119. package/skills/luckiest-coder-git-worktrees/ATTRIBUTION.md +26 -0
  120. package/skills/luckiest-coder-git-worktrees/CHANGELOG.md +22 -0
  121. package/skills/luckiest-coder-git-worktrees/SKILL.md +215 -0
  122. package/skills/luckiest-coder-guard/ATTRIBUTION.md +29 -0
  123. package/skills/luckiest-coder-guard/CHANGELOG.md +48 -0
  124. package/skills/luckiest-coder-guard/LICENSE +21 -0
  125. package/skills/luckiest-coder-guard/SKILL.md +126 -0
  126. package/skills/luckiest-coder-guard/bin/check-careful.sh +65 -0
  127. package/skills/luckiest-coder-guard/bin/check-freeze.sh +48 -0
  128. package/skills/luckiest-coder-qa/ATTRIBUTION.md +28 -0
  129. package/skills/luckiest-coder-qa/CHANGELOG.md +49 -0
  130. package/skills/luckiest-coder-qa/LICENSE +21 -0
  131. package/skills/luckiest-coder-qa/SKILL.md +160 -0
  132. package/skills/luckiest-coder-qa/references/issue-taxonomy.md +85 -0
  133. package/skills/luckiest-coder-qa/references/qa-report-template.md +126 -0
  134. package/skills/luckiest-coder-receiving-code-review/ATTRIBUTION.md +26 -0
  135. package/skills/luckiest-coder-receiving-code-review/CHANGELOG.md +28 -0
  136. package/skills/luckiest-coder-receiving-code-review/SKILL.md +226 -0
  137. package/skills/luckiest-coder-requesting-code-review/ATTRIBUTION.md +26 -0
  138. package/skills/luckiest-coder-requesting-code-review/CHANGELOG.md +31 -0
  139. package/skills/luckiest-coder-requesting-code-review/SKILL.md +118 -0
  140. package/skills/luckiest-coder-requesting-code-review/code-reviewer.md +172 -0
  141. package/skills/luckiest-coder-shipping/ATTRIBUTION.md +26 -0
  142. package/skills/luckiest-coder-shipping/CHANGELOG.md +48 -0
  143. package/skills/luckiest-coder-shipping/LICENSE +21 -0
  144. package/skills/luckiest-coder-shipping/SKILL.md +148 -0
  145. package/skills/luckiest-coder-shipping/references/canary.md +44 -0
  146. package/skills/luckiest-coder-shipping/references/test-and-coverage.md +91 -0
  147. package/skills/luckiest-coder-subagent-driven-development/ATTRIBUTION.md +26 -0
  148. package/skills/luckiest-coder-subagent-driven-development/CHANGELOG.md +28 -0
  149. package/skills/luckiest-coder-subagent-driven-development/SKILL.md +439 -0
  150. package/skills/luckiest-coder-subagent-driven-development/implementer-prompt.md +139 -0
  151. package/skills/luckiest-coder-subagent-driven-development/scripts/review-package +44 -0
  152. package/skills/luckiest-coder-subagent-driven-development/scripts/sdd-workspace +22 -0
  153. package/skills/luckiest-coder-subagent-driven-development/scripts/task-brief +40 -0
  154. package/skills/luckiest-coder-subagent-driven-development/task-reviewer-prompt.md +188 -0
  155. package/skills/luckiest-coder-tdd/ATTRIBUTION.md +26 -0
  156. package/skills/luckiest-coder-tdd/CHANGELOG.md +26 -0
  157. package/skills/luckiest-coder-tdd/SKILL.md +384 -0
  158. package/skills/luckiest-coder-tdd/testing-anti-patterns.md +299 -0
  159. package/skills/luckiest-coder-verification/ATTRIBUTION.md +26 -0
  160. package/skills/luckiest-coder-verification/CHANGELOG.md +22 -0
  161. package/skills/luckiest-coder-verification/SKILL.md +152 -0
  162. package/skills/luckiest-coder-writing-plans/ATTRIBUTION.md +26 -0
  163. package/skills/luckiest-coder-writing-plans/CHANGELOG.md +22 -0
  164. package/skills/luckiest-coder-writing-plans/SKILL.md +191 -0
  165. package/skills/luckiest-coder-writing-plans/plan-document-reviewer-prompt.md +49 -0
  166. package/skills/luckiest-coder-writing-skills/ATTRIBUTION.md +26 -0
  167. package/skills/luckiest-coder-writing-skills/CHANGELOG.md +31 -0
  168. package/skills/luckiest-coder-writing-skills/SKILL.md +702 -0
  169. package/skills/luckiest-coder-writing-skills/anthropic-best-practices.md +1150 -0
  170. package/skills/luckiest-coder-writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
  171. package/skills/luckiest-coder-writing-skills/graphviz-conventions.dot +172 -0
  172. package/skills/luckiest-coder-writing-skills/persuasion-principles.md +187 -0
  173. package/skills/luckiest-coder-writing-skills/render-graphs.js +168 -0
  174. package/skills/luckiest-coder-writing-skills/testing-skills-with-subagents.md +384 -0
  175. package/skills/luckiest-cold-email/ATTRIBUTION.md +36 -0
  176. package/skills/luckiest-cold-email/CHANGELOG.md +24 -0
  177. package/skills/luckiest-cold-email/SKILL.md +213 -0
  178. package/skills/luckiest-cold-email/evals/evals.json +94 -0
  179. package/skills/luckiest-cold-email/references/benchmarks.md +83 -0
  180. package/skills/luckiest-cold-email/references/follow-up-sequences.md +81 -0
  181. package/skills/luckiest-cold-email/references/frameworks.md +90 -0
  182. package/skills/luckiest-cold-email/references/personalization.md +79 -0
  183. package/skills/luckiest-cold-email/references/subject-lines.md +53 -0
  184. package/skills/luckiest-community-marketing/ATTRIBUTION.md +41 -0
  185. package/skills/luckiest-community-marketing/CHANGELOG.md +35 -0
  186. package/skills/luckiest-community-marketing/SKILL.md +200 -0
  187. package/skills/luckiest-community-marketing/evals/evals.json +89 -0
  188. package/skills/luckiest-competitor-profiling/ATTRIBUTION.md +36 -0
  189. package/skills/luckiest-competitor-profiling/CHANGELOG.md +24 -0
  190. package/skills/luckiest-competitor-profiling/SKILL.md +443 -0
  191. package/skills/luckiest-competitor-profiling/evals/evals.json +85 -0
  192. package/skills/luckiest-competitor-profiling/references/templates.md +167 -0
  193. package/skills/luckiest-competitor-profiling/references/tool-reference.md +179 -0
  194. package/skills/luckiest-competitors/ATTRIBUTION.md +36 -0
  195. package/skills/luckiest-competitors/CHANGELOG.md +25 -0
  196. package/skills/luckiest-competitors/SKILL.md +171 -0
  197. package/skills/luckiest-competitors/evals/evals.json +93 -0
  198. package/skills/luckiest-competitors/references/content-architecture.md +271 -0
  199. package/skills/luckiest-competitors/references/templates.md +223 -0
  200. package/skills/luckiest-content-strategy/ATTRIBUTION.md +39 -0
  201. package/skills/luckiest-content-strategy/CHANGELOG.md +23 -0
  202. package/skills/luckiest-content-strategy/SKILL.md +395 -0
  203. package/skills/luckiest-content-strategy/evals/evals.json +90 -0
  204. package/skills/luckiest-content-strategy/references/headless-cms.md +194 -0
  205. package/skills/luckiest-copy-editing/ATTRIBUTION.md +41 -0
  206. package/skills/luckiest-copy-editing/CHANGELOG.md +31 -0
  207. package/skills/luckiest-copy-editing/SKILL.md +491 -0
  208. package/skills/luckiest-copy-editing/evals/evals.json +89 -0
  209. package/skills/luckiest-copy-editing/references/checklist.md +66 -0
  210. package/skills/luckiest-copy-editing/references/content-refresh.md +38 -0
  211. package/skills/luckiest-copy-editing/references/plain-english-alternatives.md +394 -0
  212. package/skills/luckiest-copywriting/ATTRIBUTION.md +35 -0
  213. package/skills/luckiest-copywriting/CHANGELOG.md +23 -0
  214. package/skills/luckiest-copywriting/SKILL.md +296 -0
  215. package/skills/luckiest-copywriting/evals/evals.json +111 -0
  216. package/skills/luckiest-copywriting/references/copy-frameworks.md +344 -0
  217. package/skills/luckiest-copywriting/references/natural-transitions.md +272 -0
  218. package/skills/luckiest-cro/ATTRIBUTION.md +34 -0
  219. package/skills/luckiest-cro/CHANGELOG.md +23 -0
  220. package/skills/luckiest-cro/SKILL.md +215 -0
  221. package/skills/luckiest-cro/evals/evals.json +111 -0
  222. package/skills/luckiest-cro/references/experiments.md +248 -0
  223. package/skills/luckiest-cro/references/form.md +422 -0
  224. package/skills/luckiest-customer-research/ATTRIBUTION.md +40 -0
  225. package/skills/luckiest-customer-research/CHANGELOG.md +26 -0
  226. package/skills/luckiest-customer-research/SKILL.md +293 -0
  227. package/skills/luckiest-customer-research/evals/evals.json +162 -0
  228. package/skills/luckiest-customer-research/references/source-guides.md +401 -0
  229. package/skills/luckiest-directory-submissions/ATTRIBUTION.md +40 -0
  230. package/skills/luckiest-directory-submissions/CHANGELOG.md +33 -0
  231. package/skills/luckiest-directory-submissions/SKILL.md +419 -0
  232. package/skills/luckiest-directory-submissions/evals/evals.json +94 -0
  233. package/skills/luckiest-directory-submissions/references/directory-list.md +477 -0
  234. package/skills/luckiest-directory-submissions/references/positioning-variations.md +232 -0
  235. package/skills/luckiest-directory-submissions/references/submission-tracker-template.csv +266 -0
  236. package/skills/luckiest-emails/ATTRIBUTION.md +42 -0
  237. package/skills/luckiest-emails/CHANGELOG.md +33 -0
  238. package/skills/luckiest-emails/SKILL.md +220 -0
  239. package/skills/luckiest-emails/evals/evals.json +93 -0
  240. package/skills/luckiest-emails/references/copy-guidelines.md +120 -0
  241. package/skills/luckiest-emails/references/email-types.md +515 -0
  242. package/skills/luckiest-emails/references/sequence-templates.md +168 -0
  243. package/skills/luckiest-extract-design-system/ATTRIBUTION.md +8 -0
  244. package/skills/luckiest-extract-design-system/CHANGELOG.md +37 -0
  245. package/skills/luckiest-extract-design-system/SKILL.md +83 -0
  246. package/skills/luckiest-free-tools/ATTRIBUTION.md +43 -0
  247. package/skills/luckiest-free-tools/CHANGELOG.md +25 -0
  248. package/skills/luckiest-free-tools/SKILL.md +220 -0
  249. package/skills/luckiest-free-tools/evals/evals.json +90 -0
  250. package/skills/luckiest-free-tools/references/tool-types.md +217 -0
  251. package/skills/luckiest-image/ATTRIBUTION.md +43 -0
  252. package/skills/luckiest-image/CHANGELOG.md +23 -0
  253. package/skills/luckiest-image/SKILL.md +376 -0
  254. package/skills/luckiest-image/evals/evals.json +89 -0
  255. package/skills/luckiest-image/references/ai-image-prompting.md +229 -0
  256. package/skills/luckiest-launch/ATTRIBUTION.md +40 -0
  257. package/skills/luckiest-launch/CHANGELOG.md +43 -0
  258. package/skills/luckiest-launch/SKILL.md +385 -0
  259. package/skills/luckiest-launch/evals/evals.json +91 -0
  260. package/skills/luckiest-lead-magnets/ATTRIBUTION.md +35 -0
  261. package/skills/luckiest-lead-magnets/CHANGELOG.md +23 -0
  262. package/skills/luckiest-lead-magnets/SKILL.md +331 -0
  263. package/skills/luckiest-lead-magnets/evals/evals.json +87 -0
  264. package/skills/luckiest-lead-magnets/references/benchmarks.md +129 -0
  265. package/skills/luckiest-lead-magnets/references/format-guide.md +196 -0
  266. package/skills/luckiest-marketing-ideas/ATTRIBUTION.md +39 -0
  267. package/skills/luckiest-marketing-ideas/CHANGELOG.md +33 -0
  268. package/skills/luckiest-marketing-ideas/SKILL.md +191 -0
  269. package/skills/luckiest-marketing-ideas/evals/evals.json +90 -0
  270. package/skills/luckiest-marketing-ideas/references/ideas-by-category.md +366 -0
  271. package/skills/luckiest-marketing-plan/ATTRIBUTION.md +40 -0
  272. package/skills/luckiest-marketing-plan/CHANGELOG.md +23 -0
  273. package/skills/luckiest-marketing-plan/SKILL.md +285 -0
  274. package/skills/luckiest-marketing-plan/evals/evals.json +96 -0
  275. package/skills/luckiest-marketing-plan/references/aarrr-framework.md +180 -0
  276. package/skills/luckiest-marketing-plan/references/budget-planning.md +168 -0
  277. package/skills/luckiest-marketing-plan/references/client-types.md +373 -0
  278. package/skills/luckiest-marketing-plan/references/current-state-rubric.md +255 -0
  279. package/skills/luckiest-marketing-plan/references/example-quietude.md +972 -0
  280. package/skills/luckiest-marketing-plan/references/funding-stage-unlocks.md +230 -0
  281. package/skills/luckiest-marketing-plan/references/growth-patterns.md +148 -0
  282. package/skills/luckiest-marketing-plan/references/idea-cross-reference.md +265 -0
  283. package/skills/luckiest-marketing-plan/references/measurement-framework.md +213 -0
  284. package/skills/luckiest-marketing-plan/references/methodology.md +363 -0
  285. package/skills/luckiest-marketing-plan/references/ops-stack-mapping.md +197 -0
  286. package/skills/luckiest-marketing-plan/references/plan-template.md +494 -0
  287. package/skills/luckiest-marketing-plan/references/team-and-agency-model.md +278 -0
  288. package/skills/luckiest-marketing-psychology/ATTRIBUTION.md +31 -0
  289. package/skills/luckiest-marketing-psychology/CHANGELOG.md +32 -0
  290. package/skills/luckiest-marketing-psychology/SKILL.md +202 -0
  291. package/skills/luckiest-marketing-psychology/evals.json +88 -0
  292. package/skills/luckiest-model-router/ATTRIBUTION.md +11 -0
  293. package/skills/luckiest-model-router/CHANGELOG.md +45 -0
  294. package/skills/luckiest-model-router/SKILL.md +74 -0
  295. package/skills/luckiest-model-router/scripts/route.py +145 -0
  296. package/skills/luckiest-offers/ATTRIBUTION.md +39 -0
  297. package/skills/luckiest-offers/CHANGELOG.md +23 -0
  298. package/skills/luckiest-offers/SKILL.md +167 -0
  299. package/skills/luckiest-offers/references/bonus-stacking.md +150 -0
  300. package/skills/luckiest-offers/references/examples.md +215 -0
  301. package/skills/luckiest-offers/references/guarantee-design.md +172 -0
  302. package/skills/luckiest-offers/references/offer-anatomy.md +203 -0
  303. package/skills/luckiest-offers/references/offer-formats.md +240 -0
  304. package/skills/luckiest-offers/references/scarcity-urgency.md +175 -0
  305. package/skills/luckiest-offers/references/value-equation.md +134 -0
  306. package/skills/luckiest-onboarding/ATTRIBUTION.md +43 -0
  307. package/skills/luckiest-onboarding/CHANGELOG.md +31 -0
  308. package/skills/luckiest-onboarding/SKILL.md +243 -0
  309. package/skills/luckiest-onboarding/references/experiments.md +258 -0
  310. package/skills/luckiest-paywalls/ATTRIBUTION.md +39 -0
  311. package/skills/luckiest-paywalls/CHANGELOG.md +25 -0
  312. package/skills/luckiest-paywalls/SKILL.md +256 -0
  313. package/skills/luckiest-paywalls/evals/evals.json +93 -0
  314. package/skills/luckiest-paywalls/references/experiments.md +164 -0
  315. package/skills/luckiest-popups/ATTRIBUTION.md +35 -0
  316. package/skills/luckiest-popups/CHANGELOG.md +27 -0
  317. package/skills/luckiest-popups/SKILL.md +477 -0
  318. package/skills/luckiest-popups/evals/evals.json +107 -0
  319. package/skills/luckiest-pricing/ATTRIBUTION.md +35 -0
  320. package/skills/luckiest-pricing/CHANGELOG.md +31 -0
  321. package/skills/luckiest-pricing/SKILL.md +283 -0
  322. package/skills/luckiest-pricing/evals/evals.json +90 -0
  323. package/skills/luckiest-pricing/references/research-methods.md +152 -0
  324. package/skills/luckiest-pricing/references/tier-structure.md +232 -0
  325. package/skills/luckiest-product-marketing/ATTRIBUTION.md +38 -0
  326. package/skills/luckiest-product-marketing/CHANGELOG.md +23 -0
  327. package/skills/luckiest-product-marketing/SKILL.md +256 -0
  328. package/skills/luckiest-product-marketing/evals/evals.json +85 -0
  329. package/skills/luckiest-programmatic-seo/ATTRIBUTION.md +35 -0
  330. package/skills/luckiest-programmatic-seo/CHANGELOG.md +24 -0
  331. package/skills/luckiest-programmatic-seo/SKILL.md +286 -0
  332. package/skills/luckiest-programmatic-seo/evals/evals.json +95 -0
  333. package/skills/luckiest-programmatic-seo/references/playbooks.md +308 -0
  334. package/skills/luckiest-prompt-rewrite/ATTRIBUTION.md +39 -0
  335. package/skills/luckiest-prompt-rewrite/CHANGELOG.md +70 -0
  336. package/skills/luckiest-prompt-rewrite/LICENSE +21 -0
  337. package/skills/luckiest-prompt-rewrite/SKILL.md +494 -0
  338. package/skills/luckiest-prompt-rewrite/references/patterns.md +82 -0
  339. package/skills/luckiest-prompt-rewrite/references/templates.md +454 -0
  340. package/skills/luckiest-prospecting/ATTRIBUTION.md +29 -0
  341. package/skills/luckiest-prospecting/CHANGELOG.md +24 -0
  342. package/skills/luckiest-prospecting/SKILL.md +275 -0
  343. package/skills/luckiest-prospecting/evals/evals.json +107 -0
  344. package/skills/luckiest-prospecting/references/b2b-prospecting.md +106 -0
  345. package/skills/luckiest-prospecting/references/compliance.md +123 -0
  346. package/skills/luckiest-prospecting/references/data-sources.md +287 -0
  347. package/skills/luckiest-prospecting/references/local-prospecting.md +165 -0
  348. package/skills/luckiest-prospecting/references/saas-prospecting.md +123 -0
  349. package/skills/luckiest-public-relations/ATTRIBUTION.md +34 -0
  350. package/skills/luckiest-public-relations/CHANGELOG.md +23 -0
  351. package/skills/luckiest-public-relations/SKILL.md +154 -0
  352. package/skills/luckiest-public-relations/references/journalist-pitching.md +368 -0
  353. package/skills/luckiest-public-relations/references/media-outlets.md +207 -0
  354. package/skills/luckiest-public-relations/references/newsjacking.md +218 -0
  355. package/skills/luckiest-public-relations/references/press-platforms.md +188 -0
  356. package/skills/luckiest-referrals/ATTRIBUTION.md +41 -0
  357. package/skills/luckiest-referrals/CHANGELOG.md +23 -0
  358. package/skills/luckiest-referrals/SKILL.md +292 -0
  359. package/skills/luckiest-referrals/evals/evals.json +90 -0
  360. package/skills/luckiest-referrals/references/affiliate-programs.md +167 -0
  361. package/skills/luckiest-referrals/references/program-examples.md +143 -0
  362. package/skills/luckiest-revops/ATTRIBUTION.md +40 -0
  363. package/skills/luckiest-revops/CHANGELOG.md +24 -0
  364. package/skills/luckiest-revops/SKILL.md +362 -0
  365. package/skills/luckiest-revops/evals/evals.json +91 -0
  366. package/skills/luckiest-revops/references/automation-playbooks.md +290 -0
  367. package/skills/luckiest-revops/references/lifecycle-definitions.md +278 -0
  368. package/skills/luckiest-revops/references/routing-rules.md +203 -0
  369. package/skills/luckiest-revops/references/scoring-models.md +247 -0
  370. package/skills/luckiest-sales-enablement/ATTRIBUTION.md +39 -0
  371. package/skills/luckiest-sales-enablement/CHANGELOG.md +23 -0
  372. package/skills/luckiest-sales-enablement/SKILL.md +379 -0
  373. package/skills/luckiest-sales-enablement/evals/evals.json +91 -0
  374. package/skills/luckiest-sales-enablement/references/deck-frameworks.md +263 -0
  375. package/skills/luckiest-sales-enablement/references/demo-scripts.md +355 -0
  376. package/skills/luckiest-sales-enablement/references/objection-library.md +270 -0
  377. package/skills/luckiest-sales-enablement/references/one-pager-templates.md +208 -0
  378. package/skills/luckiest-schema/ATTRIBUTION.md +39 -0
  379. package/skills/luckiest-schema/CHANGELOG.md +25 -0
  380. package/skills/luckiest-schema/SKILL.md +209 -0
  381. package/skills/luckiest-schema/evals/evals.json +88 -0
  382. package/skills/luckiest-schema/references/schema-examples.md +398 -0
  383. package/skills/luckiest-seo-audit/ATTRIBUTION.md +39 -0
  384. package/skills/luckiest-seo-audit/CHANGELOG.md +23 -0
  385. package/skills/luckiest-seo-audit/SKILL.md +529 -0
  386. package/skills/luckiest-seo-audit/evals/evals.json +136 -0
  387. package/skills/luckiest-seo-audit/references/ai-writing-detection.md +200 -0
  388. package/skills/luckiest-seo-audit/references/international-seo.md +230 -0
  389. package/skills/luckiest-session-handoff/ATTRIBUTION.md +14 -0
  390. package/skills/luckiest-session-handoff/CHANGELOG.md +30 -0
  391. package/skills/luckiest-session-handoff/SKILL.md +96 -0
  392. package/skills/luckiest-signup/ATTRIBUTION.md +48 -0
  393. package/skills/luckiest-signup/CHANGELOG.md +24 -0
  394. package/skills/luckiest-signup/SKILL.md +382 -0
  395. package/skills/luckiest-signup/evals/evals.json +88 -0
  396. package/skills/luckiest-site-architecture/ATTRIBUTION.md +38 -0
  397. package/skills/luckiest-site-architecture/CHANGELOG.md +23 -0
  398. package/skills/luckiest-site-architecture/SKILL.md +383 -0
  399. package/skills/luckiest-site-architecture/evals/evals.json +88 -0
  400. package/skills/luckiest-site-architecture/references/mermaid-templates.md +216 -0
  401. package/skills/luckiest-site-architecture/references/navigation-patterns.md +305 -0
  402. package/skills/luckiest-site-architecture/references/site-type-templates.md +293 -0
  403. package/skills/luckiest-sms/ATTRIBUTION.md +27 -0
  404. package/skills/luckiest-sms/CHANGELOG.md +24 -0
  405. package/skills/luckiest-sms/SKILL.md +400 -0
  406. package/skills/luckiest-sms/evals/evals.json +100 -0
  407. package/skills/luckiest-sms/references/compliance.md +202 -0
  408. package/skills/luckiest-sms/references/platforms.md +318 -0
  409. package/skills/luckiest-sms/references/sequence-templates.md +282 -0
  410. package/skills/luckiest-social/ATTRIBUTION.md +27 -0
  411. package/skills/luckiest-social/CHANGELOG.md +24 -0
  412. package/skills/luckiest-social/SKILL.md +430 -0
  413. package/skills/luckiest-social/evals/evals.json +92 -0
  414. package/skills/luckiest-social/references/listening-sources-template.md +123 -0
  415. package/skills/luckiest-social/references/listening.md +283 -0
  416. package/skills/luckiest-social/references/platform-limits.md +110 -0
  417. package/skills/luckiest-social/references/platforms.md +170 -0
  418. package/skills/luckiest-social/references/post-templates.md +177 -0
  419. package/skills/luckiest-social/references/reverse-engineering.md +195 -0
  420. package/skills/luckiest-social/references/short-form-video.md +237 -0
  421. package/skills/luckiest-thinker/ATTRIBUTION.md +16 -0
  422. package/skills/luckiest-thinker/CHANGELOG.md +20 -0
  423. package/skills/luckiest-thinker/LICENSE +21 -0
  424. package/skills/luckiest-thinker/SKILL.md +92 -0
  425. package/skills/luckiest-thinker/references/distribution-calibration.md +106 -0
  426. package/skills/luckiest-thinker/references/research-foundations.md +214 -0
  427. package/skills/luckiest-thinker/references/thinking-patterns.md +138 -0
  428. package/skills/luckiest-trends/.skillignore +10 -0
  429. package/skills/luckiest-trends/ATTRIBUTION.md +43 -0
  430. package/skills/luckiest-trends/CHANGELOG.md +86 -0
  431. package/skills/luckiest-trends/LICENSE +21 -0
  432. package/skills/luckiest-trends/SKILL.md +194 -0
  433. package/skills/luckiest-trends/agents/openai.yaml +8 -0
  434. package/skills/luckiest-trends/references/engine-contract.md +2041 -0
  435. package/skills/luckiest-trends/references/save-html-brief.md +206 -0
  436. package/skills/luckiest-trends/scripts/briefing.py +272 -0
  437. package/skills/luckiest-trends/scripts/build-skill.sh +39 -0
  438. package/skills/luckiest-trends/scripts/compare.sh +61 -0
  439. package/skills/luckiest-trends/scripts/evaluate_search_quality.py +573 -0
  440. package/skills/luckiest-trends/scripts/last30days.py +1467 -0
  441. package/skills/luckiest-trends/scripts/lib/__init__.py +1 -0
  442. package/skills/luckiest-trends/scripts/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  443. package/skills/luckiest-trends/scripts/lib/__pycache__/arxiv.cpython-313.pyc +0 -0
  444. package/skills/luckiest-trends/scripts/lib/__pycache__/bird_x.cpython-313.pyc +0 -0
  445. package/skills/luckiest-trends/scripts/lib/__pycache__/bluesky.cpython-313.pyc +0 -0
  446. package/skills/luckiest-trends/scripts/lib/__pycache__/categories.cpython-313.pyc +0 -0
  447. package/skills/luckiest-trends/scripts/lib/__pycache__/cjk.cpython-313.pyc +0 -0
  448. package/skills/luckiest-trends/scripts/lib/__pycache__/cluster.cpython-313.pyc +0 -0
  449. package/skills/luckiest-trends/scripts/lib/__pycache__/dates.cpython-313.pyc +0 -0
  450. package/skills/luckiest-trends/scripts/lib/__pycache__/dedupe.cpython-313.pyc +0 -0
  451. package/skills/luckiest-trends/scripts/lib/__pycache__/digg.cpython-313.pyc +0 -0
  452. package/skills/luckiest-trends/scripts/lib/__pycache__/entity_extract.cpython-313.pyc +0 -0
  453. package/skills/luckiest-trends/scripts/lib/__pycache__/env.cpython-313.pyc +0 -0
  454. package/skills/luckiest-trends/scripts/lib/__pycache__/fusion.cpython-313.pyc +0 -0
  455. package/skills/luckiest-trends/scripts/lib/__pycache__/github.cpython-313.pyc +0 -0
  456. package/skills/luckiest-trends/scripts/lib/__pycache__/grounding.cpython-313.pyc +0 -0
  457. package/skills/luckiest-trends/scripts/lib/__pycache__/hackernews.cpython-313.pyc +0 -0
  458. package/skills/luckiest-trends/scripts/lib/__pycache__/hiring_signals.cpython-313.pyc +0 -0
  459. package/skills/luckiest-trends/scripts/lib/__pycache__/html_render.cpython-313.pyc +0 -0
  460. package/skills/luckiest-trends/scripts/lib/__pycache__/http.cpython-313.pyc +0 -0
  461. package/skills/luckiest-trends/scripts/lib/__pycache__/instagram.cpython-313.pyc +0 -0
  462. package/skills/luckiest-trends/scripts/lib/__pycache__/jobs.cpython-313.pyc +0 -0
  463. package/skills/luckiest-trends/scripts/lib/__pycache__/linkedin.cpython-313.pyc +0 -0
  464. package/skills/luckiest-trends/scripts/lib/__pycache__/log.cpython-313.pyc +0 -0
  465. package/skills/luckiest-trends/scripts/lib/__pycache__/normalize.cpython-313.pyc +0 -0
  466. package/skills/luckiest-trends/scripts/lib/__pycache__/permission_preflight.cpython-313.pyc +0 -0
  467. package/skills/luckiest-trends/scripts/lib/__pycache__/perplexity.cpython-313.pyc +0 -0
  468. package/skills/luckiest-trends/scripts/lib/__pycache__/pinterest.cpython-313.pyc +0 -0
  469. package/skills/luckiest-trends/scripts/lib/__pycache__/pipeline.cpython-313.pyc +0 -0
  470. package/skills/luckiest-trends/scripts/lib/__pycache__/planner.cpython-313.pyc +0 -0
  471. package/skills/luckiest-trends/scripts/lib/__pycache__/polymarket.cpython-313.pyc +0 -0
  472. package/skills/luckiest-trends/scripts/lib/__pycache__/preflight.cpython-313.pyc +0 -0
  473. package/skills/luckiest-trends/scripts/lib/__pycache__/providers.cpython-313.pyc +0 -0
  474. package/skills/luckiest-trends/scripts/lib/__pycache__/quality_nudge.cpython-313.pyc +0 -0
  475. package/skills/luckiest-trends/scripts/lib/__pycache__/query.cpython-313.pyc +0 -0
  476. package/skills/luckiest-trends/scripts/lib/__pycache__/reddit.cpython-313.pyc +0 -0
  477. package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_arctic.cpython-313.pyc +0 -0
  478. package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_enrich.cpython-313.pyc +0 -0
  479. package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_keyless.cpython-313.pyc +0 -0
  480. package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_listing.cpython-313.pyc +0 -0
  481. package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_public.cpython-313.pyc +0 -0
  482. package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_rss.cpython-313.pyc +0 -0
  483. package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_shreddit.cpython-313.pyc +0 -0
  484. package/skills/luckiest-trends/scripts/lib/__pycache__/relevance.cpython-313.pyc +0 -0
  485. package/skills/luckiest-trends/scripts/lib/__pycache__/render.cpython-313.pyc +0 -0
  486. package/skills/luckiest-trends/scripts/lib/__pycache__/rerank.cpython-313.pyc +0 -0
  487. package/skills/luckiest-trends/scripts/lib/__pycache__/resolve.cpython-313.pyc +0 -0
  488. package/skills/luckiest-trends/scripts/lib/__pycache__/schema.cpython-313.pyc +0 -0
  489. package/skills/luckiest-trends/scripts/lib/__pycache__/signals.cpython-313.pyc +0 -0
  490. package/skills/luckiest-trends/scripts/lib/__pycache__/skill_meta.cpython-313.pyc +0 -0
  491. package/skills/luckiest-trends/scripts/lib/__pycache__/snippet.cpython-313.pyc +0 -0
  492. package/skills/luckiest-trends/scripts/lib/__pycache__/subproc.cpython-313.pyc +0 -0
  493. package/skills/luckiest-trends/scripts/lib/__pycache__/techmeme.cpython-313.pyc +0 -0
  494. package/skills/luckiest-trends/scripts/lib/__pycache__/threads.cpython-313.pyc +0 -0
  495. package/skills/luckiest-trends/scripts/lib/__pycache__/tiktok.cpython-313.pyc +0 -0
  496. package/skills/luckiest-trends/scripts/lib/__pycache__/trustpilot.cpython-313.pyc +0 -0
  497. package/skills/luckiest-trends/scripts/lib/__pycache__/truthsocial.cpython-313.pyc +0 -0
  498. package/skills/luckiest-trends/scripts/lib/__pycache__/ui.cpython-313.pyc +0 -0
  499. package/skills/luckiest-trends/scripts/lib/__pycache__/web_search_keyless.cpython-313.pyc +0 -0
  500. package/skills/luckiest-trends/scripts/lib/__pycache__/xai_x.cpython-313.pyc +0 -0
  501. package/skills/luckiest-trends/scripts/lib/__pycache__/xiaohongshu_api.cpython-313.pyc +0 -0
  502. package/skills/luckiest-trends/scripts/lib/__pycache__/xquik.cpython-313.pyc +0 -0
  503. package/skills/luckiest-trends/scripts/lib/__pycache__/xurl_x.cpython-313.pyc +0 -0
  504. package/skills/luckiest-trends/scripts/lib/__pycache__/youtube_yt.cpython-313.pyc +0 -0
  505. package/skills/luckiest-trends/scripts/lib/arxiv.py +290 -0
  506. package/skills/luckiest-trends/scripts/lib/bird_x.py +639 -0
  507. package/skills/luckiest-trends/scripts/lib/bluesky.py +323 -0
  508. package/skills/luckiest-trends/scripts/lib/categories.py +283 -0
  509. package/skills/luckiest-trends/scripts/lib/chrome_cookies.py +414 -0
  510. package/skills/luckiest-trends/scripts/lib/cjk.py +107 -0
  511. package/skills/luckiest-trends/scripts/lib/cluster.py +271 -0
  512. package/skills/luckiest-trends/scripts/lib/competitors.py +198 -0
  513. package/skills/luckiest-trends/scripts/lib/cookie_extract.py +508 -0
  514. package/skills/luckiest-trends/scripts/lib/dates.py +169 -0
  515. package/skills/luckiest-trends/scripts/lib/dedupe.py +145 -0
  516. package/skills/luckiest-trends/scripts/lib/digg.py +440 -0
  517. package/skills/luckiest-trends/scripts/lib/entity_extract.py +127 -0
  518. package/skills/luckiest-trends/scripts/lib/env.py +1073 -0
  519. package/skills/luckiest-trends/scripts/lib/fanout.py +84 -0
  520. package/skills/luckiest-trends/scripts/lib/fusion.py +207 -0
  521. package/skills/luckiest-trends/scripts/lib/github.py +996 -0
  522. package/skills/luckiest-trends/scripts/lib/grounding.py +331 -0
  523. package/skills/luckiest-trends/scripts/lib/hackernews.py +357 -0
  524. package/skills/luckiest-trends/scripts/lib/health.py +92 -0
  525. package/skills/luckiest-trends/scripts/lib/hiring_signals.py +326 -0
  526. package/skills/luckiest-trends/scripts/lib/html_publish.py +73 -0
  527. package/skills/luckiest-trends/scripts/lib/html_render.py +738 -0
  528. package/skills/luckiest-trends/scripts/lib/http.py +372 -0
  529. package/skills/luckiest-trends/scripts/lib/instagram.py +521 -0
  530. package/skills/luckiest-trends/scripts/lib/jobs.py +708 -0
  531. package/skills/luckiest-trends/scripts/lib/linkedin.py +363 -0
  532. package/skills/luckiest-trends/scripts/lib/log.py +35 -0
  533. package/skills/luckiest-trends/scripts/lib/normalize.py +759 -0
  534. package/skills/luckiest-trends/scripts/lib/permission_preflight.py +242 -0
  535. package/skills/luckiest-trends/scripts/lib/perplexity.py +745 -0
  536. package/skills/luckiest-trends/scripts/lib/pinterest.py +154 -0
  537. package/skills/luckiest-trends/scripts/lib/pipeline.py +1720 -0
  538. package/skills/luckiest-trends/scripts/lib/planner.py +763 -0
  539. package/skills/luckiest-trends/scripts/lib/polymarket.py +794 -0
  540. package/skills/luckiest-trends/scripts/lib/preflight.py +119 -0
  541. package/skills/luckiest-trends/scripts/lib/providers.py +380 -0
  542. package/skills/luckiest-trends/scripts/lib/quality_nudge.py +383 -0
  543. package/skills/luckiest-trends/scripts/lib/query.py +202 -0
  544. package/skills/luckiest-trends/scripts/lib/reddit.py +731 -0
  545. package/skills/luckiest-trends/scripts/lib/reddit_arctic.py +92 -0
  546. package/skills/luckiest-trends/scripts/lib/reddit_enrich.py +322 -0
  547. package/skills/luckiest-trends/scripts/lib/reddit_keyless.py +329 -0
  548. package/skills/luckiest-trends/scripts/lib/reddit_listing.py +188 -0
  549. package/skills/luckiest-trends/scripts/lib/reddit_public.py +275 -0
  550. package/skills/luckiest-trends/scripts/lib/reddit_rss.py +224 -0
  551. package/skills/luckiest-trends/scripts/lib/reddit_shreddit.py +184 -0
  552. package/skills/luckiest-trends/scripts/lib/relevance.py +200 -0
  553. package/skills/luckiest-trends/scripts/lib/render.py +2130 -0
  554. package/skills/luckiest-trends/scripts/lib/rerank.py +708 -0
  555. package/skills/luckiest-trends/scripts/lib/resolve.py +345 -0
  556. package/skills/luckiest-trends/scripts/lib/safari_cookies.py +193 -0
  557. package/skills/luckiest-trends/scripts/lib/schema.py +319 -0
  558. package/skills/luckiest-trends/scripts/lib/setup_wizard.py +843 -0
  559. package/skills/luckiest-trends/scripts/lib/signals.py +334 -0
  560. package/skills/luckiest-trends/scripts/lib/skill_meta.py +33 -0
  561. package/skills/luckiest-trends/scripts/lib/snippet.py +51 -0
  562. package/skills/luckiest-trends/scripts/lib/subproc.py +116 -0
  563. package/skills/luckiest-trends/scripts/lib/techmeme.py +239 -0
  564. package/skills/luckiest-trends/scripts/lib/threads.py +207 -0
  565. package/skills/luckiest-trends/scripts/lib/tiktok.py +596 -0
  566. package/skills/luckiest-trends/scripts/lib/transcribe.py +235 -0
  567. package/skills/luckiest-trends/scripts/lib/trustpilot.py +277 -0
  568. package/skills/luckiest-trends/scripts/lib/truthsocial.py +162 -0
  569. package/skills/luckiest-trends/scripts/lib/ui.py +626 -0
  570. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/LICENSE +21 -0
  571. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/bird-search.mjs +147 -0
  572. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/cookies.js +220 -0
  573. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/features.json +17 -0
  574. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/paginate-cursor.js +37 -0
  575. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/query-ids.json +20 -0
  576. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/runtime-features.js +151 -0
  577. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/runtime-query-ids.js +264 -0
  578. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/twitter-client-base.js +129 -0
  579. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/twitter-client-constants.js +50 -0
  580. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/twitter-client-features.js +347 -0
  581. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/twitter-client-search.js +157 -0
  582. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/twitter-client-types.js +2 -0
  583. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/twitter-client-utils.js +511 -0
  584. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/package.json +13 -0
  585. package/skills/luckiest-trends/scripts/lib/web_fetch_keyless.py +105 -0
  586. package/skills/luckiest-trends/scripts/lib/web_search_keyless.py +158 -0
  587. package/skills/luckiest-trends/scripts/lib/xai_x.py +236 -0
  588. package/skills/luckiest-trends/scripts/lib/xiaohongshu_api.py +162 -0
  589. package/skills/luckiest-trends/scripts/lib/xquik.py +404 -0
  590. package/skills/luckiest-trends/scripts/lib/xurl_x.py +171 -0
  591. package/skills/luckiest-trends/scripts/lib/youtube_yt.py +1365 -0
  592. package/skills/luckiest-trends/scripts/setup-keychain.sh +123 -0
  593. package/skills/luckiest-trends/scripts/setup-pass.sh +132 -0
  594. package/skills/luckiest-trends/scripts/store.py +1020 -0
  595. package/skills/luckiest-trends/scripts/test-v1-vs-v2.sh +220 -0
  596. package/skills/luckiest-trends/scripts/test_device_auth.py +128 -0
  597. package/skills/luckiest-trends/scripts/verify_v3.py +195 -0
  598. package/skills/luckiest-trends/scripts/watchlist.py +318 -0
  599. package/skills/luckiest-video/ATTRIBUTION.md +27 -0
  600. package/skills/luckiest-video/CHANGELOG.md +23 -0
  601. package/skills/luckiest-video/SKILL.md +371 -0
  602. package/skills/luckiest-video/evals/evals.json +88 -0
  603. package/skills/luckiest-video/references/ai-video-prompting.md +175 -0
  604. package/skills/remix/SKILL.md +32 -0
  605. package/commands/luckiest/go.md +0 -45
  606. package/commands/luckiest/plan.md +0 -55
  607. /package/commands/{luckiest/charms.md → charms.md} +0 -0
  608. /package/commands/{luckiest/helpers.md → helpers.md} +0 -0
  609. /package/commands/{luckiest/home.md → home.md} +0 -0
  610. /package/commands/{luckiest/leaderboard.md → leaderboard.md} +0 -0
  611. /package/commands/{luckiest/merge.md → merge.md} +0 -0
  612. /package/commands/{luckiest/overlaps.md → overlaps.md} +0 -0
  613. /package/commands/{luckiest/skills.md → skills.md} +0 -0
  614. /package/commands/{luckiest/start.md → start.md} +0 -0
  615. /package/commands/{luckiest/status.md → status.md} +0 -0
  616. /package/commands/{luckiest/updates.md → updates.md} +0 -0
  617. /package/commands/{luckiest/vouch.md → vouch.md} +0 -0
  618. /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 ''