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,1020 @@
1
+ #!/usr/bin/env python3
2
+ """SQLite research accumulator for last30days.
3
+
4
+ Stores topics, research runs, and findings with:
5
+ - WAL mode for safe concurrent access (cron + user)
6
+ - FTS5 full-text search with porter+unicode61 tokenizer
7
+ - URL-based dedup with engagement metric updates on re-sighting
8
+ - Lightweight schema migrations without external dependencies
9
+
10
+ Database location: ~/.local/share/last30days/research.db
11
+ """
12
+
13
+ import argparse
14
+ import json
15
+ import sqlite3
16
+ import sys
17
+ from datetime import datetime, timedelta, timezone
18
+ from pathlib import Path
19
+ from typing import Any, Dict, List, Optional
20
+
21
+ SCRIPT_DIR = Path(__file__).parent.resolve()
22
+ sys.path.insert(0, str(SCRIPT_DIR))
23
+
24
+ from lib import schema
25
+
26
+ DB_DIR = Path.home() / ".local" / "share" / "last30days"
27
+ DB_PATH = DB_DIR / "research.db"
28
+
29
+ # Allow override for testing
30
+ _db_override = None
31
+
32
+
33
+ def _get_db_path() -> Path:
34
+ return _db_override or DB_PATH
35
+
36
+
37
+ SCHEMA_V1 = """
38
+ PRAGMA journal_mode=WAL;
39
+ PRAGMA synchronous=NORMAL;
40
+ PRAGMA cache_size=-64000;
41
+
42
+ CREATE TABLE IF NOT EXISTS schema_version (
43
+ version INTEGER PRIMARY KEY,
44
+ applied_at TEXT DEFAULT (datetime('now'))
45
+ );
46
+
47
+ CREATE TABLE IF NOT EXISTS topics (
48
+ id INTEGER PRIMARY KEY,
49
+ name TEXT UNIQUE NOT NULL,
50
+ search_queries TEXT,
51
+ schedule TEXT,
52
+ enabled INTEGER DEFAULT 1,
53
+ created_at TEXT DEFAULT (datetime('now')),
54
+ updated_at TEXT DEFAULT (datetime('now'))
55
+ );
56
+
57
+ CREATE TABLE IF NOT EXISTS research_runs (
58
+ id INTEGER PRIMARY KEY,
59
+ topic_id INTEGER REFERENCES topics(id),
60
+ run_date TEXT NOT NULL,
61
+ source_mode TEXT,
62
+ prompt_tokens INTEGER,
63
+ completion_tokens INTEGER,
64
+ token_cost REAL,
65
+ duration_seconds REAL,
66
+ status TEXT DEFAULT 'completed',
67
+ error_message TEXT,
68
+ findings_new INTEGER DEFAULT 0,
69
+ findings_updated INTEGER DEFAULT 0,
70
+ created_at TEXT DEFAULT (datetime('now'))
71
+ );
72
+
73
+ CREATE TABLE IF NOT EXISTS findings (
74
+ id INTEGER PRIMARY KEY,
75
+ run_id INTEGER REFERENCES research_runs(id),
76
+ topic_id INTEGER REFERENCES topics(id),
77
+ source TEXT NOT NULL,
78
+ source_url TEXT UNIQUE,
79
+ source_title TEXT,
80
+ author TEXT,
81
+ content TEXT,
82
+ summary TEXT,
83
+ engagement_score REAL,
84
+ relevance_score REAL,
85
+ first_seen TEXT DEFAULT (datetime('now')),
86
+ last_seen TEXT DEFAULT (datetime('now')),
87
+ sighting_count INTEGER DEFAULT 1,
88
+ dismissed INTEGER DEFAULT 0
89
+ );
90
+
91
+ CREATE INDEX IF NOT EXISTS idx_findings_topic ON findings(topic_id, first_seen);
92
+ CREATE INDEX IF NOT EXISTS idx_findings_source ON findings(source, topic_id);
93
+ CREATE INDEX IF NOT EXISTS idx_findings_url ON findings(source_url);
94
+
95
+ CREATE VIRTUAL TABLE IF NOT EXISTS findings_fts USING fts5(
96
+ content, summary, source_title, author,
97
+ tokenize='porter unicode61',
98
+ content='findings',
99
+ content_rowid='id'
100
+ );
101
+
102
+ CREATE TRIGGER IF NOT EXISTS findings_ai AFTER INSERT ON findings BEGIN
103
+ INSERT INTO findings_fts(rowid, content, summary, source_title, author)
104
+ VALUES (new.id, new.content, new.summary, new.source_title, new.author);
105
+ END;
106
+
107
+ CREATE TRIGGER IF NOT EXISTS findings_ad AFTER DELETE ON findings BEGIN
108
+ INSERT INTO findings_fts(findings_fts, rowid, content, summary, source_title, author)
109
+ VALUES ('delete', old.id, old.content, old.summary, old.source_title, old.author);
110
+ END;
111
+
112
+ CREATE TRIGGER IF NOT EXISTS findings_au AFTER UPDATE ON findings BEGIN
113
+ INSERT INTO findings_fts(findings_fts, rowid, content, summary, source_title, author)
114
+ VALUES ('delete', old.id, old.content, old.summary, old.source_title, old.author);
115
+ INSERT INTO findings_fts(rowid, content, summary, source_title, author)
116
+ VALUES (new.id, new.content, new.summary, new.source_title, new.author);
117
+ END;
118
+
119
+ CREATE TABLE IF NOT EXISTS settings (
120
+ key TEXT PRIMARY KEY,
121
+ value TEXT,
122
+ updated_at TEXT DEFAULT (datetime('now'))
123
+ );
124
+ """
125
+
126
+ SCHEMA_V1_DEFAULTS = """
127
+ INSERT OR IGNORE INTO schema_version (version) VALUES (1);
128
+ INSERT OR IGNORE INTO settings (key, value) VALUES ('daily_budget', '5.00');
129
+ INSERT OR IGNORE INTO settings (key, value) VALUES ('delivery_channel', '');
130
+ INSERT OR IGNORE INTO settings (key, value) VALUES ('delivery_mode', 'announce');
131
+ INSERT OR IGNORE INTO settings (key, value) VALUES ('briefing_format', 'concise');
132
+ INSERT OR IGNORE INTO settings (key, value) VALUES ('default_schedule', '0 8 * * *');
133
+ """
134
+
135
+ _UPDATABLE_RUN_COLUMNS = frozenset({
136
+ "source_mode",
137
+ "prompt_tokens",
138
+ "completion_tokens",
139
+ "token_cost",
140
+ "duration_seconds",
141
+ "status",
142
+ "error_message",
143
+ "findings_new",
144
+ "findings_updated",
145
+ })
146
+
147
+ _UPDATABLE_FINDING_COLUMNS = frozenset({
148
+ "source",
149
+ "source_url",
150
+ "source_title",
151
+ "author",
152
+ "content",
153
+ "summary",
154
+ "engagement_score",
155
+ "relevance_score",
156
+ "last_seen",
157
+ "sighting_count",
158
+ "dismissed",
159
+ })
160
+
161
+ # Future migrations keyed by version number
162
+ MIGRATIONS: Dict[int, str] = {
163
+ 2: """
164
+ CREATE TABLE IF NOT EXISTS finding_sightings (
165
+ id INTEGER PRIMARY KEY,
166
+ finding_id INTEGER NOT NULL REFERENCES findings(id) ON DELETE CASCADE,
167
+ run_id INTEGER REFERENCES research_runs(id) ON DELETE CASCADE,
168
+ topic_id INTEGER REFERENCES topics(id) ON DELETE CASCADE,
169
+ source TEXT NOT NULL,
170
+ source_url TEXT NOT NULL,
171
+ source_title TEXT,
172
+ engagement_score REAL,
173
+ relevance_score REAL,
174
+ seen_at TEXT DEFAULT (datetime('now')),
175
+ UNIQUE(run_id, finding_id)
176
+ );
177
+
178
+ CREATE INDEX IF NOT EXISTS idx_finding_sightings_run
179
+ ON finding_sightings(run_id, topic_id);
180
+ CREATE INDEX IF NOT EXISTS idx_finding_sightings_topic_seen
181
+ ON finding_sightings(topic_id, seen_at);
182
+ CREATE INDEX IF NOT EXISTS idx_finding_sightings_url
183
+ ON finding_sightings(source_url);
184
+ """,
185
+ }
186
+
187
+
188
+ def _connect(db_path: Optional[Path] = None) -> sqlite3.Connection:
189
+ """Open a connection with WAL mode and row factory."""
190
+ path = db_path or _get_db_path()
191
+ conn = sqlite3.connect(str(path))
192
+ conn.row_factory = sqlite3.Row
193
+ conn.execute("PRAGMA journal_mode=WAL")
194
+ conn.execute("PRAGMA synchronous=NORMAL")
195
+ conn.execute("PRAGMA foreign_keys=ON")
196
+ # WAL lets readers coexist with one writer, but two writers (cron + user)
197
+ # still contend for the write lock. Default busy_timeout is 0, so the loser
198
+ # raises "database is locked" instantly; wait instead.
199
+ conn.execute("PRAGMA busy_timeout=5000")
200
+ return conn
201
+
202
+
203
+ def init_db(db_path: Optional[Path] = None) -> Path:
204
+ """Create database and tables if they don't exist. Returns the DB path."""
205
+ path = db_path or _get_db_path()
206
+ path.parent.mkdir(parents=True, exist_ok=True)
207
+
208
+ conn = _connect(path)
209
+ try:
210
+ conn.executescript(SCHEMA_V1)
211
+ conn.executescript(SCHEMA_V1_DEFAULTS)
212
+ _run_migrations(conn)
213
+ conn.commit()
214
+ finally:
215
+ conn.close()
216
+
217
+ return path
218
+
219
+
220
+ def _run_migrations(conn: sqlite3.Connection):
221
+ """Apply pending schema migrations."""
222
+ current = conn.execute(
223
+ "SELECT MAX(version) FROM schema_version"
224
+ ).fetchone()[0] or 0
225
+
226
+ for version in sorted(MIGRATIONS.keys()):
227
+ if version > current:
228
+ conn.executescript(MIGRATIONS[version])
229
+ conn.execute(
230
+ "INSERT INTO schema_version (version) VALUES (?)", (version,)
231
+ )
232
+
233
+
234
+ # --- Topics ---
235
+
236
+
237
+ def add_topic(
238
+ name: str,
239
+ search_queries: Optional[List[str]] = None,
240
+ schedule: str = "0 8 * * *",
241
+ ) -> Dict[str, Any]:
242
+ """Add a topic to the watchlist. Returns the topic dict."""
243
+ init_db()
244
+ conn = _connect()
245
+ try:
246
+ queries_json = json.dumps(search_queries) if search_queries else None
247
+ conn.execute(
248
+ """INSERT INTO topics (name, search_queries, schedule)
249
+ VALUES (?, ?, ?)
250
+ ON CONFLICT(name) DO UPDATE SET
251
+ search_queries = excluded.search_queries,
252
+ schedule = excluded.schedule,
253
+ updated_at = datetime('now')""",
254
+ (name, queries_json, schedule),
255
+ )
256
+ conn.commit()
257
+ row = conn.execute(
258
+ "SELECT * FROM topics WHERE name = ?", (name,)
259
+ ).fetchone()
260
+ return dict(row)
261
+ finally:
262
+ conn.close()
263
+
264
+
265
+ def remove_topic(name: str) -> bool:
266
+ """Remove a topic from the watchlist. Returns True if found."""
267
+ init_db()
268
+ conn = _connect()
269
+ try:
270
+ row = conn.execute(
271
+ "SELECT id FROM topics WHERE name = ?", (name,)
272
+ ).fetchone()
273
+ if not row:
274
+ return False
275
+ topic_id = row["id"]
276
+ # Delete findings and runs for this topic
277
+ conn.execute("DELETE FROM findings WHERE topic_id = ?", (topic_id,))
278
+ conn.execute("DELETE FROM research_runs WHERE topic_id = ?", (topic_id,))
279
+ conn.execute("DELETE FROM topics WHERE id = ?", (topic_id,))
280
+ conn.commit()
281
+ return True
282
+ finally:
283
+ conn.close()
284
+
285
+
286
+ def list_topics() -> List[Dict[str, Any]]:
287
+ """List all topics with stats."""
288
+ init_db()
289
+ conn = _connect()
290
+ try:
291
+ rows = conn.execute(
292
+ """SELECT t.*,
293
+ (SELECT COUNT(*) FROM findings WHERE topic_id = t.id) as finding_count,
294
+ (SELECT MAX(run_date) FROM research_runs WHERE topic_id = t.id) as last_run,
295
+ (SELECT status FROM research_runs WHERE topic_id = t.id
296
+ ORDER BY created_at DESC LIMIT 1) as last_status
297
+ FROM topics t
298
+ ORDER BY t.name"""
299
+ ).fetchall()
300
+ return [dict(r) for r in rows]
301
+ finally:
302
+ conn.close()
303
+
304
+
305
+ def get_topic(name: str) -> Optional[Dict[str, Any]]:
306
+ """Get a topic by name."""
307
+ init_db()
308
+ conn = _connect()
309
+ try:
310
+ row = conn.execute(
311
+ "SELECT * FROM topics WHERE name = ?", (name,)
312
+ ).fetchone()
313
+ return dict(row) if row else None
314
+ finally:
315
+ conn.close()
316
+
317
+
318
+ # --- Research Runs ---
319
+
320
+
321
+ def record_run(
322
+ topic_id: int,
323
+ source_mode: str = "both",
324
+ status: str = "completed",
325
+ error_message: Optional[str] = None,
326
+ duration_seconds: float = 0,
327
+ prompt_tokens: int = 0,
328
+ completion_tokens: int = 0,
329
+ token_cost: float = 0,
330
+ ) -> int:
331
+ """Record a research run. Returns the run ID."""
332
+ conn = _connect()
333
+ try:
334
+ cursor = conn.execute(
335
+ """INSERT INTO research_runs
336
+ (topic_id, run_date, source_mode, status, error_message,
337
+ duration_seconds, prompt_tokens, completion_tokens, token_cost)
338
+ VALUES (?, datetime('now'), ?, ?, ?, ?, ?, ?, ?)""",
339
+ (
340
+ topic_id, source_mode, status, error_message,
341
+ duration_seconds, prompt_tokens, completion_tokens, token_cost,
342
+ ),
343
+ )
344
+ conn.commit()
345
+ return cursor.lastrowid
346
+ finally:
347
+ conn.close()
348
+
349
+
350
+ def update_run(run_id: int, **kwargs):
351
+ """Update a research run's fields."""
352
+ conn = _connect()
353
+ try:
354
+ invalid_columns = sorted(set(kwargs) - _UPDATABLE_RUN_COLUMNS)
355
+ if invalid_columns:
356
+ raise ValueError(
357
+ f"Invalid run update fields: {', '.join(invalid_columns)}"
358
+ )
359
+ sets = ", ".join(f"{k} = ?" for k in kwargs)
360
+ values = list(kwargs.values()) + [run_id]
361
+ conn.execute(f"UPDATE research_runs SET {sets} WHERE id = ?", values)
362
+ conn.commit()
363
+ finally:
364
+ conn.close()
365
+
366
+
367
+ def get_latest_completed_runs(topic_id: int, limit: int = 2) -> List[Dict[str, Any]]:
368
+ """Return newest completed runs for a topic."""
369
+ conn = _connect()
370
+ try:
371
+ rows = conn.execute(
372
+ """SELECT * FROM research_runs
373
+ WHERE topic_id = ? AND status = 'completed'
374
+ ORDER BY datetime(run_date) DESC, id DESC
375
+ LIMIT ?""",
376
+ (topic_id, limit),
377
+ ).fetchall()
378
+ return [dict(r) for r in rows]
379
+ finally:
380
+ conn.close()
381
+
382
+
383
+ # --- Findings ---
384
+
385
+
386
+ def store_findings(
387
+ run_id: int,
388
+ topic_id: int,
389
+ findings: List[Dict[str, Any]],
390
+ ) -> Dict[str, int]:
391
+ """Store findings with URL-based dedup. Returns counts of new/updated."""
392
+ # Collect findings that have a URL, preserving order.
393
+ with_urls: List[tuple[str, Dict[str, Any]]] = []
394
+ for f in findings:
395
+ url = f.get("source_url") or f.get("url")
396
+ if url:
397
+ with_urls.append((url, f))
398
+
399
+ if not with_urls:
400
+ conn = _connect()
401
+ try:
402
+ conn.execute(
403
+ "UPDATE research_runs SET findings_new = 0, findings_updated = 0 WHERE id = ?",
404
+ (run_id,),
405
+ )
406
+ conn.commit()
407
+ finally:
408
+ conn.close()
409
+ return {"new": 0, "updated": 0}
410
+
411
+ conn = _connect()
412
+ try:
413
+ # Single batch SELECT to find existing findings by URL.
414
+ urls = [url for url, _ in with_urls]
415
+ placeholders = ",".join("?" for _ in urls)
416
+ rows = conn.execute(
417
+ f"SELECT id, source_url, engagement_score FROM findings WHERE source_url IN ({placeholders})",
418
+ urls,
419
+ ).fetchall()
420
+ existing_by_url = {row["source_url"]: row for row in rows}
421
+
422
+ update_rows: List[tuple] = []
423
+ insert_rows: List[tuple] = []
424
+
425
+ for url, f in with_urls:
426
+ existing = existing_by_url.get(url)
427
+ new_engagement = f.get("engagement_score", 0)
428
+ if existing:
429
+ update_rows.append((
430
+ max(new_engagement, existing["engagement_score"] or 0),
431
+ run_id,
432
+ existing["id"],
433
+ ))
434
+ else:
435
+ insert_rows.append((
436
+ run_id,
437
+ topic_id,
438
+ f.get("source", "unknown"),
439
+ url,
440
+ f.get("source_title") or f.get("title", ""),
441
+ f.get("author", ""),
442
+ f.get("content") or f.get("text", ""),
443
+ f.get("summary", ""),
444
+ new_engagement,
445
+ f.get("relevance_score", 0),
446
+ ))
447
+
448
+ if update_rows:
449
+ conn.executemany(
450
+ """UPDATE findings SET
451
+ last_seen = datetime('now'),
452
+ sighting_count = sighting_count + 1,
453
+ engagement_score = ?,
454
+ run_id = ?
455
+ WHERE id = ?""",
456
+ update_rows,
457
+ )
458
+ if insert_rows:
459
+ # source_url is UNIQUE. The SELECT above is not atomic with this
460
+ # write, so a concurrent run (cron + user) can insert the same URL
461
+ # between our read and write. Upsert on conflict instead of letting
462
+ # IntegrityError abort the whole batch and lose every finding.
463
+ conn.executemany(
464
+ """INSERT INTO findings
465
+ (run_id, topic_id, source, source_url, source_title,
466
+ author, content, summary, engagement_score, relevance_score)
467
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
468
+ ON CONFLICT(source_url) DO UPDATE SET
469
+ last_seen = datetime('now'),
470
+ sighting_count = sighting_count + 1,
471
+ engagement_score = max(
472
+ engagement_score, excluded.engagement_score),
473
+ run_id = excluded.run_id""",
474
+ insert_rows,
475
+ )
476
+
477
+ new_count = len(insert_rows)
478
+ updated_count = len(update_rows)
479
+ if insert_rows:
480
+ # A row whose URL was inserted by a concurrent run between our SELECT
481
+ # and the upsert resolves via ON CONFLICT (an update, not a new row),
482
+ # bumping its sighting_count above 1. Re-derive the split so
483
+ # research_runs.findings_new isn't inflated by conflict-resolved rows
484
+ # (source_url is field index 3 in each insert tuple).
485
+ inserted_urls = [row[3] for row in insert_rows]
486
+ placeholders = ",".join("?" for _ in inserted_urls)
487
+ conflicted = conn.execute(
488
+ f"SELECT COUNT(*) FROM findings "
489
+ f"WHERE source_url IN ({placeholders}) AND sighting_count > 1",
490
+ inserted_urls,
491
+ ).fetchone()[0]
492
+ new_count -= conflicted
493
+ updated_count += conflicted
494
+ _record_sightings(conn, run_id, topic_id, with_urls, existing_by_url)
495
+ conn.execute(
496
+ "UPDATE research_runs SET findings_new = ?, findings_updated = ? WHERE id = ?",
497
+ (new_count, updated_count, run_id),
498
+ )
499
+ conn.commit()
500
+ finally:
501
+ conn.close()
502
+
503
+ return {"new": new_count, "updated": updated_count}
504
+
505
+
506
+ def _record_sightings(
507
+ conn: sqlite3.Connection,
508
+ run_id: int,
509
+ topic_id: int,
510
+ findings_with_urls: List[tuple[str, Dict[str, Any]]],
511
+ existing_by_url: Optional[Dict[str, sqlite3.Row]] = None,
512
+ ) -> None:
513
+ """Record the findings observed during this run.
514
+
515
+ The aggregate findings table keeps one row per URL and updates that row on
516
+ re-sighting. This ledger preserves the run/topic membership needed for
517
+ watchlist deltas and dossiers.
518
+ """
519
+ if not findings_with_urls:
520
+ return
521
+
522
+ by_url = {url: finding for url, finding in findings_with_urls}
523
+ rows_by_url = dict(existing_by_url or {})
524
+
525
+ missing_urls = [url for url in by_url if url not in rows_by_url]
526
+ if missing_urls:
527
+ placeholders = ",".join("?" for _ in missing_urls)
528
+ rows = conn.execute(
529
+ f"SELECT id, source_url FROM findings WHERE source_url IN ({placeholders})",
530
+ missing_urls,
531
+ ).fetchall()
532
+ rows_by_url.update({row["source_url"]: row for row in rows})
533
+
534
+ sighting_rows = []
535
+ for url, finding in by_url.items():
536
+ row = rows_by_url.get(url)
537
+ if row is None:
538
+ continue
539
+ sighting_rows.append((
540
+ row["id"],
541
+ run_id,
542
+ topic_id,
543
+ finding.get("source", "unknown"),
544
+ url,
545
+ finding.get("source_title") or finding.get("title", ""),
546
+ finding.get("engagement_score", 0),
547
+ finding.get("relevance_score", 0),
548
+ ))
549
+
550
+ if not sighting_rows:
551
+ return
552
+
553
+ conn.executemany(
554
+ """INSERT INTO finding_sightings
555
+ (finding_id, run_id, topic_id, source, source_url, source_title,
556
+ engagement_score, relevance_score)
557
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
558
+ ON CONFLICT(run_id, finding_id) DO UPDATE SET
559
+ topic_id = excluded.topic_id,
560
+ source = excluded.source,
561
+ source_url = excluded.source_url,
562
+ source_title = excluded.source_title,
563
+ engagement_score = excluded.engagement_score,
564
+ relevance_score = excluded.relevance_score""",
565
+ sighting_rows,
566
+ )
567
+
568
+
569
+ def get_sightings_for_run(topic_id: int, run_id: int) -> List[Dict[str, Any]]:
570
+ """Return findings observed for a topic during a specific run."""
571
+ conn = _connect()
572
+ try:
573
+ rows = conn.execute(
574
+ """SELECT * FROM finding_sightings
575
+ WHERE topic_id = ? AND run_id = ?
576
+ ORDER BY id""",
577
+ (topic_id, run_id),
578
+ ).fetchall()
579
+ return [dict(r) for r in rows]
580
+ finally:
581
+ conn.close()
582
+
583
+
584
+ def compute_topic_delta(topic_id: int) -> Dict[str, Any]:
585
+ """Compare the latest completed watchlist run with the previous run."""
586
+ runs = get_latest_completed_runs(topic_id, limit=2)
587
+ topic = _get_topic_by_id(topic_id)
588
+ topic_name = topic["name"] if topic else str(topic_id)
589
+ if len(runs) < 2:
590
+ return {
591
+ "topic": topic_name,
592
+ "status": "insufficient_history",
593
+ "message": "Need at least two completed runs to compute a delta.",
594
+ }
595
+
596
+ current_run, previous_run = runs[0], runs[1]
597
+ current = _sightings_by_url(get_sightings_for_run(topic_id, current_run["id"]))
598
+ previous = _sightings_by_url(get_sightings_for_run(topic_id, previous_run["id"]))
599
+
600
+ current_urls = set(current)
601
+ previous_urls = set(previous)
602
+ new_urls = sorted(current_urls - previous_urls)
603
+ continued_urls = sorted(current_urls & previous_urls)
604
+ dropped_urls = sorted(previous_urls - current_urls)
605
+
606
+ findings = {
607
+ "new": [current[url] for url in new_urls],
608
+ "continued": [current[url] for url in continued_urls],
609
+ "dropped": [previous[url] for url in dropped_urls],
610
+ }
611
+
612
+ return {
613
+ "topic": topic_name,
614
+ "status": "ok",
615
+ "current_run_id": current_run["id"],
616
+ "previous_run_id": previous_run["id"],
617
+ "new": len(new_urls),
618
+ "continued": len(continued_urls),
619
+ "dropped": len(dropped_urls),
620
+ "sources": _delta_source_counts(findings),
621
+ "findings": findings,
622
+ }
623
+
624
+
625
+ def _get_topic_by_id(topic_id: int) -> Optional[Dict[str, Any]]:
626
+ conn = _connect()
627
+ try:
628
+ row = conn.execute("SELECT * FROM topics WHERE id = ?", (topic_id,)).fetchone()
629
+ return dict(row) if row else None
630
+ finally:
631
+ conn.close()
632
+
633
+
634
+ def _sightings_by_url(sightings: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
635
+ """Index sightings by stable URL identity for run-to-run delta comparisons.
636
+
637
+ URL-less sightings are intentionally excluded because there is no stable
638
+ cross-run identity to classify them as new, continued, or dropped.
639
+ """
640
+ return {
641
+ sighting["source_url"]: sighting
642
+ for sighting in sightings
643
+ if sighting.get("source_url")
644
+ }
645
+
646
+
647
+ def _delta_source_counts(
648
+ findings: Dict[str, List[Dict[str, Any]]]
649
+ ) -> Dict[str, Dict[str, int]]:
650
+ sources = sorted({
651
+ finding.get("source") or "unknown"
652
+ for group in findings.values()
653
+ for finding in group
654
+ })
655
+ counts = {
656
+ source: {"new": 0, "continued": 0, "dropped": 0}
657
+ for source in sources
658
+ }
659
+ for group_name, group in findings.items():
660
+ for finding in group:
661
+ source = finding.get("source") or "unknown"
662
+ counts[source][group_name] += 1
663
+ return counts
664
+
665
+
666
+ def get_new_findings(
667
+ topic_id: int,
668
+ since: Optional[str] = None,
669
+ ) -> List[Dict[str, Any]]:
670
+ """Get findings for a topic, optionally since a date."""
671
+ conn = _connect()
672
+ try:
673
+ if since:
674
+ rows = conn.execute(
675
+ """SELECT * FROM findings
676
+ WHERE topic_id = ? AND first_seen >= ? AND dismissed = 0
677
+ ORDER BY first_seen DESC""",
678
+ (topic_id, since),
679
+ ).fetchall()
680
+ else:
681
+ rows = conn.execute(
682
+ """SELECT * FROM findings
683
+ WHERE topic_id = ? AND dismissed = 0
684
+ ORDER BY first_seen DESC""",
685
+ (topic_id,),
686
+ ).fetchall()
687
+ return [dict(r) for r in rows]
688
+ finally:
689
+ conn.close()
690
+
691
+
692
+ def search_findings(query: str, limit: int = 20) -> List[Dict[str, Any]]:
693
+ """FTS5 search across all findings with BM25 ranking."""
694
+ conn = _connect()
695
+ try:
696
+ rows = conn.execute(
697
+ """SELECT f.*, bm25(findings_fts) as rank, t.name as topic_name
698
+ FROM findings_fts
699
+ JOIN findings f ON f.id = findings_fts.rowid
700
+ LEFT JOIN topics t ON t.id = f.topic_id
701
+ WHERE findings_fts MATCH ?
702
+ ORDER BY rank
703
+ LIMIT ?""",
704
+ (query, limit),
705
+ ).fetchall()
706
+ return [dict(r) for r in rows]
707
+ finally:
708
+ conn.close()
709
+
710
+
711
+ def update_finding(finding_id: int, **kwargs):
712
+ """Update a finding's fields."""
713
+ conn = _connect()
714
+ try:
715
+ invalid_columns = sorted(set(kwargs) - _UPDATABLE_FINDING_COLUMNS)
716
+ if invalid_columns:
717
+ raise ValueError(
718
+ f"Invalid finding update fields: {', '.join(invalid_columns)}"
719
+ )
720
+ sets = ", ".join(f"{k} = ?" for k in kwargs)
721
+ values = list(kwargs.values()) + [finding_id]
722
+ conn.execute(f"UPDATE findings SET {sets} WHERE id = ?", values)
723
+ conn.commit()
724
+ finally:
725
+ conn.close()
726
+
727
+
728
+ def delete_finding(finding_id: int):
729
+ """Delete a finding."""
730
+ conn = _connect()
731
+ try:
732
+ conn.execute("DELETE FROM findings WHERE id = ?", (finding_id,))
733
+ conn.commit()
734
+ finally:
735
+ conn.close()
736
+
737
+
738
+ def dismiss_finding(finding_id: int):
739
+ """Mark a finding as dismissed."""
740
+ update_finding(finding_id, dismissed=1)
741
+
742
+
743
+ # --- Cost Tracking ---
744
+
745
+
746
+ def get_daily_cost(date: Optional[str] = None) -> float:
747
+ """Get total token cost for a given day (default: today)."""
748
+ conn = _connect()
749
+ try:
750
+ if not date:
751
+ date = datetime.now(timezone.utc).strftime("%Y-%m-%d")
752
+ row = conn.execute(
753
+ """SELECT COALESCE(SUM(token_cost), 0) as total
754
+ FROM research_runs
755
+ WHERE date(run_date) = date(?)""",
756
+ (date,),
757
+ ).fetchone()
758
+ return row["total"]
759
+ finally:
760
+ conn.close()
761
+
762
+
763
+ # --- Settings ---
764
+
765
+
766
+ def get_setting(key: str, default: Optional[str] = None) -> Optional[str]:
767
+ """Get a setting value."""
768
+ init_db()
769
+ conn = _connect()
770
+ try:
771
+ row = conn.execute(
772
+ "SELECT value FROM settings WHERE key = ?", (key,)
773
+ ).fetchone()
774
+ return row["value"] if row else default
775
+ finally:
776
+ conn.close()
777
+
778
+
779
+ def set_setting(key: str, value: str):
780
+ """Set a setting value."""
781
+ init_db()
782
+ conn = _connect()
783
+ try:
784
+ conn.execute(
785
+ """INSERT INTO settings (key, value, updated_at)
786
+ VALUES (?, ?, datetime('now'))
787
+ ON CONFLICT(key) DO UPDATE SET
788
+ value = excluded.value,
789
+ updated_at = datetime('now')""",
790
+ (key, value),
791
+ )
792
+ conn.commit()
793
+ finally:
794
+ conn.close()
795
+
796
+
797
+ # --- Stats ---
798
+
799
+
800
+ def get_stats() -> Dict[str, Any]:
801
+ """Get overall database stats."""
802
+ conn = _connect()
803
+ try:
804
+ topic_count = conn.execute("SELECT COUNT(*) FROM topics WHERE enabled = 1").fetchone()[0]
805
+ finding_count = conn.execute("SELECT COUNT(*) FROM findings").fetchone()[0]
806
+
807
+ week_ago = (datetime.now(timezone.utc) - timedelta(days=7)).strftime("%Y-%m-%d")
808
+ runs_7d = conn.execute(
809
+ "SELECT COUNT(*) FROM research_runs WHERE run_date >= ?", (week_ago,)
810
+ ).fetchone()[0]
811
+ successful_7d = conn.execute(
812
+ "SELECT COUNT(*) FROM research_runs WHERE run_date >= ? AND status = 'completed'",
813
+ (week_ago,),
814
+ ).fetchone()[0]
815
+ failed_7d = conn.execute(
816
+ "SELECT COUNT(*) FROM research_runs WHERE run_date >= ? AND status = 'failed'",
817
+ (week_ago,),
818
+ ).fetchone()[0]
819
+ cost_7d = conn.execute(
820
+ "SELECT COALESCE(SUM(token_cost), 0) FROM research_runs WHERE run_date >= ?",
821
+ (week_ago,),
822
+ ).fetchone()[0]
823
+
824
+ # Source breakdown
825
+ sources = {}
826
+ for row in conn.execute(
827
+ "SELECT source, COUNT(*) as cnt FROM findings GROUP BY source"
828
+ ).fetchall():
829
+ sources[row["source"]] = row["cnt"]
830
+
831
+ db_path = _get_db_path()
832
+ db_size = db_path.stat().st_size if db_path.exists() else 0
833
+
834
+ return {
835
+ "topics_active": topic_count,
836
+ "total_findings": finding_count,
837
+ "db_size_bytes": db_size,
838
+ "runs_7d": runs_7d,
839
+ "successful_7d": successful_7d,
840
+ "failed_7d": failed_7d,
841
+ "cost_7d": cost_7d,
842
+ "sources": sources,
843
+ "daily_budget": get_setting("daily_budget", "5.00"),
844
+ }
845
+ finally:
846
+ conn.close()
847
+
848
+
849
+ def get_trending(days: int = 7) -> List[Dict[str, Any]]:
850
+ """Get topics ranked by recent finding activity."""
851
+ conn = _connect()
852
+ try:
853
+ since = (datetime.now(timezone.utc) - timedelta(days=days)).strftime("%Y-%m-%d")
854
+ rows = conn.execute(
855
+ """SELECT t.name, t.id,
856
+ COUNT(f.id) as new_findings,
857
+ COALESCE(SUM(f.engagement_score), 0) as total_engagement
858
+ FROM topics t
859
+ LEFT JOIN findings f ON f.topic_id = t.id AND f.first_seen >= ?
860
+ WHERE t.enabled = 1
861
+ GROUP BY t.id
862
+ ORDER BY new_findings DESC""",
863
+ (since,),
864
+ ).fetchall()
865
+ return [dict(r) for r in rows]
866
+ finally:
867
+ conn.close()
868
+
869
+
870
+ def finding_from_candidate(candidate: schema.Candidate) -> Dict[str, Any]:
871
+ """Convert a ranked candidate into a persisted finding."""
872
+ primary_item = schema.candidate_primary_item(candidate)
873
+ corroborating_sources = [
874
+ source for source in schema.candidate_sources(candidate)
875
+ if source and source != candidate.source
876
+ ]
877
+ summary = candidate.explanation or candidate.snippet or ""
878
+ if corroborating_sources:
879
+ prefix = f"Also seen in: {', '.join(corroborating_sources)}."
880
+ summary = f"{prefix} {summary}".strip()
881
+ body = (
882
+ primary_item.body
883
+ if primary_item and primary_item.body
884
+ else candidate.snippet or candidate.title
885
+ )
886
+ author = primary_item.author if primary_item and primary_item.author else ""
887
+ return {
888
+ "source": candidate.source or "unknown",
889
+ "source_url": candidate.url,
890
+ "source_title": candidate.title,
891
+ "author": author,
892
+ "content": body,
893
+ "summary": summary,
894
+ "engagement_score": candidate.engagement or 0,
895
+ "relevance_score": candidate.final_score or candidate.rerank_score or candidate.local_relevance,
896
+ }
897
+
898
+
899
+ def findings_from_report(
900
+ report: schema.Report,
901
+ *,
902
+ limit: Optional[int] = None,
903
+ ) -> List[Dict[str, Any]]:
904
+ """Convert report into persisted findings.
905
+
906
+ Uses ranked candidates (post-rerank) when available for quality scores and explanations.
907
+ Supplements with raw items from items_by_source for HN/PM that didn't rank highly
908
+ but are valuable for watchlist persistence. When ranked_candidates is empty
909
+ (degraded path — rerank failed or was skipped), falls back to supplementing
910
+ all sources from items_by_source so findings aren't silently dropped.
911
+ """
912
+ findings = []
913
+ seen_urls = set()
914
+
915
+ for candidate in report.ranked_candidates:
916
+ findings.append(finding_from_candidate(candidate))
917
+ seen_urls.add(candidate.url)
918
+
919
+ supplement_sources = (
920
+ list(report.items_by_source)
921
+ if not report.ranked_candidates
922
+ else ["hackernews", "polymarket"]
923
+ )
924
+ for source_name in supplement_sources:
925
+ if source_name not in report.items_by_source:
926
+ continue
927
+ for item in report.items_by_source[source_name]:
928
+ if item.url in seen_urls:
929
+ continue
930
+ findings.append({
931
+ "source": source_name,
932
+ "source_url": item.url,
933
+ "source_title": item.title,
934
+ "author": item.author or "",
935
+ "content": item.body or "",
936
+ "summary": item.snippet or (item.body[:500] if item.body else ""),
937
+ "engagement_score": item.engagement_score or 0.0,
938
+ "relevance_score": item.local_relevance or 0.5,
939
+ })
940
+ seen_urls.add(item.url)
941
+
942
+ return findings[:limit] if limit is not None else findings
943
+
944
+
945
+ # --- CLI interface ---
946
+
947
+
948
+ def _cli_query(args):
949
+ """Handle CLI query command."""
950
+ topic = get_topic(args.topic)
951
+ if not topic:
952
+ print(json.dumps({"error": f"Topic not found: {args.topic}"}))
953
+ return
954
+
955
+ since = None
956
+ if args.since:
957
+ # Parse duration like "7d", "30d". Use UTC to match SQLite's
958
+ # datetime('now') which writes first_seen in UTC.
959
+ days = int(args.since.rstrip("d"))
960
+ since = (datetime.now(timezone.utc) - timedelta(days=days)).strftime("%Y-%m-%d")
961
+
962
+ findings = get_new_findings(topic["id"], since)
963
+ print(json.dumps({"topic": topic["name"], "findings": findings, "count": len(findings)}, default=str))
964
+
965
+
966
+ def _cli_search(args):
967
+ """Handle CLI search command."""
968
+ results = search_findings(args.query, limit=args.limit)
969
+ print(json.dumps({"query": args.query, "results": results, "count": len(results)}, default=str))
970
+
971
+
972
+ def _cli_trending(args):
973
+ """Handle CLI trending command."""
974
+ results = get_trending(args.days)
975
+ print(json.dumps({"trending": results}, default=str))
976
+
977
+
978
+ def _cli_stats(args):
979
+ """Handle CLI stats command."""
980
+ stats = get_stats()
981
+ print(json.dumps(stats, default=str))
982
+
983
+
984
+ def main():
985
+ parser = argparse.ArgumentParser(description="Query the last30days research database")
986
+ sub = parser.add_subparsers(dest="command")
987
+
988
+ # query
989
+ q = sub.add_parser("query", help="Query findings for a topic")
990
+ q.add_argument("topic", help="Topic name")
991
+ q.add_argument("--since", help="Duration like '7d' or '30d'")
992
+ q.set_defaults(func=_cli_query)
993
+
994
+ # search
995
+ s = sub.add_parser("search", help="Full-text search across findings")
996
+ s.add_argument("query", help="Search query")
997
+ s.add_argument("--limit", type=int, default=20, help="Max results")
998
+ s.set_defaults(func=_cli_search)
999
+
1000
+ # trending
1001
+ t = sub.add_parser("trending", help="Show trending topics")
1002
+ t.add_argument("--days", type=int, default=7, help="Look back N days")
1003
+ t.set_defaults(func=_cli_trending)
1004
+
1005
+ # stats
1006
+ st = sub.add_parser("stats", help="Show database stats")
1007
+ st.set_defaults(func=_cli_stats)
1008
+
1009
+ args = parser.parse_args()
1010
+ if not args.command:
1011
+ parser.print_help()
1012
+ sys.exit(1)
1013
+
1014
+ # Ensure DB exists
1015
+ init_db()
1016
+ args.func(args)
1017
+
1018
+
1019
+ if __name__ == "__main__":
1020
+ main()