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,1467 @@
1
+ #!/usr/bin/env python3
2
+ # ruff: noqa: E402
3
+ """last30days CLI."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import argparse
8
+ import atexit
9
+ import datetime
10
+ import json
11
+ import os
12
+ import re
13
+ import signal
14
+ import sys
15
+ import threading
16
+ from pathlib import Path
17
+
18
+ MIN_PYTHON = (3, 12)
19
+
20
+
21
+ def ensure_supported_python(version_info: tuple[int, int, int] | object | None = None) -> None:
22
+ if version_info is None:
23
+ version_info = sys.version_info
24
+ major, minor, micro = tuple(version_info[:3])
25
+ if (major, minor) >= MIN_PYTHON:
26
+ return
27
+ req = f"{MIN_PYTHON[0]}.{MIN_PYTHON[1]}"
28
+ sys.stderr.write(
29
+ f"last30days v3 requires Python {req}+.\n"
30
+ f"Detected Python {major}.{minor}.{micro}.\n"
31
+ f"Install with:\n"
32
+ f" Mac: brew install python@{req}\n"
33
+ f" Windows: winget install Python.Python.{req}\n"
34
+ f" Linux: sudo apt install python{req} (or pyenv install {req})\n"
35
+ f"Then rerun: python{req} <path-to-script> setup\n"
36
+ )
37
+ raise SystemExit(1)
38
+
39
+
40
+ ensure_supported_python()
41
+
42
+ if os.name == "nt":
43
+ for stream in (sys.stdout, sys.stderr):
44
+ if hasattr(stream, "reconfigure"):
45
+ stream.reconfigure(encoding="utf-8", errors="replace")
46
+
47
+ SCRIPT_DIR = Path(__file__).parent.resolve()
48
+ sys.path.insert(0, str(SCRIPT_DIR))
49
+
50
+ from lib import dates, env, html_render, permission_preflight, pipeline, render, schema, ui
51
+
52
+ _child_pids: set[int] = set()
53
+ _child_pids_lock = threading.Lock()
54
+
55
+
56
+ def register_child_pid(pid: int) -> None:
57
+ with _child_pids_lock:
58
+ _child_pids.add(pid)
59
+
60
+
61
+ def unregister_child_pid(pid: int) -> None:
62
+ with _child_pids_lock:
63
+ _child_pids.discard(pid)
64
+
65
+
66
+ def _cleanup_children() -> None:
67
+ with _child_pids_lock:
68
+ pids = list(_child_pids)
69
+ for pid in pids:
70
+ try:
71
+ if hasattr(os, "killpg"):
72
+ os.killpg(os.getpgid(pid), signal.SIGTERM)
73
+ else:
74
+ os.kill(pid, signal.SIGTERM)
75
+ except (ProcessLookupError, PermissionError, OSError):
76
+ continue
77
+
78
+
79
+ atexit.register(_cleanup_children)
80
+
81
+
82
+ def parse_search_flag(raw: str, flag_name: str = "--search") -> list[str]:
83
+ sources = []
84
+ for source in raw.split(","):
85
+ source = source.strip().lower()
86
+ if not source:
87
+ continue
88
+ normalized = pipeline.SEARCH_ALIAS.get(source, source)
89
+ if normalized not in pipeline.MOCK_AVAILABLE_SOURCES:
90
+ raise SystemExit(f"Unknown search source in {flag_name}: {source}")
91
+ if normalized not in sources:
92
+ sources.append(normalized)
93
+ if not sources:
94
+ raise SystemExit(f"{flag_name} requires at least one source.")
95
+ return sources
96
+
97
+ def parse_as_of_date_arg(value: str) -> str:
98
+ try:
99
+ parsed = dates.parse_as_of_date(value)
100
+ except ValueError as exc:
101
+ raise argparse.ArgumentTypeError(str(exc)) from exc
102
+ return parsed
103
+
104
+ def resolve_requested_sources(args_search: str | None, config: dict) -> list[str] | None:
105
+ """Resolve the requested source set: explicit --search wins, then the
106
+ LAST30DAYS_DEFAULT_SEARCH config key (env var or .env file), then None
107
+ (per-query default behavior). The config fallback lets users pin a fixed
108
+ source set that survives upgrades without patching SKILL.md (#442).
109
+ """
110
+ if args_search:
111
+ return parse_search_flag(args_search)
112
+ default_search = (config.get("LAST30DAYS_DEFAULT_SEARCH") or "").strip()
113
+ if default_search:
114
+ return parse_search_flag(default_search, flag_name="LAST30DAYS_DEFAULT_SEARCH")
115
+ return None
116
+
117
+
118
+ def slugify(value: str) -> str:
119
+ slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
120
+ return slug or "last30days"
121
+
122
+
123
+ def save_output(
124
+ report: schema.Report,
125
+ emit: str,
126
+ save_dir: str,
127
+ suffix: str = "",
128
+ synthesis_md: str | None = None,
129
+ topic_override: str | None = None,
130
+ rendered_content: str | None = None,
131
+ ) -> Path:
132
+ from datetime import datetime
133
+ path = Path(save_dir).expanduser().resolve()
134
+ path.mkdir(parents=True, exist_ok=True)
135
+ slug = slugify(topic_override or report.topic)
136
+ extension = "json" if emit == "json" else "html" if emit == "html" else "md"
137
+ raw_label = "raw-html" if emit == "html" else "raw"
138
+ suffix_part = f"-{suffix}" if suffix else ""
139
+ out_path = path / f"{slug}-{raw_label}{suffix_part}.{extension}"
140
+ if out_path.exists():
141
+ out_path = path / f"{slug}-{raw_label}{suffix_part}-{datetime.now().strftime('%Y-%m-%d')}.{extension}"
142
+ # Markdown saves keep the complete debug artifact. JSON and HTML preserve
143
+ # their requested wire format so file extensions match their content.
144
+ if rendered_content is not None:
145
+ content = rendered_content
146
+ elif emit in {"json", "html"}:
147
+ content = emit_output(report, emit, synthesis_md=synthesis_md)
148
+ else:
149
+ content = render.render_full(report)
150
+ out_path.write_text(content, encoding="utf-8")
151
+ return out_path
152
+
153
+
154
+ def save_rendered_output(rendered_content: str, output_file: str) -> Path:
155
+ out_path = Path(output_file).expanduser().resolve()
156
+ out_path.parent.mkdir(parents=True, exist_ok=True)
157
+ out_path.write_text(rendered_content, encoding="utf-8")
158
+ return out_path
159
+
160
+
161
+ def _publish_metadata_path(html_path: Path) -> Path:
162
+ return html_path.with_name(f"{html_path.name}.publish.json")
163
+
164
+
165
+ def _write_publish_metadata(html_path: Path, publish_result: dict[str, object]) -> None:
166
+ payload = {
167
+ "url": publish_result.get("url"),
168
+ "site_id": publish_result.get("site_id"),
169
+ "status": publish_result.get("status"),
170
+ "published_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
171
+ }
172
+ _publish_metadata_path(html_path).write_text(json.dumps(payload, indent=2), encoding="utf-8")
173
+
174
+
175
+ def publish_rendered_html(
176
+ rendered: str,
177
+ *,
178
+ password: str | None = None,
179
+ companion_paths: list[Path] | None = None,
180
+ ) -> dict[str, object]:
181
+ from lib import html_publish
182
+
183
+ result = html_publish.publish_html(rendered, password=password)
184
+ metadata_errors: list[str] = []
185
+ for path in companion_paths or []:
186
+ try:
187
+ _write_publish_metadata(path, result)
188
+ except OSError as exc:
189
+ metadata_errors.append(f"{path}: {exc}")
190
+ if metadata_errors:
191
+ result = dict(result)
192
+ result["_metadata_errors"] = metadata_errors
193
+ return result
194
+
195
+
196
+ def _publish_password_for_args(args: argparse.Namespace) -> str | None:
197
+ return (args.publish_password or os.environ.get("LAST30DAYS_PUBLISH_PASSWORD") or None)
198
+
199
+
200
+ def emit_output(
201
+ report: schema.Report,
202
+ emit: str,
203
+ fun_level: str = "medium",
204
+ save_path: str | None = None,
205
+ synthesis_md: str | None = None,
206
+ ) -> str:
207
+ if emit == "json":
208
+ return json.dumps(schema.to_dict(report), indent=2, sort_keys=True)
209
+ if emit == "html":
210
+ return html_render.render_html(
211
+ report, fun_level=fun_level, save_path=save_path, synthesis_md=synthesis_md,
212
+ )
213
+ if emit in {"compact", "md"}:
214
+ return render.render_compact(report, fun_level=fun_level, save_path=save_path)
215
+ if emit == "context":
216
+ return render.render_context(report)
217
+ if emit == "brief":
218
+ return render.render_brief(report)
219
+ raise SystemExit(f"Unsupported emit mode: {emit}")
220
+
221
+
222
+ def emit_comparison_output(
223
+ entity_reports: list[tuple[str, schema.Report]],
224
+ emit: str,
225
+ fun_level: str = "medium",
226
+ save_path: str | None = None,
227
+ synthesis_md: str | None = None,
228
+ ) -> str:
229
+ if emit == "json":
230
+ payload = {
231
+ "comparison": True,
232
+ "entities": [label for label, _ in entity_reports],
233
+ "reports": [
234
+ {"entity": label, "report": schema.to_dict(report)}
235
+ for label, report in entity_reports
236
+ ],
237
+ }
238
+ return json.dumps(payload, indent=2, sort_keys=True)
239
+ if emit == "html":
240
+ return html_render.render_html_comparison(
241
+ entity_reports,
242
+ fun_level=fun_level,
243
+ save_path=save_path,
244
+ synthesis_md=synthesis_md,
245
+ )
246
+ if emit in {"compact", "md"}:
247
+ return render.render_comparison_multi(
248
+ entity_reports, fun_level=fun_level, save_path=save_path,
249
+ )
250
+ if emit == "context":
251
+ return render.render_comparison_multi_context(entity_reports)
252
+ raise SystemExit(f"Unsupported emit mode: {emit}")
253
+
254
+
255
+ def comparison_topic(entity_reports: list[tuple[str, schema.Report]]) -> str:
256
+ return " vs ".join(label for label, _ in entity_reports)
257
+
258
+
259
+ def compute_save_path_display(save_dir: str, topic: str, suffix: str, emit: str) -> str:
260
+ """Compute the user-friendly save path string that will be shown in the footer.
261
+
262
+ Uses ~ when the saved file is under the user's home directory; otherwise
263
+ returns the absolute path.
264
+ """
265
+ from pathlib import Path as _Path
266
+ path = _Path(save_dir).expanduser().resolve()
267
+ slug = slugify(topic)
268
+ extension = "json" if emit == "json" else "html" if emit == "html" else "md"
269
+ raw_label = "raw-html" if emit == "html" else "raw"
270
+ suffix_part = f"-{suffix}" if suffix else ""
271
+ raw = path / f"{slug}-{raw_label}{suffix_part}.{extension}"
272
+ try:
273
+ home = _Path.home().resolve()
274
+ relative = raw.relative_to(home)
275
+ return f"~/{relative.as_posix()}"
276
+ except ValueError:
277
+ return raw.as_posix()
278
+
279
+
280
+ def compute_output_path_display(output_file: str) -> str:
281
+ """Compute the user-friendly explicit output path shown in render footers."""
282
+ raw = Path(output_file).expanduser().resolve()
283
+ try:
284
+ home = Path.home().resolve()
285
+ relative = raw.relative_to(home)
286
+ return f"~/{relative.as_posix()}"
287
+ except ValueError:
288
+ return raw.as_posix()
289
+
290
+
291
+ def read_synthesis_file(path: str) -> str:
292
+ try:
293
+ return Path(path).expanduser().read_text(encoding="utf-8")
294
+ except OSError as exc:
295
+ sys.stderr.write(f"[last30days] Cannot read --synthesis-file: {exc}\n")
296
+ raise SystemExit(2)
297
+
298
+
299
+ def persist_report(report: schema.Report) -> dict[str, int]:
300
+ import store
301
+
302
+ store.init_db()
303
+ topic_row = store.add_topic(report.topic)
304
+ topic_id = topic_row["id"]
305
+ source_mode = ",".join(sorted(report.items_by_source)) or "v3"
306
+ run_id = store.record_run(topic_id, source_mode=source_mode, status="running")
307
+ try:
308
+ findings = store.findings_from_report(report)
309
+ counts = store.store_findings(run_id, topic_id, findings)
310
+ store.update_run(
311
+ run_id,
312
+ status="completed",
313
+ findings_new=counts["new"],
314
+ findings_updated=counts["updated"],
315
+ )
316
+ return counts
317
+ except Exception as exc:
318
+ store.update_run(run_id, status="failed", error_message=str(exc)[:500])
319
+ raise
320
+
321
+
322
+ def build_parser() -> argparse.ArgumentParser:
323
+ parser = argparse.ArgumentParser(
324
+ description="Research a topic across live social, market, and grounded web sources.",
325
+ allow_abbrev=False,
326
+ )
327
+ parser.add_argument("topic", nargs="*", help="Research topic")
328
+ parser.add_argument("--emit", default="compact", choices=["compact", "json", "context", "md", "html", "brief"])
329
+ parser.add_argument("--search", help="Comma-separated source list")
330
+ parser.add_argument("--quick", action="store_true", help="Lower-latency retrieval profile")
331
+ parser.add_argument("--deep", action="store_true", help="Higher-recall retrieval profile")
332
+ parser.add_argument("--debug", action="store_true", help="Enable HTTP debug logging")
333
+ parser.add_argument("--mock", action="store_true", help="Use mock retrieval fixtures")
334
+ parser.add_argument("--diagnose", action="store_true", help="Print provider and source availability")
335
+ parser.add_argument("--preflight", action="store_true",
336
+ help="Print a safe human-readable permission preflight")
337
+ parser.add_argument("--preflight-report-on-save-dir", help=argparse.SUPPRESS)
338
+ parser.add_argument("--no-browser-cookies", action="store_true",
339
+ help="Disable browser-cookie extraction even when FROM_BROWSER is configured")
340
+ parser.add_argument("--save-dir", help="Optional directory for saving the rendered output")
341
+ parser.add_argument("--output", help="Optional exact file path for saving the rendered output")
342
+ parser.add_argument("--synthesis-file", help="Markdown synthesis to embed in --emit=html output")
343
+ parser.add_argument("--publish-html", action="store_true",
344
+ help="Publish --emit=html output to ht-ml.app (explicit opt-in; public by default)")
345
+ parser.add_argument("--publish-password",
346
+ help="Optional shared password for --publish-html; prefer LAST30DAYS_PUBLISH_PASSWORD to avoid exposing secrets in process lists")
347
+ parser.add_argument("--store", action="store_true", help="Persist ranked findings to the SQLite research store")
348
+ parser.add_argument("--x-handle", help="X handle for targeted supplemental search")
349
+ parser.add_argument("--x-related", help="Comma-separated related X handles (searched with lower weight)")
350
+ parser.add_argument("--web-backend", default="auto",
351
+ choices=["auto", "brave", "exa", "serper", "parallel", "none"],
352
+ help="Web search backend (default: auto, tries Brave then Exa then Serper then Parallel)")
353
+ parser.add_argument("--deep-research", action="store_true",
354
+ help="Use Perplexity Deep Research (~$0.90/query) for in-depth analysis. Requires PERPLEXITY_API_KEY or OPENROUTER_API_KEY.")
355
+ parser.add_argument("--hiring-signals", action="store_true",
356
+ help="Analyze public jobs/careers postings as evidence-backed company focus signals.")
357
+ parser.add_argument("--plan", help="JSON query plan (skips internal LLM planner). Can be a JSON string or a file path.")
358
+ parser.add_argument("--save-suffix", help="Suffix for saved output filename (e.g., 'gemini' → kanye-west-raw-gemini.md)")
359
+ parser.add_argument("--subreddits", help="Comma-separated broad/category subreddit names to search (e.g., SaaS,Entrepreneur)")
360
+ parser.add_argument("--dedicated-subreddits", help="Comma-separated entity-home subreddit names (e.g., Kanye,WestSubEver). Pulled in full (top+hot+new) and exempt from the relevance floor since the whole sub is the topic.")
361
+ parser.add_argument("--tiktok-hashtags", help="Comma-separated TikTok hashtags without # (e.g., tella,screenrecording)")
362
+ parser.add_argument("--tiktok-creators", help="Comma-separated TikTok creator handles (e.g., TellaHQ,taborplace)")
363
+ parser.add_argument("--ig-creators", help="Comma-separated Instagram creator handles (e.g., tella.tv,laborstories)")
364
+ parser.add_argument(
365
+ "--days",
366
+ "--lookback-days",
367
+ dest="lookback_days",
368
+ type=int,
369
+ default=30,
370
+ help="Number of days to look back for research (default: 30, watchlist uses 90)",
371
+ )
372
+ parser.add_argument(
373
+ "--as-of",
374
+ dest="as_of_date",
375
+ type=parse_as_of_date_arg,
376
+ help=(
377
+ "End date for the lookback window in YYYY-MM-DD format. "
378
+ "When set, --days looks back from this date instead of today."
379
+ ),
380
+ )
381
+ parser.add_argument("--auto-resolve", action="store_true",
382
+ help="Use web search to discover subreddits/handles before planning (for platforms without WebSearch)")
383
+ parser.add_argument("--github-user", help="GitHub username for person-mode search (e.g., steipete)")
384
+ parser.add_argument("--github-repo", help="Comma-separated owner/repo for project-mode search (e.g., openclaw/openclaw,paperclipai/paperclip)")
385
+ parser.add_argument(
386
+ "--competitors",
387
+ nargs="?",
388
+ const=2,
389
+ type=int,
390
+ default=None,
391
+ metavar="N",
392
+ help="Auto-discover N competitor entities and fan out last30days across all of them as a comparison (default N=2 → 3-way: original + 2 peers; range 1..6). Use --competitors-list to override discovery.",
393
+ )
394
+ parser.add_argument(
395
+ "--competitors-list",
396
+ dest="competitors_list",
397
+ help="Comma-separated competitor entities to skip discovery (e.g., 'Anthropic,xAI,Google Gemini'). Implies --competitors.",
398
+ )
399
+ parser.add_argument(
400
+ "--polymarket-keywords",
401
+ dest="polymarket_keywords",
402
+ help=(
403
+ "Comma-separated keywords that Polymarket market titles must match "
404
+ "to be included. Use for ambiguous single-token topics like 'Warriors' "
405
+ "(nba,gsw,golden-state) to filter out Glasgow Warriors rugby, Honor "
406
+ "of Kings Rogue Warriors, etc. When omitted, Polymarket returns all "
407
+ "matching markets — so expect cross-entity noise on generic topics."
408
+ ),
409
+ )
410
+ parser.add_argument(
411
+ "--competitors-plan",
412
+ dest="competitors_plan",
413
+ help=(
414
+ "JSON mapping of per-entity Step 0.55 targeting for competitor / vs-mode "
415
+ "sub-runs. Schema: {entity_name: {x_handle?, x_related?, subreddits?, "
416
+ "github_user?, github_repos?, context?}}. Accepts inline JSON or a file "
417
+ "path. Implies --competitors. Preferred over --competitors-list when the "
418
+ "hosting model has already resolved per-entity handles and subs."
419
+ ),
420
+ )
421
+ return parser
422
+
423
+
424
+ def parse_competitors_plan(raw: str | None) -> dict[str, dict]:
425
+ """Parse a --competitors-plan argument into a {entity_name_lower: plan_entry} dict.
426
+
427
+ Accepts inline JSON or a file path (matches --plan). Returns {} on None/empty.
428
+ Validation: top-level must be a dict; each value must be a dict. Unknown fields
429
+ in entry values log a warning but do not abort. Invalid JSON or non-dict shape
430
+ raises SystemExit(2) with a clear stderr message.
431
+ """
432
+ if not raw:
433
+ return {}
434
+ plan_str = raw
435
+ if os.path.isfile(plan_str):
436
+ try:
437
+ plan_str = open(plan_str, encoding="utf-8").read()
438
+ except (OSError, UnicodeDecodeError) as exc:
439
+ sys.stderr.write(f"[CompetitorsPlan] Cannot read plan file: {exc}\n")
440
+ raise SystemExit(2)
441
+ try:
442
+ parsed = json.loads(plan_str)
443
+ except json.JSONDecodeError as exc:
444
+ sys.stderr.write(f"[CompetitorsPlan] Invalid JSON: {exc}\n")
445
+ raise SystemExit(2)
446
+ if not isinstance(parsed, dict):
447
+ sys.stderr.write(
448
+ f"[CompetitorsPlan] Top-level must be a dict of "
449
+ f"{{entity: {{targeting}}}}, got {type(parsed).__name__}\n"
450
+ )
451
+ raise SystemExit(2)
452
+ known_fields = {
453
+ "x_handle", "x_related", "subreddits",
454
+ "github_user", "github_repos", "context",
455
+ }
456
+ normalized: dict[str, dict] = {}
457
+ for entity, entry in parsed.items():
458
+ if not isinstance(entry, dict):
459
+ sys.stderr.write(
460
+ f"[CompetitorsPlan] Entry for {entity!r} must be a dict, "
461
+ f"got {type(entry).__name__}; skipping.\n"
462
+ )
463
+ continue
464
+ unknown = set(entry.keys()) - known_fields
465
+ if unknown:
466
+ sys.stderr.write(
467
+ f"[CompetitorsPlan] Unknown fields in {entity!r}: "
468
+ f"{sorted(unknown)}; ignoring.\n"
469
+ )
470
+ normalized[entity.strip().lower()] = {
471
+ k: v for k, v in entry.items() if k in known_fields
472
+ }
473
+ return normalized
474
+
475
+
476
+ def subrun_kwargs_for(
477
+ entity: str,
478
+ plan_entry: dict,
479
+ *,
480
+ resolved: dict,
481
+ ) -> dict:
482
+ """Build an explicit per-entity kwargs dict for pipeline.run().
483
+
484
+ Plan values win over auto_resolve values. Returns keys for all per-entity
485
+ targeting flags so callers never fall through to closure defaults.
486
+
487
+ This helper is the single source of truth for sub-run kwargs — main-topic
488
+ flags can only leak if a caller bypasses it.
489
+ """
490
+ def _choose(plan_key: str, resolved_key: str | None = None):
491
+ if plan_key in plan_entry and plan_entry[plan_key]:
492
+ return plan_entry[plan_key]
493
+ if resolved_key is not None and resolved.get(resolved_key):
494
+ return resolved[resolved_key]
495
+ return None
496
+
497
+ x_handle = _choose("x_handle", "x_handle")
498
+ if isinstance(x_handle, str):
499
+ x_handle = x_handle.lstrip("@") or None
500
+
501
+ subreddits = _choose("subreddits", "subreddits")
502
+ if isinstance(subreddits, list):
503
+ subreddits = [s.strip().removeprefix("r/") for s in subreddits if s.strip()] or None
504
+
505
+ x_related = plan_entry.get("x_related")
506
+ if isinstance(x_related, list):
507
+ x_related = [h.strip().lstrip("@") for h in x_related if h.strip()] or None
508
+ else:
509
+ x_related = None
510
+
511
+ github_user = _choose("github_user", "github_user")
512
+ if isinstance(github_user, str):
513
+ github_user = github_user.lstrip("@").lower() or None
514
+
515
+ github_repos = _choose("github_repos", "github_repos")
516
+ if isinstance(github_repos, list):
517
+ github_repos = [r.strip() for r in github_repos if r.strip() and "/" in r.strip()] or None
518
+
519
+ context = plan_entry.get("context") or resolved.get("context") or ""
520
+
521
+ return {
522
+ "x_handle": x_handle,
523
+ "x_related": x_related,
524
+ "subreddits": subreddits,
525
+ "github_user": github_user,
526
+ "github_repos": github_repos,
527
+ "_context": context,
528
+ }
529
+
530
+
531
+ COMPETITORS_MIN = 1
532
+ COMPETITORS_MAX = 6
533
+ COMPETITORS_DEFAULT = 2
534
+
535
+
536
+ def resolve_competitors_args(args: argparse.Namespace) -> tuple[bool, int, list[str]]:
537
+ """Normalize --competitors / --competitors-list into (enabled, count, explicit_list).
538
+
539
+ - (False, 0, []) when neither flag is set.
540
+ - An explicit list always wins; count is derived from list length.
541
+ - A numeric count outside [1, 6] is clamped with a stderr warning.
542
+ - count <= 0 (explicit) raises SystemExit(2).
543
+ """
544
+ explicit_list: list[str] = []
545
+ list_flag_provided = args.competitors_list is not None
546
+ if list_flag_provided:
547
+ explicit_list = [
548
+ entity.strip()
549
+ for entity in args.competitors_list.split(",")
550
+ if entity.strip()
551
+ ]
552
+ if not explicit_list:
553
+ sys.stderr.write("[Competitors] --competitors-list is empty.\n")
554
+ raise SystemExit(2)
555
+
556
+ competitors_flag = args.competitors
557
+ list_present = bool(explicit_list)
558
+ flag_present = competitors_flag is not None
559
+
560
+ if not list_present and not flag_present:
561
+ return False, 0, []
562
+
563
+ if list_present:
564
+ count = len(explicit_list)
565
+ if flag_present and competitors_flag != count:
566
+ sys.stderr.write(
567
+ f"[Competitors] --competitors={competitors_flag} ignored; using "
568
+ f"{count} entries from --competitors-list.\n"
569
+ )
570
+ if count > COMPETITORS_MAX:
571
+ sys.stderr.write(
572
+ f"[Competitors] --competitors-list has {count} entries, clamping to {COMPETITORS_MAX}.\n"
573
+ )
574
+ explicit_list = explicit_list[:COMPETITORS_MAX]
575
+ count = COMPETITORS_MAX
576
+ return True, count, explicit_list
577
+
578
+ # flag_present, no explicit list
579
+ count = competitors_flag
580
+ if count < COMPETITORS_MIN:
581
+ sys.stderr.write(
582
+ f"[Competitors] --competitors must be >= {COMPETITORS_MIN} (got {count}).\n"
583
+ )
584
+ raise SystemExit(2)
585
+ if count > COMPETITORS_MAX:
586
+ sys.stderr.write(
587
+ f"[Competitors] --competitors={count} exceeds max {COMPETITORS_MAX}; clamping.\n"
588
+ )
589
+ count = COMPETITORS_MAX
590
+ return True, count, []
591
+
592
+
593
+ def _missing_sources_for_promo(diag: dict[str, object]) -> str | None:
594
+ available = set(diag.get("available_sources") or [])
595
+ missing = []
596
+ if "reddit" not in available:
597
+ missing.append("reddit")
598
+ if "x" not in available:
599
+ missing.append("x")
600
+ # The web promo nudges toward a paid backend for higher-quality web search.
601
+ # Grounding is now available keyless on non-native hosts, so key the promo on
602
+ # the absence of a *paid* backend, not on grounding availability. Suppress it
603
+ # entirely on native-search hosts, where the model's own search is better and
604
+ # setting a paid engine key would be the wrong advice.
605
+ if not diag.get("native_web_backend") and not diag.get("native_search"):
606
+ missing.append("web")
607
+ if not missing:
608
+ return None
609
+ if "reddit" in missing and "x" in missing:
610
+ return "both"
611
+ return missing[0]
612
+
613
+
614
+ def _show_runtime_ui(
615
+ report: schema.Report,
616
+ progress: ui.ProgressDisplay,
617
+ diag: dict[str, object],
618
+ suppress_web_promo: bool = False,
619
+ ) -> None:
620
+ counts = {source: len(items) for source, items in report.items_by_source.items()}
621
+ display_sources = list(
622
+ dict.fromkeys(
623
+ [
624
+ *report.query_plan.source_weights.keys(),
625
+ *report.items_by_source.keys(),
626
+ *report.errors_by_source.keys(),
627
+ ]
628
+ )
629
+ )
630
+ progress.end_processing()
631
+ progress.show_complete(
632
+ source_counts=counts,
633
+ display_sources=display_sources,
634
+ )
635
+ promo = _missing_sources_for_promo(diag)
636
+ # The `web` promo nudges users to set BRAVE_API_KEY / SERPER_API_KEY, which
637
+ # is wrong advice when a hosting reasoning model (Claude Code, Codex,
638
+ # Hermes, Gemini) is driving — those already have WebSearch and can
639
+ # pre-resolve Step 0.55 themselves. Suppress the web promo when a hosting
640
+ # model signal is present (--plan or --competitors-plan was passed).
641
+ if promo:
642
+ if suppress_web_promo and promo == "web":
643
+ return
644
+ if suppress_web_promo and promo == "both":
645
+ # "both" means reddit + web both missing; still nudge reddit but
646
+ # skip the web line. show_promo has a per-source variant.
647
+ progress.show_promo("reddit", diag=diag)
648
+ return
649
+ progress.show_promo(promo, diag=diag)
650
+
651
+
652
+ REPORT_CACHE_VERSION = "last30days-report-cache/v1"
653
+ DEFAULT_REPORT_CACHE_TTL_SECONDS = 3600
654
+
655
+
656
+ def _last_report_cache_path() -> Path | None:
657
+ if env.CONFIG_DIR is None:
658
+ return None
659
+ return env.CONFIG_DIR / "last-report.json"
660
+
661
+
662
+ def _report_cache_ttl_seconds(config: dict[str, object]) -> int:
663
+ raw = os.environ.get("LAST30DAYS_REPORT_CACHE_TTL_SECONDS")
664
+ if raw is None:
665
+ raw = config.get("LAST30DAYS_REPORT_CACHE_TTL_SECONDS")
666
+ if raw is None or raw == "":
667
+ return DEFAULT_REPORT_CACHE_TTL_SECONDS
668
+ try:
669
+ return max(0, int(raw))
670
+ except (TypeError, ValueError):
671
+ return DEFAULT_REPORT_CACHE_TTL_SECONDS
672
+
673
+
674
+ def _is_report_cache_fresh(timestamp: object, ttl_seconds: int) -> bool:
675
+ if ttl_seconds <= 0:
676
+ return False
677
+ if not isinstance(timestamp, str) or not timestamp:
678
+ return False
679
+ try:
680
+ created_at = datetime.datetime.fromisoformat(timestamp)
681
+ except ValueError:
682
+ return False
683
+ if created_at.tzinfo is None:
684
+ created_at = created_at.replace(tzinfo=datetime.timezone.utc)
685
+ age = datetime.datetime.now(datetime.timezone.utc) - created_at.astimezone(
686
+ datetime.timezone.utc
687
+ )
688
+ return age.total_seconds() <= ttl_seconds
689
+
690
+
691
+ def _write_last_run(
692
+ topic: str,
693
+ report: "schema.Report",
694
+ entity_reports: list[tuple[str, schema.Report]] | None = None,
695
+ ) -> None:
696
+ try:
697
+ if env.CONFIG_DIR is None:
698
+ return
699
+ target = env.CONFIG_DIR
700
+ target.mkdir(parents=True, exist_ok=True)
701
+ counts = {source: len(items) for source, items in report.items_by_source.items()}
702
+ payload = {
703
+ "topic": topic,
704
+ "timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
705
+ "sources": counts,
706
+ "total": sum(counts.values()),
707
+ "report_cache": str(target / "last-report.json"),
708
+ "comparison": bool(entity_reports),
709
+ }
710
+ (target / "last-run.json").write_text(json.dumps(payload, indent=2))
711
+ cached_reports = entity_reports or [(report.topic, report)]
712
+ cache_payload = {
713
+ "schema": REPORT_CACHE_VERSION,
714
+ "topic": topic,
715
+ "timestamp": payload["timestamp"],
716
+ "comparison": bool(entity_reports),
717
+ "reports": [
718
+ {"entity": label, "report": schema.to_dict(cached_report)}
719
+ for label, cached_report in cached_reports
720
+ ],
721
+ }
722
+ (target / "last-report.json").write_text(json.dumps(cache_payload, indent=2))
723
+ except Exception:
724
+ pass
725
+
726
+
727
+ def _load_last_report_cache(
728
+ topic: str,
729
+ ttl_seconds: int = DEFAULT_REPORT_CACHE_TTL_SECONDS,
730
+ ) -> tuple[schema.Report, list[tuple[str, schema.Report]] | None, Path] | None:
731
+ cache_path = _last_report_cache_path()
732
+ if cache_path is None or not cache_path.exists():
733
+ return None
734
+ try:
735
+ payload = json.loads(cache_path.read_text(encoding="utf-8"))
736
+ if payload.get("schema") != REPORT_CACHE_VERSION:
737
+ return None
738
+ if not _is_report_cache_fresh(payload.get("timestamp"), ttl_seconds):
739
+ return None
740
+ cached_topic = str(payload.get("topic") or "").strip().lower()
741
+ if cached_topic != topic.strip().lower():
742
+ return None
743
+ reports_payload = payload.get("reports") or []
744
+ if not reports_payload:
745
+ return None
746
+ entity_reports = [
747
+ (str(item.get("entity") or ""), schema.report_from_dict(item["report"]))
748
+ for item in reports_payload
749
+ if isinstance(item, dict) and isinstance(item.get("report"), dict)
750
+ ]
751
+ if not entity_reports:
752
+ return None
753
+ if payload.get("comparison"):
754
+ if len(entity_reports) < 2:
755
+ return None
756
+ if len(entity_reports) != len(reports_payload):
757
+ return None
758
+ return entity_reports[0][1], entity_reports, cache_path
759
+ return entity_reports[0][1], None, cache_path
760
+ except Exception:
761
+ return None
762
+
763
+
764
+ def _render_save_and_print(
765
+ args: argparse.Namespace,
766
+ report: schema.Report,
767
+ entity_reports: list[tuple[str, schema.Report]] | None,
768
+ synthesis_md: str | None,
769
+ config: dict[str, object],
770
+ ) -> int:
771
+ fun_level = str(config.get("FUN_LEVEL", "medium")).lower()
772
+ # Comparison HTML is the one case where the saved file's title and content
773
+ # have to be overridden away from the leading entity's report. Compute the
774
+ # gate once so the footer-display and save-output paths can't disagree.
775
+ is_comparison_html = bool(entity_reports) and args.emit == "html"
776
+ footer_save_path = None
777
+ if args.output:
778
+ footer_save_path = compute_output_path_display(args.output)
779
+ elif args.save_dir:
780
+ save_topic_for_display = comparison_topic(entity_reports) if is_comparison_html else report.topic
781
+ footer_save_path = compute_save_path_display(
782
+ args.save_dir, save_topic_for_display, args.save_suffix or "", args.emit
783
+ )
784
+
785
+ if entity_reports:
786
+ rendered = emit_comparison_output(
787
+ entity_reports,
788
+ args.emit,
789
+ fun_level=fun_level,
790
+ save_path=footer_save_path,
791
+ synthesis_md=synthesis_md,
792
+ )
793
+ else:
794
+ rendered = emit_output(
795
+ report,
796
+ args.emit,
797
+ fun_level=fun_level,
798
+ save_path=footer_save_path,
799
+ synthesis_md=synthesis_md,
800
+ )
801
+ publish_companion_paths: list[Path] = []
802
+ if args.output:
803
+ output_path = save_rendered_output(rendered, args.output)
804
+ if args.emit == "html":
805
+ publish_companion_paths.append(output_path)
806
+ sys.stderr.write(f"[last30days] Saved output to {output_path}\n")
807
+ sys.stderr.flush()
808
+ if args.save_dir:
809
+ # Save the main topic's raw file (single-entity or comparison main).
810
+ save_path = save_output(
811
+ report,
812
+ args.emit,
813
+ args.save_dir,
814
+ suffix=args.save_suffix or "",
815
+ synthesis_md=synthesis_md,
816
+ topic_override=comparison_topic(entity_reports) if is_comparison_html else None,
817
+ rendered_content=rendered if is_comparison_html else None,
818
+ )
819
+ if args.emit == "html":
820
+ publish_companion_paths.append(save_path)
821
+ sys.stderr.write(f"[last30days] Saved output to {save_path}\n")
822
+ comparison_peer_paths: list[Path] = []
823
+ # Competitor / vs-mode: also save a per-entity raw file for each peer.
824
+ # Matches historical vs-mode behavior (N passes -> N save files).
825
+ if entity_reports and len(entity_reports) > 1:
826
+ for label, entity_report in entity_reports[1:]:
827
+ peer_path = save_output(
828
+ entity_report, args.emit, args.save_dir,
829
+ suffix=args.save_suffix or "",
830
+ synthesis_md=synthesis_md,
831
+ )
832
+ comparison_peer_paths.append(peer_path)
833
+ sys.stderr.write(f"[last30days] Saved output to {peer_path}\n")
834
+ peers_display = ", ".join(str(path) for path in comparison_peer_paths)
835
+ sys.stderr.write(
836
+ f"[last30days] Comparison artifact set: main={save_path}; "
837
+ f"peers={peers_display}\n"
838
+ )
839
+ sys.stderr.flush()
840
+ if args.publish_html:
841
+ try:
842
+ publish_result = publish_rendered_html(
843
+ rendered,
844
+ password=_publish_password_for_args(args),
845
+ companion_paths=publish_companion_paths,
846
+ )
847
+ sys.stderr.write(f"[last30days] Published HTML to {publish_result['url']}\n")
848
+ for warning in publish_result.get("_metadata_errors") or []:
849
+ sys.stderr.write(f"[last30days] Publish metadata warning: {warning}\n")
850
+ if publish_result.get("update_key"):
851
+ sys.stderr.write(
852
+ "[last30days] ht-ml.app returned an update key; not writing it "
853
+ "to stdout, HTML, or publish metadata.\n"
854
+ )
855
+ sys.stderr.flush()
856
+ except Exception as exc:
857
+ sys.stderr.write(f"[last30days] HTML publish failed: {exc}\n")
858
+ sys.stderr.flush()
859
+ print(rendered)
860
+ return 0
861
+
862
+
863
+ def _propagate_config_to_environ(config: dict[str, object]) -> None:
864
+ """Push relevant env keys to os.environ so provider modules can read them.
865
+
866
+ The env.get_config() function reads from a .env file, but providers.py
867
+ reads from os.environ directly. Without this, OPENAI_BASE_URL and
868
+ XAI_BASE_URL overrides are silently ignored. This is a no-op for
869
+ keys that are already set in process env.
870
+ """
871
+ for key in ("OPENAI_BASE_URL", "XAI_BASE_URL"):
872
+ val = config.get(key)
873
+ if val and not os.environ.get(key):
874
+ os.environ[key] = val
875
+
876
+
877
+ def _setup_allows_browser_cookies(args: argparse.Namespace, extra_argv: list[str]) -> bool:
878
+ return (
879
+ not args.no_browser_cookies
880
+ and not args.diagnose
881
+ and not args.preflight
882
+ and "--allow-browser-cookies" in extra_argv
883
+ )
884
+
885
+
886
+ SETUP_PASSTHROUGH_FLAGS = {
887
+ "--allow-browser-cookies",
888
+ "--device-auth",
889
+ "--github",
890
+ "--openclaw",
891
+ }
892
+
893
+ SKILL_ONLY_FLAGS = {
894
+ "--agent",
895
+ }
896
+
897
+
898
+ def _validate_extra_argv(parser: argparse.ArgumentParser, topic: str, extra_argv: list[str]) -> None:
899
+ if not extra_argv:
900
+ return
901
+ if topic.lower() == "setup":
902
+ unsupported = [arg for arg in extra_argv if arg not in SETUP_PASSTHROUGH_FLAGS]
903
+ if unsupported:
904
+ parser.error(
905
+ "unsupported setup argument(s): "
906
+ + ", ".join(unsupported)
907
+ + f"; supported setup passthrough flags are {', '.join(sorted(SETUP_PASSTHROUGH_FLAGS))}"
908
+ )
909
+ return
910
+ skill_only = [arg for arg in extra_argv if arg in SKILL_ONLY_FLAGS]
911
+ other_unknown = [arg for arg in extra_argv if arg not in SKILL_ONLY_FLAGS]
912
+ if skill_only:
913
+ message = (
914
+ "unsupported Python CLI argument(s): "
915
+ + ", ".join(skill_only)
916
+ + "; these are skill arguments and must not be forwarded to scripts/last30days.py"
917
+ )
918
+ if other_unknown:
919
+ message += "; also unsupported: " + ", ".join(other_unknown)
920
+ parser.error(message)
921
+ parser.error("unsupported Python CLI argument(s): " + ", ".join(extra_argv))
922
+
923
+
924
+ def _config_policy_for_args(args: argparse.Namespace, topic: str, extra_argv: list[str]) -> env.ConfigLoadPolicy:
925
+ if args.no_browser_cookies:
926
+ browser_mode = "off"
927
+ elif args.diagnose or args.preflight:
928
+ browser_mode = "plan_only"
929
+ elif topic.lower() == "setup":
930
+ browser_mode = "read" if _setup_allows_browser_cookies(args, extra_argv) else "off"
931
+ else:
932
+ browser_mode = "read"
933
+ return env.ConfigLoadPolicy(
934
+ browser_cookies=browser_mode,
935
+ inspect_ignored_project_config=args.diagnose or args.preflight,
936
+ )
937
+
938
+
939
+ def main() -> int:
940
+ parser = build_parser()
941
+ # Use parse_known_args so setup sub-flags (--device-auth, --github,
942
+ # --openclaw) pass through without argparse hard-exiting.
943
+ args, extra_argv = parser.parse_known_args()
944
+ if args.debug:
945
+ os.environ["LAST30DAYS_DEBUG"] = "1"
946
+
947
+ topic = " ".join(args.topic).strip()
948
+ original_topic = topic
949
+ _validate_extra_argv(parser, topic, extra_argv)
950
+ config = env.get_config(policy=_config_policy_for_args(args, topic, extra_argv))
951
+ _propagate_config_to_environ(config)
952
+
953
+ # Env-var fallback for --save-dir, mirroring the LAST30DAYS_STORE pattern below.
954
+ # Uses `is None` / `is not None` checks (not truthy `or`) at every layer so that
955
+ # `--save-dir ""`, `LAST30DAYS_MEMORY_DIR=""` (shell-export-empty), and explicit
956
+ # absence each correctly suppress save. An `or` chain would collapse the empty
957
+ # shell-export into the same path as unset, silently falling through to .env.
958
+ if args.save_dir is None:
959
+ env_val = os.environ.get("LAST30DAYS_MEMORY_DIR")
960
+ args.save_dir = env_val if env_val is not None else config.get("LAST30DAYS_MEMORY_DIR")
961
+
962
+ # Surface SSH-routing config as an env var so library modules (e.g.
963
+ # youtube_yt) can read it without taking a config dependency. This
964
+ # routes yt-dlp through `ssh <host>` to bypass YouTube's bot-wall on
965
+ # datacenter IPs (see lib/youtube_yt.py for details).
966
+ if config.get("LAST30DAYS_YOUTUBE_SSH_HOST") and "LAST30DAYS_YOUTUBE_SSH_HOST" not in os.environ:
967
+ os.environ["LAST30DAYS_YOUTUBE_SSH_HOST"] = config["LAST30DAYS_YOUTUBE_SSH_HOST"]
968
+
969
+ if args.preflight:
970
+ requested_sources = resolve_requested_sources(args.search, config)
971
+ diag = pipeline.diagnose(config, requested_sources, safe=True)
972
+ if args.save_dir or args.preflight_report_on_save_dir:
973
+ preflight = permission_preflight.build(
974
+ config,
975
+ diag,
976
+ planned_save_dir=args.save_dir,
977
+ report_on_save_dir=args.preflight_report_on_save_dir,
978
+ )
979
+ else:
980
+ preflight = diag["permission_preflight"]
981
+ if args.emit == "json":
982
+ print(json.dumps(preflight, indent=2, sort_keys=True))
983
+ else:
984
+ print(permission_preflight.render_text(preflight), end="")
985
+ return 0
986
+
987
+ # Handle setup subcommand
988
+ if topic.lower() == "setup":
989
+ from lib import setup_wizard
990
+ if "--openclaw" in extra_argv:
991
+ results = setup_wizard.run_openclaw_setup(config)
992
+ print(json.dumps(results))
993
+ return 0
994
+ if "--github" in extra_argv or "--device-auth" in extra_argv:
995
+ if "--github" in extra_argv:
996
+ results = setup_wizard.run_github_auth()
997
+ else:
998
+ results = setup_wizard.run_full_device_auth()
999
+ # Persist the returned key so the paid sources activate on the next
1000
+ # run, and mask it in stdout so the secret never lands in the host
1001
+ # model's captured Bash output.
1002
+ api_key = results.get("api_key")
1003
+ if results.get("status") == "success" and api_key:
1004
+ results["persisted"] = setup_wizard.write_api_key(env.CONFIG_FILE, api_key)
1005
+ results["api_key"] = setup_wizard.mask_api_key(api_key)
1006
+ else:
1007
+ results["persisted"] = False
1008
+ print(json.dumps(results))
1009
+ return 0
1010
+ sys.stderr.write("Running auto-setup...\n")
1011
+ results = setup_wizard.run_auto_setup(
1012
+ config,
1013
+ allow_browser_cookies=_setup_allows_browser_cookies(args, extra_argv),
1014
+ )
1015
+ # Persist FROM_BROWSER only when every service's cookies came from the
1016
+ # SAME single browser — then we can fast-path future runs to it. If
1017
+ # different services matched different browsers, or none matched, leave
1018
+ # FROM_BROWSER unset so the safe default remains no browser-cookie
1019
+ # reads. We deliberately do NOT pin "auto" here (it would re-probe
1020
+ # Chrome and re-trigger the prompt) nor a single browser (it would
1021
+ # silently skip the service that used the other one).
1022
+ found_browsers = set(results.get("cookies_found", {}).values())
1023
+ from_browser = found_browsers.pop() if len(found_browsers) == 1 else None
1024
+ setup_wizard.write_setup_config(env.CONFIG_FILE, from_browser=from_browser)
1025
+ results["env_written"] = True
1026
+ sys.stderr.write(setup_wizard.get_setup_status_text(results) + "\n")
1027
+ return 0
1028
+
1029
+ requested_sources = resolve_requested_sources(args.search, config)
1030
+ diag = pipeline.diagnose(config, requested_sources, safe=args.diagnose)
1031
+
1032
+ if args.diagnose:
1033
+ print(json.dumps(diag, indent=2, sort_keys=True))
1034
+ return 0
1035
+
1036
+ if not topic:
1037
+ parser.print_usage(sys.stderr)
1038
+ return 2
1039
+ if args.publish_html and args.emit != "html":
1040
+ sys.stderr.write("[last30days] --publish-html requires --emit=html\n")
1041
+ return 2
1042
+
1043
+ synthesis_md = None
1044
+ if args.synthesis_file:
1045
+ if args.emit == "html":
1046
+ synthesis_md = read_synthesis_file(args.synthesis_file)
1047
+ else:
1048
+ sys.stderr.write("[last30days] Warning: --synthesis-file is only used with --emit=html; ignoring.\n")
1049
+
1050
+ if not os.environ.get("LAST30DAYS_SKIP_PREFLIGHT"):
1051
+ from lib import preflight
1052
+ refuse_msg = preflight.check_class_1_trap(topic)
1053
+ if refuse_msg:
1054
+ sys.stderr.write(refuse_msg)
1055
+ return 2
1056
+
1057
+ if args.emit == "html" and synthesis_md is not None:
1058
+ cached = _load_last_report_cache(
1059
+ topic,
1060
+ ttl_seconds=_report_cache_ttl_seconds(config),
1061
+ )
1062
+ if cached is not None:
1063
+ cached_report, cached_entity_reports, cache_path = cached
1064
+ sys.stderr.write(
1065
+ f"[last30days] Reusing cached report data from {cache_path}\n"
1066
+ )
1067
+ sys.stderr.flush()
1068
+ return _render_save_and_print(
1069
+ args, cached_report, cached_entity_reports, synthesis_md, config
1070
+ )
1071
+ sys.stderr.write(
1072
+ "[last30days] No matching cached report data for "
1073
+ "--emit=html --synthesis-file; running fresh research.\n"
1074
+ )
1075
+ sys.stderr.flush()
1076
+
1077
+ progress = ui.ProgressDisplay(topic, show_banner=True)
1078
+ progress.start_processing()
1079
+
1080
+ depth = "deep" if args.deep else "quick" if args.quick else "default"
1081
+ try:
1082
+ x_related = [h.strip() for h in args.x_related.split(",") if h.strip()] if args.x_related else None
1083
+ subreddits = [s.strip().removeprefix("r/") for s in args.subreddits.split(",") if s.strip()] if args.subreddits else None
1084
+ dedicated_subreddits = [s.strip().removeprefix("r/") for s in args.dedicated_subreddits.split(",") if s.strip()] if args.dedicated_subreddits else None
1085
+ tiktok_hashtags = [h.strip().lstrip("#") for h in args.tiktok_hashtags.split(",") if h.strip()] if args.tiktok_hashtags else None
1086
+ tiktok_creators = [c.strip().lstrip("@") for c in args.tiktok_creators.split(",") if c.strip()] if args.tiktok_creators else None
1087
+ ig_creators = [c.strip().lstrip("@") for c in args.ig_creators.split(",") if c.strip()] if args.ig_creators else None
1088
+ # Parse external plan if provided via --plan flag
1089
+ external_plan = None
1090
+ if args.plan:
1091
+ import json as _json
1092
+ plan_str = args.plan
1093
+ if os.path.isfile(plan_str):
1094
+ try:
1095
+ plan_str = open(plan_str, encoding="utf-8").read()
1096
+ except (OSError, UnicodeDecodeError) as exc:
1097
+ sys.stderr.write(f"[Planner] Cannot read --plan file: {exc}\n")
1098
+ raise SystemExit(2)
1099
+ try:
1100
+ external_plan = _json.loads(plan_str)
1101
+ except _json.JSONDecodeError as exc:
1102
+ sys.stderr.write(f"[Planner] Invalid --plan JSON: {exc}\n")
1103
+ # Fail fast instead of silently dropping to the internal planner
1104
+ # and burning a paid run the user did not ask for. Mirrors the
1105
+ # --plan file-read branch above and parse_competitors_plan.
1106
+ raise SystemExit(2)
1107
+
1108
+ # Auto-resolve: use web search to discover subreddits/handles before planning.
1109
+ # This is the engine-side equivalent of SKILL.md Steps 0.55/0.75 for platforms
1110
+ # without WebSearch (OpenClaw, Codex, raw CLI).
1111
+ repos_from_auto_resolve = False
1112
+ if args.auto_resolve and not external_plan:
1113
+ from lib import resolve
1114
+ resolution = resolve.auto_resolve(topic, config)
1115
+ if resolution.get("subreddits") and not subreddits:
1116
+ subreddits = resolution["subreddits"]
1117
+ sys.stderr.write(f"[AutoResolve] Subreddits: {', '.join(subreddits)}\n")
1118
+ if resolution.get("x_handle") and not args.x_handle:
1119
+ args.x_handle = resolution["x_handle"]
1120
+ sys.stderr.write(f"[AutoResolve] X handle: @{args.x_handle}\n")
1121
+ if resolution.get("github_user") and not args.github_user:
1122
+ args.github_user = resolution["github_user"]
1123
+ sys.stderr.write(f"[AutoResolve] GitHub user: @{args.github_user}\n")
1124
+ if resolution.get("github_repos") and not args.github_repo:
1125
+ args.github_repo = ",".join(resolution["github_repos"])
1126
+ # auto_resolve already canonicalized via canonicalize_github_repos(cap=5);
1127
+ # mark so we don't re-canonicalize below and clobber its relevance order.
1128
+ repos_from_auto_resolve = True
1129
+ sys.stderr.write(f"[AutoResolve] GitHub repos: {args.github_repo}\n")
1130
+ if resolution.get("context"):
1131
+ # Inject context into external_plan metadata for the planner to use
1132
+ if not external_plan:
1133
+ external_plan = None # planner will use its own, but with context
1134
+ # Store context for the planner prompt injection
1135
+ config["_auto_resolve_context"] = resolution["context"]
1136
+ sys.stderr.write(f"[AutoResolve] Context: {resolution['context'][:80]}...\n")
1137
+
1138
+ github_user = args.github_user.lstrip("@").lower() if args.github_user else None
1139
+ github_repos = [r.strip() for r in args.github_repo.split(",") if r.strip() and "/" in r.strip()] if args.github_repo else None
1140
+
1141
+ # Only canonicalize when repos came from a user-supplied --github-repo flag.
1142
+ # When repos_from_auto_resolve is True, auto_resolve already ran
1143
+ # canonicalize_github_repos(cap=5) and ranked by relevance; re-running here
1144
+ # with cap=None can re-sort by topic-slug match and lose that ordering.
1145
+ if github_repos and not repos_from_auto_resolve:
1146
+ from lib import resolve as resolve_lib
1147
+ original_github_repos = github_repos[:]
1148
+ github_repos = resolve_lib.canonicalize_github_repos(topic, github_repos, cap=None)
1149
+ if github_repos != original_github_repos:
1150
+ sys.stderr.write(
1151
+ "[GitHub] Canonicalized repos: "
1152
+ f"{','.join(original_github_repos)} -> {','.join(github_repos)}\n"
1153
+ )
1154
+
1155
+ # --deep-research: auto-enable perplexity source and set deep flag
1156
+ if args.deep_research:
1157
+ if not (config.get("PERPLEXITY_API_KEY") or config.get("OPENROUTER_API_KEY")):
1158
+ print("Error: --deep-research requires PERPLEXITY_API_KEY or OPENROUTER_API_KEY", file=sys.stderr)
1159
+ sys.exit(1)
1160
+ config["_deep_research"] = True
1161
+ # Auto-enable perplexity in INCLUDE_SOURCES
1162
+ include = config.get("INCLUDE_SOURCES") or ""
1163
+ if "perplexity" not in include.lower():
1164
+ config["INCLUDE_SOURCES"] = f"{include},perplexity" if include else "perplexity"
1165
+
1166
+ comp_enabled, comp_count, comp_explicit = resolve_competitors_args(args)
1167
+ comp_plan = parse_competitors_plan(args.competitors_plan)
1168
+
1169
+ # Polymarket disambiguation: if user passed --polymarket-keywords,
1170
+ # store on config so the polymarket adapter can filter matches.
1171
+ if args.polymarket_keywords:
1172
+ keywords = [
1173
+ k.strip().lower()
1174
+ for k in args.polymarket_keywords.split(",")
1175
+ if k.strip()
1176
+ ]
1177
+ if keywords:
1178
+ config["_polymarket_keywords"] = keywords
1179
+
1180
+ # vs-mode: if the topic string contains " vs " / " versus " and the
1181
+ # planner can split it into >=2 entities, route through the same
1182
+ # N-pass fanout path as --competitors. The first entity becomes the
1183
+ # main topic; remaining entities become the competitor list. User's
1184
+ # outer --x-handle / --subreddits apply to the first entity unless
1185
+ # --competitors-plan covers it.
1186
+ from lib import planner as _planner
1187
+ vs_entities = _planner._comparison_entities(topic)
1188
+ if len(vs_entities) >= 2 and not comp_enabled:
1189
+ topic = vs_entities[0]
1190
+ comp_enabled = True
1191
+ comp_count = len(vs_entities) - 1
1192
+ comp_explicit = vs_entities[1:]
1193
+ sys.stderr.write(
1194
+ f"[Competitors] vs-mode: routing to N-pass fanout: "
1195
+ f"{' vs '.join(vs_entities)}\n"
1196
+ )
1197
+
1198
+ # Dedicated subs ride the config dict (already threaded to every source
1199
+ # fetch) so the keyless Reddit path can pull them floor-exempt without
1200
+ # widening pipeline.run / _retrieve_stream signatures.
1201
+ if dedicated_subreddits:
1202
+ config["_dedicated_subreddits"] = dedicated_subreddits
1203
+
1204
+ def _main_runner() -> schema.Report:
1205
+ r = pipeline.run(
1206
+ topic=topic,
1207
+ config=config,
1208
+ depth=depth,
1209
+ requested_sources=requested_sources,
1210
+ mock=args.mock,
1211
+ x_handle=args.x_handle,
1212
+ x_related=x_related,
1213
+ web_backend=args.web_backend,
1214
+ external_plan=external_plan,
1215
+ subreddits=subreddits,
1216
+ tiktok_hashtags=tiktok_hashtags,
1217
+ tiktok_creators=tiktok_creators,
1218
+ ig_creators=ig_creators,
1219
+ lookback_days=args.lookback_days,
1220
+ as_of_date=args.as_of_date,
1221
+ github_user=github_user,
1222
+ github_repos=github_repos,
1223
+ internal_subrun=comp_enabled,
1224
+ hiring_signals_mode=args.hiring_signals,
1225
+ )
1226
+ r.artifacts["resolved"] = {
1227
+ "entity": topic,
1228
+ "x_handle": (args.x_handle or "").lstrip("@"),
1229
+ "subreddits": list(subreddits or []),
1230
+ "github_user": (github_user or ""),
1231
+ "github_repos": list(github_repos or []),
1232
+ "context": config.get("_auto_resolve_context", "") or "",
1233
+ }
1234
+ return r
1235
+
1236
+ if comp_enabled:
1237
+ from lib import competitors as competitors_mod
1238
+ from lib import fanout, resolve as resolve_mod
1239
+
1240
+ if comp_explicit:
1241
+ discovered = comp_explicit
1242
+ else:
1243
+ if not resolve_mod._has_backend(config) and not args.mock:
1244
+ sys.stderr.write(
1245
+ "[Competitors] Cannot auto-discover peers without help.\n"
1246
+ "\n"
1247
+ "RECOMMENDED PATH (hosting reasoning models — Claude Code, Codex, "
1248
+ "Hermes, Gemini, any agent with a WebSearch tool): YOU have "
1249
+ "WebSearch. Use it to run full Step 0.55 per entity, then invoke "
1250
+ "the engine with a vs-topic plus --competitors-plan:\n"
1251
+ " 1. WebSearch for '{topic} competitors' or '{topic} alternatives'.\n"
1252
+ " 2. For each peer, WebSearch for handles/subs/github (Step 0.55).\n"
1253
+ " 3. Re-invoke: /last30days '{topic} vs {peer1} vs {peer2}' "
1254
+ "--competitors-plan '{\"Peer1\":{\"x_handle\":\"h1\",\"subreddits\":"
1255
+ "[\"s1\"],...},\"Peer2\":{...}}'.\n"
1256
+ "See SKILL.md 'Competitor mode' for the full protocol.\n"
1257
+ "\n"
1258
+ "HEADLESS / CRON PATH (no hosting model available): set "
1259
+ "BRAVE_API_KEY / EXA_API_KEY / SERPER_API_KEY / PARALLEL_API_KEY / "
1260
+ "PERPLEXITY_API_KEY / OPENROUTER_API_KEY and re-run.\n"
1261
+ "\n"
1262
+ "MINIMUM ESCAPE HATCH: pass --competitors-list 'A,B,C' to skip "
1263
+ "discovery. Without --competitors-plan, peer sub-runs fall back to "
1264
+ "planner defaults and produce visibly thinner data than the main.\n"
1265
+ )
1266
+ return 2
1267
+ discovered = competitors_mod.discover_competitors(
1268
+ topic, comp_count, config, lookback_days=args.lookback_days,
1269
+ )
1270
+ if not discovered:
1271
+ sys.stderr.write(
1272
+ f"[Competitors] No peers discovered for {topic!r}; aborting "
1273
+ "comparison run. Pass --competitors-list to override.\n"
1274
+ )
1275
+ return 2
1276
+
1277
+ sys.stderr.write(
1278
+ f"[Competitors] Comparing: {topic} vs " + " vs ".join(discovered) + "\n"
1279
+ )
1280
+
1281
+ def _competitor_runner(entity: str) -> schema.Report:
1282
+ # Deep-copy config so per-entity auto_resolve context does not
1283
+ # leak across sub-runs. Each sub-run writes its own
1284
+ # `_auto_resolve_context` into its local config copy.
1285
+ entity_config = dict(config)
1286
+ plan_entry = comp_plan.get(entity.strip().lower(), {})
1287
+ resolved = {
1288
+ "entity": entity,
1289
+ "x_handle": "",
1290
+ "subreddits": [],
1291
+ "github_user": "",
1292
+ "github_repos": [],
1293
+ "context": "",
1294
+ }
1295
+ # Skip engine-internal auto_resolve when the hosting model
1296
+ # pre-resolved via --competitors-plan (saves a redundant
1297
+ # round-trip and makes per-entity Step 0.55 purely
1298
+ # hosting-model-driven).
1299
+ plan_covers_fully = bool(plan_entry.get("x_handle")) and bool(
1300
+ plan_entry.get("subreddits")
1301
+ )
1302
+ if (
1303
+ not args.mock
1304
+ and not plan_covers_fully
1305
+ and resolve_mod._has_backend(entity_config)
1306
+ ):
1307
+ try:
1308
+ r = resolve_mod.auto_resolve(entity, entity_config)
1309
+ except Exception as exc:
1310
+ sys.stderr.write(
1311
+ f"[Competitors] auto_resolve failed for {entity!r}: "
1312
+ f"{type(exc).__name__}: {exc}\n"
1313
+ )
1314
+ r = {}
1315
+ resolved["x_handle"] = r.get("x_handle", "") or ""
1316
+ resolved["subreddits"] = list(r.get("subreddits") or [])
1317
+ resolved["github_user"] = r.get("github_user", "") or ""
1318
+ resolved["github_repos"] = list(r.get("github_repos") or [])
1319
+ resolved["context"] = r.get("context", "") or ""
1320
+ kwargs = subrun_kwargs_for(entity, plan_entry, resolved=resolved)
1321
+ # Record effective per-entity targeting for the Resolved block.
1322
+ resolved_effective = {
1323
+ "entity": entity,
1324
+ "x_handle": kwargs["x_handle"] or "",
1325
+ "subreddits": kwargs["subreddits"] or [],
1326
+ "github_user": kwargs["github_user"] or "",
1327
+ "github_repos": kwargs["github_repos"] or [],
1328
+ "context": kwargs["_context"],
1329
+ }
1330
+ if kwargs["_context"]:
1331
+ entity_config["_auto_resolve_context"] = kwargs["_context"]
1332
+ sys.stderr.write(
1333
+ f"[Competitors] {entity}: "
1334
+ f"x=@{resolved_effective['x_handle'] or '-'} "
1335
+ f"subs={len(resolved_effective['subreddits'])} "
1336
+ f"gh={resolved_effective['github_user'] or '-'} "
1337
+ f"({'plan' if plan_entry else 'auto'})\n"
1338
+ )
1339
+ report = pipeline.run(
1340
+ topic=entity,
1341
+ config=entity_config,
1342
+ depth=depth,
1343
+ requested_sources=requested_sources,
1344
+ mock=args.mock,
1345
+ x_handle=kwargs["x_handle"],
1346
+ x_related=kwargs["x_related"],
1347
+ subreddits=kwargs["subreddits"],
1348
+ github_user=kwargs["github_user"],
1349
+ github_repos=kwargs["github_repos"],
1350
+ web_backend=args.web_backend,
1351
+ lookback_days=args.lookback_days,
1352
+ as_of_date=args.as_of_date,
1353
+ hiring_signals_mode=args.hiring_signals,
1354
+ internal_subrun=True,
1355
+ )
1356
+ report.artifacts["resolved"] = resolved_effective
1357
+ return report
1358
+
1359
+ entity_reports = fanout.run_competitor_fanout(
1360
+ main_topic=topic,
1361
+ main_runner=_main_runner,
1362
+ competitors=discovered,
1363
+ competitor_runner=_competitor_runner,
1364
+ )
1365
+ if len(entity_reports) < 2:
1366
+ progress.end_processing()
1367
+ sys.stderr.write(
1368
+ f"[Competitors] Fewer than 2 sub-runs survived ({len(entity_reports)}); "
1369
+ "cannot render a comparison. Re-run without --competitors or check the "
1370
+ "warnings above.\n"
1371
+ )
1372
+ return 1
1373
+ report = entity_reports[0][1]
1374
+ else:
1375
+ entity_reports = None
1376
+ report = _main_runner()
1377
+ except Exception as exc:
1378
+ progress.end_processing()
1379
+ progress.show_error(str(exc))
1380
+ raise
1381
+ _show_runtime_ui(
1382
+ report, progress, diag,
1383
+ suppress_web_promo=bool(external_plan or comp_plan),
1384
+ )
1385
+ _write_last_run(original_topic, report, entity_reports=entity_reports)
1386
+ # LAST30DAYS_STORE env var = persistence default-on. Read both os.environ
1387
+ # (for shell-exported users) and config (for users who set it in
1388
+ # ~/.config/last30days/.env, which env.py loads but does not propagate
1389
+ # to os.environ). Mirrors the LAST30DAYS_DEBUG / LAST30DAYS_SKIP_PREFLIGHT
1390
+ # convention; env-var or config wins, with `--store` flag still working.
1391
+ _store_env = (
1392
+ os.environ.get("LAST30DAYS_STORE")
1393
+ or config.get("LAST30DAYS_STORE")
1394
+ or ""
1395
+ ).lower()
1396
+ if args.store or _store_env in ("1", "true", "yes"):
1397
+ counts = persist_report(report)
1398
+ sys.stderr.write(
1399
+ f"[last30days] Stored {counts['new']} new, {counts['updated']} updated findings\n"
1400
+ )
1401
+ sys.stderr.flush()
1402
+
1403
+ # Show quality nudge if applicable. Explicit hiring-signal runs are
1404
+ # intentionally jobs-focused, so generic source setup advice is noise.
1405
+ if not args.hiring_signals:
1406
+ try:
1407
+ from lib import quality_nudge
1408
+ from lib import youtube_yt as _youtube_yt
1409
+ # Populate transcript-fetch ratio so quality_nudge can detect the
1410
+ # degraded-YouTube failure mode (videos returned but transcripts
1411
+ # silently failed - typically a stale yt-dlp binary).
1412
+ youtube_items = report.items_by_source.get("youtube") or []
1413
+ _yt_fetch_stats = _youtube_yt.get_transcript_fetch_stats()
1414
+ instagram_items = report.items_by_source.get("instagram") or []
1415
+ research_results = {
1416
+ "youtube_videos_count": len(youtube_items),
1417
+ "youtube_transcripts_count": sum(
1418
+ 1 for it in youtube_items
1419
+ if (it.metadata.get("transcript_highlights") or it.metadata.get("transcript_snippet"))
1420
+ ),
1421
+ "youtube_error": report.errors_by_source.get("youtube"),
1422
+ "x_error": report.errors_by_source.get("x"),
1423
+ # Captions-disabled videos can never produce a transcript regardless
1424
+ # of yt-dlp version; subtract them from the degraded-ratio
1425
+ # denominator so a single uploader-disabled video does not trip the
1426
+ # "stale yt-dlp" nudge.
1427
+ "youtube_captions_disabled_count": sum(
1428
+ 1 for it in youtube_items if it.metadata.get("captions_disabled")
1429
+ ),
1430
+ # Actual yt-dlp fetch outcomes for this run. The counts above are
1431
+ # computed from post-pruning items, so they can't tell "fetches
1432
+ # failed (stale binary)" from "fetches succeeded but the videos
1433
+ # were pruned downstream"; the latter was producing false
1434
+ # stale-yt-dlp nudges (#531).
1435
+ "youtube_transcript_fetch_attempts": _yt_fetch_stats["attempts"],
1436
+ "youtube_transcript_fetch_failures": _yt_fetch_stats["failures"],
1437
+ # Track Instagram returned-zero-items so quality_nudge can detect
1438
+ # the silent-failure case (SC configured but the v2 reels endpoint
1439
+ # 500'd through both the original query and the hashtag retry).
1440
+ "instagram_items_count": len(instagram_items),
1441
+ }
1442
+ quality = quality_nudge.compute_quality_score(config, research_results)
1443
+ if quality.get("nudge_text"):
1444
+ sys.stderr.write(f"\n{quality['nudge_text']}\n")
1445
+ sys.stderr.flush()
1446
+ except Exception:
1447
+ pass
1448
+
1449
+ # Signal to render_compact whether pre-research flags were supplied.
1450
+ # Used to emit a Pre-Research Status warning when the model skipped
1451
+ # Step 0.5 / 0.55 and invoked the engine bare on an eligible topic.
1452
+ pre_research_flags_present = bool(
1453
+ args.x_handle
1454
+ or args.github_user
1455
+ or args.subreddits
1456
+ or args.plan
1457
+ or args.auto_resolve
1458
+ or args.tiktok_creators
1459
+ or args.ig_creators
1460
+ )
1461
+ report.artifacts["pre_research_flags_present"] = pre_research_flags_present
1462
+
1463
+ return _render_save_and_print(args, report, entity_reports, synthesis_md, config)
1464
+
1465
+
1466
+ if __name__ == "__main__":
1467
+ raise SystemExit(main())