luckiest-co 1.0.47 → 1.0.49

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 (274) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/package.json +1 -1
  4. package/skills/ab-testing/CHANGELOG.md +5 -0
  5. package/skills/ab-testing/SKILL.md +12 -9
  6. package/skills/ab-testing/evals/evals.json +18 -7
  7. package/skills/ab-testing/references/sample-size-guide.md +2 -2
  8. package/skills/ads/CHANGELOG.md +5 -0
  9. package/skills/ads/SKILL.md +4 -4
  10. package/skills/ai-seo/CHANGELOG.md +7 -0
  11. package/skills/ai-seo/SKILL.md +27 -20
  12. package/skills/analytics/CHANGELOG.md +6 -0
  13. package/skills/analytics/SKILL.md +4 -3
  14. package/skills/analytics/evals/evals.json +27 -0
  15. package/skills/analytics/references/ga4-implementation.md +7 -3
  16. package/skills/analytics/references/gtm-implementation.md +8 -2
  17. package/skills/aso/CHANGELOG.md +5 -0
  18. package/skills/aso/SKILL.md +4 -4
  19. package/skills/aso/evals/evals.json +15 -2
  20. package/skills/aso/references/benchmarks.md +4 -2
  21. package/skills/aso/references/google-play-specs.md +5 -1
  22. package/skills/brainstorm/SKILL.md +59 -16
  23. package/skills/brainstorm/builder-check-prompt.md +23 -0
  24. package/skills/churn-prevention/CHANGELOG.md +5 -0
  25. package/skills/churn-prevention/SKILL.md +3 -3
  26. package/skills/churn-prevention/evals/evals.json +14 -0
  27. package/skills/churn-prevention/references/dunning-playbook.md +6 -1
  28. package/skills/coder-brainstorming/CHANGELOG.md +10 -0
  29. package/skills/coder-brainstorming/SKILL.md +62 -20
  30. package/skills/coder-brainstorming/builder-check-prompt.md +23 -0
  31. package/skills/coder-brainstorming/scripts/server.cjs +1 -1
  32. package/skills/coder-debugging/CHANGELOG.md +4 -0
  33. package/skills/coder-debugging/SKILL.md +5 -5
  34. package/skills/coder-requesting-code-review/CHANGELOG.md +4 -0
  35. package/skills/coder-requesting-code-review/SKILL.md +4 -4
  36. package/skills/coder-subagent-driven-development/CHANGELOG.md +4 -0
  37. package/skills/coder-subagent-driven-development/SKILL.md +3 -3
  38. package/skills/coder-subagent-driven-development/scripts/sdd-workspace +3 -1
  39. package/skills/cold-email/CHANGELOG.md +7 -0
  40. package/skills/cold-email/SKILL.md +27 -7
  41. package/skills/cold-email/evals/evals.json +30 -0
  42. package/skills/cold-email/references/benchmarks.md +15 -11
  43. package/skills/cold-email/references/deliverability.md +110 -0
  44. package/skills/cold-email/references/linkedin-outreach.md +91 -0
  45. package/skills/cold-email/references/multichannel-cadence.md +79 -0
  46. package/skills/cold-email/references/outbound-plays.md +88 -0
  47. package/skills/cold-email/references/personalization.md +11 -0
  48. package/skills/cold-email/references/reply-handling.md +65 -0
  49. package/skills/competitor-profiling/CHANGELOG.md +5 -0
  50. package/skills/competitor-profiling/SKILL.md +4 -4
  51. package/skills/content-strategy/CHANGELOG.md +5 -0
  52. package/skills/content-strategy/SKILL.md +5 -3
  53. package/skills/copy-editing/CHANGELOG.md +5 -0
  54. package/skills/copy-editing/SKILL.md +5 -5
  55. package/skills/copy-editing/references/content-refresh.md +33 -0
  56. package/skills/cro/CHANGELOG.md +5 -0
  57. package/skills/cro/SKILL.md +14 -3
  58. package/skills/cro/references/form.md +1 -1
  59. package/skills/directory-submissions/CHANGELOG.md +5 -0
  60. package/skills/directory-submissions/SKILL.md +13 -4
  61. package/skills/directory-submissions/evals/evals.json +15 -1
  62. package/skills/directory-submissions/references/directory-list.md +2 -4
  63. package/skills/emails/CHANGELOG.md +5 -0
  64. package/skills/emails/SKILL.md +14 -4
  65. package/skills/launch/CHANGELOG.md +7 -0
  66. package/skills/launch/SKILL.md +8 -4
  67. package/skills/launch/evals/evals.json +29 -0
  68. package/skills/launch/references/shipped-changes.md +84 -0
  69. package/skills/launch/references/site-launch-qa.md +55 -0
  70. package/skills/lead-magnets/CHANGELOG.md +5 -0
  71. package/skills/lead-magnets/SKILL.md +6 -4
  72. package/skills/lead-magnets/evals/evals.json +18 -0
  73. package/skills/lead-magnets/references/content-to-skill-corpus-design.md +89 -0
  74. package/skills/lead-magnets/references/content-to-skill-release.md +71 -0
  75. package/skills/lead-magnets/references/content-to-skill.md +108 -0
  76. package/skills/marketing-plan/CHANGELOG.md +5 -0
  77. package/skills/marketing-plan/SKILL.md +3 -3
  78. package/skills/marketing-plan/evals/evals.json +13 -0
  79. package/skills/marketing-plan/references/budget-planning.md +26 -17
  80. package/skills/model-router/CHANGELOG.md +6 -0
  81. package/skills/model-router/SKILL.md +3 -3
  82. package/skills/model-router/scripts/route.py +1 -1
  83. package/skills/model-router/scripts/test_route.py +5 -0
  84. package/skills/pricing/CHANGELOG.md +5 -0
  85. package/skills/pricing/SKILL.md +4 -4
  86. package/skills/pricing/evals/evals.json +12 -0
  87. package/skills/pricing/references/research-methods.md +8 -4
  88. package/skills/programmatic-seo/CHANGELOG.md +5 -0
  89. package/skills/programmatic-seo/SKILL.md +8 -4
  90. package/skills/programmatic-seo/evals/evals.json +13 -0
  91. package/skills/programmatic-seo/references/implementation-platforms.md +37 -0
  92. package/skills/prospecting/CHANGELOG.md +7 -0
  93. package/skills/prospecting/SKILL.md +27 -13
  94. package/skills/prospecting/evals/evals.json +41 -0
  95. package/skills/prospecting/references/account-research.md +59 -0
  96. package/skills/prospecting/references/b2b-prospecting.md +1 -1
  97. package/skills/prospecting/references/compliance.md +44 -6
  98. package/skills/prospecting/references/data-sources.md +13 -15
  99. package/skills/prospecting/references/enrichment-playbook.md +61 -0
  100. package/skills/prospecting/references/saas-prospecting.md +1 -1
  101. package/skills/prospecting/references/signal-plays.md +99 -0
  102. package/skills/prospecting/references/sourcing-and-enrichment.md +89 -0
  103. package/skills/referrals/CHANGELOG.md +5 -0
  104. package/skills/referrals/SKILL.md +10 -4
  105. package/skills/referrals/evals/evals.json +99 -0
  106. package/skills/referrals/references/affiliate-operations.md +64 -0
  107. package/skills/referrals/references/affiliate-programs.md +2 -2
  108. package/skills/referrals/references/program-examples.md +14 -6
  109. package/skills/referrals/references/review-platform-rules.md +36 -0
  110. package/skills/referrals/references/review-program-operations.md +62 -0
  111. package/skills/referrals/references/review-programs.md +96 -0
  112. package/skills/revops/CHANGELOG.md +5 -0
  113. package/skills/revops/SKILL.md +10 -6
  114. package/skills/revops/evals/evals.json +13 -0
  115. package/skills/revops/references/automation-playbooks.md +1 -1
  116. package/skills/revops/references/inbound-routing.md +43 -0
  117. package/skills/revops/references/lifecycle-definitions.md +31 -0
  118. package/skills/sales-enablement/CHANGELOG.md +5 -0
  119. package/skills/sales-enablement/SKILL.md +6 -4
  120. package/skills/sales-enablement/evals/evals.json +14 -0
  121. package/skills/sales-enablement/references/cold-call-scripts.md +68 -0
  122. package/skills/schema/CHANGELOG.md +5 -0
  123. package/skills/schema/SKILL.md +20 -8
  124. package/skills/schema/evals/evals.json +40 -0
  125. package/skills/schema/references/schema-examples.md +9 -15
  126. package/skills/seo-audit/CHANGELOG.md +5 -0
  127. package/skills/seo-audit/SKILL.md +10 -16
  128. package/skills/seo-audit/evals/evals.json +68 -0
  129. package/skills/seo-audit/references/international-seo.md +9 -11
  130. package/skills/seo-audit/references/local-seo-pages-and-measurement.md +39 -0
  131. package/skills/seo-audit/references/local-seo-profile-and-citations.md +41 -0
  132. package/skills/seo-audit/references/local-seo.md +86 -0
  133. package/skills/seo-audit/references/rankings-push.md +62 -0
  134. package/skills/seo-audit/references/title-tags.md +89 -0
  135. package/skills/signup/CHANGELOG.md +5 -0
  136. package/skills/signup/SKILL.md +7 -3
  137. package/skills/site-architecture/CHANGELOG.md +5 -0
  138. package/skills/site-architecture/SKILL.md +5 -4
  139. package/skills/site-architecture/evals/evals.json +25 -0
  140. package/skills/site-architecture/references/platforms-and-migration.md +84 -0
  141. package/skills/social/CHANGELOG.md +5 -0
  142. package/skills/social/SKILL.md +4 -4
  143. package/skills/trends/.skillignore +3 -0
  144. package/skills/trends/CHANGELOG.md +20 -0
  145. package/skills/trends/SKILL.md +12 -8
  146. package/skills/trends/references/agent-mode.md +36 -0
  147. package/skills/trends/references/category-peers.md +38 -0
  148. package/skills/trends/references/comparison.md +227 -0
  149. package/skills/trends/references/competitors.md +29 -0
  150. package/skills/trends/references/discovery.md +90 -0
  151. package/skills/trends/references/engine-contract.md +111 -1889
  152. package/skills/trends/references/followups.md +112 -0
  153. package/skills/trends/references/grok-bot-x.md +55 -0
  154. package/skills/trends/references/hiring-signals.md +19 -0
  155. package/skills/trends/references/library-queue.md +33 -0
  156. package/skills/trends/references/recommendations.md +86 -0
  157. package/skills/trends/references/research-runbook.md +781 -0
  158. package/skills/trends/references/runtime.md +165 -0
  159. package/skills/trends/references/save-html-brief.md +39 -20
  160. package/skills/trends/references/setup-wizard.md +253 -0
  161. package/skills/trends/references/synthesis.md +289 -0
  162. package/skills/trends/scripts/box_chrome_login.py +209 -0
  163. package/skills/trends/scripts/briefing.py +9 -17
  164. package/skills/trends/scripts/last30days.py +3247 -280
  165. package/skills/trends/scripts/lib/agentcookie.py +196 -0
  166. package/skills/trends/scripts/lib/amazon.py +877 -0
  167. package/skills/trends/scripts/lib/arxiv.py +42 -8
  168. package/skills/trends/scripts/lib/backends.py +769 -0
  169. package/skills/trends/scripts/lib/bird_x.py +340 -57
  170. package/skills/trends/scripts/lib/bluesky.py +84 -10
  171. package/skills/trends/scripts/lib/bounded_get.py +156 -0
  172. package/skills/trends/scripts/lib/brightdata.py +291 -0
  173. package/skills/trends/scripts/lib/categories.py +7 -1
  174. package/skills/trends/scripts/lib/chrome_cdp.py +455 -0
  175. package/skills/trends/scripts/lib/chrome_cookies.py +132 -39
  176. package/skills/trends/scripts/lib/cluster.py +31 -54
  177. package/skills/trends/scripts/lib/competitors.py +10 -1
  178. package/skills/trends/scripts/lib/cookie_extract.py +221 -44
  179. package/skills/trends/scripts/lib/cookie_paths.py +27 -0
  180. package/skills/trends/scripts/lib/corpus.py +439 -0
  181. package/skills/trends/scripts/lib/devto.py +313 -0
  182. package/skills/trends/scripts/lib/discovery_handoff.py +988 -0
  183. package/skills/trends/scripts/lib/doctor.py +2166 -0
  184. package/skills/trends/scripts/lib/dripstack.py +203 -0
  185. package/skills/trends/scripts/lib/entity_extract.py +37 -0
  186. package/skills/trends/scripts/lib/env.py +1092 -138
  187. package/skills/trends/scripts/lib/fanout.py +11 -1
  188. package/skills/trends/scripts/lib/feed.py +61 -0
  189. package/skills/trends/scripts/lib/fetch_memo.py +135 -0
  190. package/skills/trends/scripts/lib/freshness.py +566 -0
  191. package/skills/trends/scripts/lib/fusion.py +314 -28
  192. package/skills/trends/scripts/lib/github.py +438 -24
  193. package/skills/trends/scripts/lib/grok_x.py +1237 -0
  194. package/skills/trends/scripts/lib/grounding.py +129 -11
  195. package/skills/trends/scripts/lib/hackernews.py +61 -4
  196. package/skills/trends/scripts/lib/health.py +373 -0
  197. package/skills/trends/scripts/lib/hosted.py +333 -0
  198. package/skills/trends/scripts/lib/html_publish.py +34 -0
  199. package/skills/trends/scripts/lib/html_render.py +169 -3
  200. package/skills/trends/scripts/lib/http.py +1215 -70
  201. package/skills/trends/scripts/lib/instagram.py +132 -9
  202. package/skills/trends/scripts/lib/jobs.py +4 -2
  203. package/skills/trends/scripts/lib/library.py +308 -0
  204. package/skills/trends/scripts/lib/library_index.py +477 -0
  205. package/skills/trends/scripts/lib/linkedin.py +271 -24
  206. package/skills/trends/scripts/lib/log.py +5 -2
  207. package/skills/trends/scripts/lib/meta_ads.py +982 -0
  208. package/skills/trends/scripts/lib/normalize.py +382 -28
  209. package/skills/trends/scripts/lib/parallel_mcp.py +234 -0
  210. package/skills/trends/scripts/lib/permission_preflight.py +42 -3
  211. package/skills/trends/scripts/lib/perplexity.py +1080 -468
  212. package/skills/trends/scripts/lib/pipeline.py +5079 -211
  213. package/skills/trends/scripts/lib/planner.py +396 -53
  214. package/skills/trends/scripts/lib/polymarket.py +285 -23
  215. package/skills/trends/scripts/lib/prescriptions.py +432 -0
  216. package/skills/trends/scripts/lib/private_files.py +21 -0
  217. package/skills/trends/scripts/lib/providers.py +231 -10
  218. package/skills/trends/scripts/lib/quality_nudge.py +122 -37
  219. package/skills/trends/scripts/lib/reddit.py +177 -12
  220. package/skills/trends/scripts/lib/reddit_arctic.py +146 -5
  221. package/skills/trends/scripts/lib/reddit_keyless.py +121 -60
  222. package/skills/trends/scripts/lib/reddit_listing.py +215 -14
  223. package/skills/trends/scripts/lib/reddit_public.py +12 -13
  224. package/skills/trends/scripts/lib/reddit_search.py +304 -0
  225. package/skills/trends/scripts/lib/reddit_shreddit.py +51 -7
  226. package/skills/trends/scripts/lib/registers.py +134 -0
  227. package/skills/trends/scripts/lib/relevance.py +17 -0
  228. package/skills/trends/scripts/lib/render.py +2161 -271
  229. package/skills/trends/scripts/lib/rerank.py +623 -55
  230. package/skills/trends/scripts/lib/resolve.py +50 -1
  231. package/skills/trends/scripts/lib/safari_cookies.py +42 -15
  232. package/skills/trends/scripts/lib/schema.py +782 -1
  233. package/skills/trends/scripts/lib/setup_wizard.py +779 -120
  234. package/skills/trends/scripts/lib/signals.py +206 -7
  235. package/skills/trends/scripts/lib/stocktwits.py +374 -0
  236. package/skills/trends/scripts/lib/subproc.py +307 -53
  237. package/skills/trends/scripts/lib/techmeme.py +89 -64
  238. package/skills/trends/scripts/lib/telegram.py +317 -0
  239. package/skills/trends/scripts/lib/tiktok.py +13 -9
  240. package/skills/trends/scripts/lib/topic_shape.py +340 -0
  241. package/skills/trends/scripts/lib/transcribe.py +12 -3
  242. package/skills/trends/scripts/lib/trustpilot.py +256 -5
  243. package/skills/trends/scripts/lib/truthsocial.py +9 -1
  244. package/skills/trends/scripts/lib/ui.py +6 -0
  245. package/skills/trends/scripts/lib/usage.py +132 -0
  246. package/skills/trends/scripts/lib/web_search_keyless.py +63 -4
  247. package/skills/trends/scripts/lib/windows_job.py +138 -0
  248. package/skills/trends/scripts/lib/x_api.py +824 -0
  249. package/skills/trends/scripts/lib/x_envelope.py +733 -0
  250. package/skills/trends/scripts/lib/x_judge.py +292 -0
  251. package/skills/trends/scripts/lib/xai_x.py +26 -3
  252. package/skills/trends/scripts/lib/xquik.py +89 -15
  253. package/skills/trends/scripts/lib/xurl_x.py +284 -91
  254. package/skills/trends/scripts/lib/youtube_yt.py +570 -119
  255. package/skills/trends/scripts/setup-keychain.sh +12 -4
  256. package/skills/trends/scripts/setup-pass.sh +3 -0
  257. package/skills/trends/scripts/store.py +444 -50
  258. package/skills/trends/scripts/watchlist.py +38 -11
  259. package/skills/trends/scripts/x_lane_parity.py +107 -0
  260. package/skills/video-studio/CHANGELOG.md +4 -0
  261. package/skills/video-studio/SKILL.md +4 -4
  262. package/skills/video-studio/references/styles/product-flight.md +45 -0
  263. package/skills/video-studio/references/styles.md +3 -1
  264. package/skills/video-studio-motion/ATTRIBUTION.md +1 -1
  265. package/skills/video-studio-motion/CHANGELOG.md +4 -0
  266. package/skills/video-studio-motion/SKILL.md +8 -5
  267. package/skills/youtube-planner/ATTRIBUTION.md +25 -0
  268. package/skills/youtube-planner/CHANGELOG.md +12 -0
  269. package/skills/youtube-planner/SKILL.md +191 -0
  270. package/skills/youtube-planner/references/plan-template.md +57 -0
  271. package/skills/youtube-planner/references/stages.md +119 -0
  272. package/skills/youtube-planner/scripts/floor_ceiling.py +132 -0
  273. package/skills/coder-brainstorming/spec-document-reviewer-prompt.md +0 -49
  274. package/skills/trends/scripts/lib/reddit_rss.py +0 -224
@@ -0,0 +1,340 @@
1
+ """Deterministic topic naming and junk-shape classification for discovery.
2
+
3
+ Discovery mode surfaces short, named, content-worthy topics instead of raw
4
+ post titles. This module is the pure-function, stdlib-only stage-1 fallback
5
+ for that pipeline (used when no LLM is available, and as the deterministic
6
+ baseline the LLM path is judged against):
7
+
8
+ - ``distill_topic_name(title, snippet)`` distills a listing title into a
9
+ 2-6 word searchable topic name: question/framing scaffolding is stripped,
10
+ proper-noun / digit-bearing entity phrases are preferred and emitted as an
11
+ ORDERED phrase in title order (never a bag of words), and the cleaned,
12
+ truncated title is the final fallback so the result is never empty for any
13
+ title with word content.
14
+ - ``is_junk_shape(title, snippet)`` flags listing shapes that should never
15
+ become topics: help-me questions, beginner asks, and first-person musings.
16
+ Launch titles ("Show HN: ...") and entity-bearing news statements are not
17
+ junk.
18
+
19
+ Both functions take plain strings and return plain values - no candidate
20
+ objects, no config, no I/O - so they are trivially testable and reusable.
21
+
22
+ Names produced here are used downstream as short search queries and grounding
23
+ strings, so they never carry trailing punctuation or quote characters. Per the
24
+ head-token convention, callers must never assume a distilled name appears as a
25
+ contiguous substring of any document.
26
+
27
+ Token conventions (stopwords, capital/digit entity signals) are inherited from
28
+ ``entity_extract`` and extended here; unlike ``extract_text_entities`` this
29
+ module preserves title order and original casing because the output is a
30
+ human-readable phrase, not a matching set. Non-Latin (CJK) titles never crash:
31
+ they carry no Latin entity signal, so they fall through to the cleaned-title
32
+ path, capped at ``_MAX_NAME_CHARS``.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import re
38
+ from typing import List, NamedTuple, Optional
39
+
40
+ from .entity_extract import ENTITY_STOPWORDS
41
+
42
+ _MAX_NAME_WORDS = 6
43
+ _MAX_NAME_CHARS = 80
44
+
45
+ # Extends the shared entity stopwords with pronouns, auxiliaries, contractions
46
+ # and musing filler that read as capitalized sentence-openers in titles but are
47
+ # never entities ("My", "Everyone", "Don't", ...). Deliberate casualty: the
48
+ # acronyms "US" and "IT" are swallowed by their pronoun homographs.
49
+ _ANCHOR_STOPWORDS = frozenset(ENTITY_STOPWORDS) | frozenset({
50
+ "i", "i'm", "i've", "i'd", "i'll", "me", "my", "mine", "myself",
51
+ "we", "we're", "we've", "our", "ours", "us",
52
+ "you", "you're", "your", "yours",
53
+ "am", "were", "be", "why", "when", "where", "which", "whom", "whose",
54
+ "does", "did", "doing", "done", "should", "shall", "may", "might", "must",
55
+ "if", "or", "so", "as", "any", "anyone", "anybody", "someone", "somebody",
56
+ "everyone", "everybody", "nobody", "none", "no", "yes",
57
+ "please", "thanks", "thank", "really", "actually", "very", "well",
58
+ "while", "during", "still", "even", "ever", "never", "always",
59
+ "don't", "dont", "can't", "cant", "won't", "wont", "isn't", "isnt",
60
+ "aren't", "arent", "doesn't", "doesnt", "didn't", "didnt",
61
+ "it's", "that's", "there's", "here's", "let's", "what's", "who's", "how's",
62
+ "mean", "means", "meant", "same", "thing", "things", "stuff",
63
+ "way", "ways", "lot", "lots", "kind", "sort",
64
+ "today", "yesterday", "tomorrow",
65
+ })
66
+
67
+ # Characters stripped from token edges for display (internal hyphens/dots in
68
+ # "open-source" / "example.com" survive). Includes unicode dashes/ellipsis.
69
+ _EDGE_CHARS = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~–—…"
70
+ _TRAILING_JUNK = ".,;:!?…'\"`- "
71
+
72
+ _POSSESSIVE_RE = re.compile(r"(?<=\w)'s\b", re.IGNORECASE)
73
+ _DOUBLE_QUOTE_RE = re.compile(r"[\"“”„«»]")
74
+ _LONE_APOSTROPHE_RE = re.compile(r"(?<!\w)'|'(?!\w)")
75
+ _SENTENCE_END_RE = re.compile(r"[.!?;:,]$")
76
+
77
+ # Framing scaffolding stripped (iteratively) from the start of a title before
78
+ # naming: forum labels, interrogative openers, first-person setup, politeness
79
+ # filler, and leading articles. Junk *classification* has its own patterns
80
+ # below; these only clean the string we name from.
81
+ _SCAFFOLD_RES = [re.compile(p, re.IGNORECASE) for p in (
82
+ r"^(show hn|ask hn|tell hn|launch hn|psa|eli5|tifu|til|discussion|"
83
+ r"question|help|advice|update|rant|vent|meta)\s*[:\-–—]\s*",
84
+ r"^(how|what|when|where|which|why|who)\s+"
85
+ r"(do|does|did|is|are|was|were|am|can|could|should|would|will|to|i|we|you|your|my|one)\s+",
86
+ r"^(is|are|does|do|did|can|could|should|would|will|has|have|am)\s+"
87
+ r"(there|it|this|anyone|anybody|someone|somebody|we|you|i|they|my|your)\s+",
88
+ r"^(i|we)\s+(think|believe|feel|guess|wonder|noticed|realized|have run|"
89
+ r"have been|have|had|am|was|were|just|finally|recently|need|want|"
90
+ r"would like|tried|keep|built|made|created|wrote|spent)\s+",
91
+ r"^(i'm|i've|i'd|we're|we've)\s+",
92
+ r"^my\s+(coworker|co-worker|colleague|boss|friend|manager|team|company|"
93
+ r"startup|wife|husband|partner|mom|dad|mother|father|brother|sister|"
94
+ r"son|daughter|kid|kids|roommate|neighbor)\s+\w+\s+",
95
+ r"^(hey|hi|hello|guys|folks|please|okay|ok|so|honestly|serious question)[,!\s]\s*",
96
+ r"^(a|an|the)\s+",
97
+ )]
98
+
99
+ # --- junk-shape markers (matched against the cleaned, lowercased title) -----
100
+
101
+ _LAUNCH_RE = re.compile(r"^(show hn|launch hn)\b")
102
+ # Leading interrogatives: wh-words count only with a question follow-through
103
+ # ("What is the best..." is junk; "What Gemma 4 means..." is an explainer).
104
+ _WH_JUNK_RE = re.compile(
105
+ r"^(how|what|why|when|where|which|who)\s+"
106
+ r"(do|does|did|is|are|was|were|am|can|could|should|would|will|to|i|we|you|your|my|one)\b"
107
+ )
108
+ _AUX_JUNK_RE = re.compile(
109
+ r"^(is|are|does|do|did|can|could|should|would|will|has|have|am)\s+"
110
+ r"(there|it|this|anyone|anybody|someone|somebody|we|you|i|they|my|your)\b"
111
+ )
112
+ _HELP_RE = re.compile(
113
+ r"\bneed (some |a little )?(help|advice)\b|\bplease help\b|\bhelp me\b|"
114
+ r"^help\b|\bany (advice|recommendation|recommendations|suggestions|recs|tips)\b|"
115
+ r"\blooking for (advice|recommendations|suggestions|help|tips)\b|"
116
+ r"\bwhere (do|should|would) (i|we) (even )?(start|begin)\b|\bwhere to start\b|"
117
+ r"\bbeginner (question|here)\b|\bnoob (question|here)\b|"
118
+ r"\btotal beginner\b|\bcomplete beginner\b|\bam i missing something\b|"
119
+ r"\brecommend me\b"
120
+ )
121
+ _MUSING_RE = re.compile(
122
+ r"^(i think|i feel|i believe|i guess|i wonder|i have been|i've been|i keep|"
123
+ r"my thoughts|thoughts on|unpopular opinion|hot take|am i the only one|"
124
+ r"is it just me|anyone else|does anyone else|rant|vent|change my mind|cmv)\b"
125
+ )
126
+ _EVERYONE_RE = re.compile(
127
+ r"\beveryone (is|does|says|seems|keeps|wants)\b.{0,80}\bbut (do|are|can|should|will|did) we\b"
128
+ )
129
+
130
+
131
+ class _Token(NamedTuple):
132
+ display: str # edge-punctuation-stripped, original casing
133
+ lower: str
134
+ is_anchor: bool # proper-noun / digit / acronym entity signal
135
+ breaks_after: bool # sentence/clause boundary follows this token
136
+
137
+
138
+ def distill_topic_name(title: str, snippet: str = "") -> str:
139
+ """Distill a listing title (+ optional snippet) into a 2-6 word topic name.
140
+
141
+ The name is an ordered phrase built from entity anchors in title order,
142
+ safe to use as a short search query: <= 6 words, <= 80 chars, no trailing
143
+ punctuation, no quote characters. Never empty for any input with word
144
+ content (the sole exception: title AND snippet contain no word characters,
145
+ which returns "").
146
+ """
147
+ base = _normalize(title) or _normalize(snippet)
148
+ if not base:
149
+ return ""
150
+
151
+ stripped = _strip_scaffolding(base)
152
+ tokens = _tokenize(stripped)
153
+ if not tokens:
154
+ tokens = _tokenize(base)
155
+ if not tokens:
156
+ return ""
157
+ words = [t.display for t in tokens]
158
+
159
+ # Already-short titles pass through unless a stronger entity phrase is
160
+ # buried mid-title (first word not an anchor while anchors exist).
161
+ if len(words) <= _MAX_NAME_WORDS and (tokens[0].is_anchor or not any(t.is_anchor for t in tokens)):
162
+ return _finalize(words)
163
+
164
+ phrase = _entity_phrase(tokens)
165
+ if phrase:
166
+ return _finalize(phrase)
167
+
168
+ # Title had no entity anchors: try the snippet's leading entity phrase.
169
+ if snippet:
170
+ snippet_tokens = _tokenize(_strip_scaffolding(_normalize(snippet)))
171
+ snippet_phrase = _entity_phrase(snippet_tokens)
172
+ if snippet_phrase:
173
+ return _finalize(snippet_phrase)
174
+
175
+ # Final fallback: cleaned title truncated to the word cap.
176
+ return _finalize(words[:_MAX_NAME_WORDS])
177
+
178
+
179
+ def is_junk_shape(title: str, snippet: str = "") -> bool:
180
+ """True when the listing shape is not content-worthy.
181
+
182
+ Rule-based markers: leading interrogatives, help/advice asks, first-person
183
+ musings, and trailing "?" with no named entity in the title. Launch titles
184
+ ("Show HN: ...") and entity-bearing news statements are not junk. The
185
+ snippet is consulted only when the title itself has no entity anchors.
186
+ """
187
+ cleaned = _normalize(title)
188
+ if not cleaned:
189
+ cleaned = _normalize(snippet)
190
+ if not cleaned:
191
+ return True # nothing nameable at all
192
+ lower = cleaned.lower()
193
+
194
+ if _LAUNCH_RE.search(lower):
195
+ return False
196
+ if _WH_JUNK_RE.search(lower) or _AUX_JUNK_RE.search(lower):
197
+ return True
198
+ if _HELP_RE.search(lower) or _MUSING_RE.search(lower) or _EVERYONE_RE.search(lower):
199
+ return True
200
+
201
+ has_entity = any(t.is_anchor for t in _tokenize(cleaned))
202
+ if lower.endswith(("?", "?")) and not has_entity:
203
+ return True
204
+ if not has_entity and snippet:
205
+ snippet_lower = _normalize(snippet).lower()
206
+ if (_HELP_RE.search(snippet_lower) or _MUSING_RE.search(snippet_lower)
207
+ or _AUX_JUNK_RE.search(snippet_lower) or _EVERYONE_RE.search(snippet_lower)):
208
+ return True
209
+ return False
210
+
211
+
212
+ # ---------------------------------------------------------------------------
213
+
214
+
215
+ def _normalize(text: str) -> str:
216
+ """Collapse whitespace, drop quote characters, fold possessives ("4's" -> "4")."""
217
+ if not text:
218
+ return ""
219
+ text = text.replace("’", "'").replace("‘", "'").replace("`", "'").replace("´", "'")
220
+ text = _POSSESSIVE_RE.sub("", text)
221
+ text = _DOUBLE_QUOTE_RE.sub(" ", text)
222
+ text = _LONE_APOSTROPHE_RE.sub(" ", text)
223
+ return " ".join(text.split())
224
+
225
+
226
+ def _strip_scaffolding(text: str) -> str:
227
+ """Iteratively strip question/framing scaffolding from the title start."""
228
+ for _ in range(6):
229
+ before = text
230
+ for pattern in _SCAFFOLD_RES:
231
+ text = pattern.sub("", text, count=1).lstrip(" ,-")
232
+ if text == before:
233
+ break
234
+ return text.strip()
235
+
236
+
237
+ def _is_anchor(display: str) -> bool:
238
+ """Entity signal per entity_extract conventions: capitals, digits, acronyms."""
239
+ if not display:
240
+ return False
241
+ if display.lower() in _ANCHOR_STOPWORDS:
242
+ return False
243
+ if any(c.isdigit() for c in display):
244
+ return True
245
+ if len(display) < 2:
246
+ return False
247
+ if display[0].isupper():
248
+ return True
249
+ return any(c.isupper() for c in display[1:]) # iPhone, gpt4all-style
250
+
251
+
252
+ def _tokenize(text: str) -> List[_Token]:
253
+ """Split into display tokens, tagging entity anchors and clause boundaries."""
254
+ tokens: List[_Token] = []
255
+ for raw in text.split():
256
+ display = raw.strip(_EDGE_CHARS)
257
+ if not display:
258
+ # Pure-punctuation token (a bare dash, "..."): clause boundary.
259
+ if tokens:
260
+ tokens[-1] = tokens[-1]._replace(breaks_after=True)
261
+ continue
262
+ tokens.append(_Token(
263
+ display=display,
264
+ lower=display.lower(),
265
+ is_anchor=_is_anchor(display),
266
+ breaks_after=bool(_SENTENCE_END_RE.search(raw)),
267
+ ))
268
+ return tokens
269
+
270
+
271
+ def _entity_phrase(tokens: List[_Token]) -> Optional[List[str]]:
272
+ """Build an ordered phrase from entity-anchor runs, in title order.
273
+
274
+ Adjacent anchor runs separated by <= 2 contentful (non-stopword,
275
+ non-boundary) words are merged with their connecting words kept, so the
276
+ phrase stays readable ("AI agent handle Slack", not "AI Slack"). Runs are
277
+ then concatenated in title order up to the word cap.
278
+ """
279
+ runs: List[tuple[int, int]] = [] # inclusive (start, end) token indices
280
+ i = 0
281
+ while i < len(tokens):
282
+ if tokens[i].is_anchor:
283
+ j = i
284
+ while j + 1 < len(tokens) and tokens[j + 1].is_anchor and not tokens[j].breaks_after:
285
+ j += 1
286
+ runs.append((i, j))
287
+ i = j + 1
288
+ else:
289
+ i += 1
290
+ if not runs:
291
+ return None
292
+
293
+ merged = [runs[0]]
294
+ for start, end in runs[1:]:
295
+ prev_start, prev_end = merged[-1]
296
+ gap = tokens[prev_end + 1:start]
297
+ if (
298
+ 0 < len(gap) <= 2
299
+ and not tokens[prev_end].breaks_after
300
+ and all(g.lower not in _ANCHOR_STOPWORDS and not g.breaks_after for g in gap)
301
+ ):
302
+ merged[-1] = (prev_start, end)
303
+ else:
304
+ merged.append((start, end))
305
+
306
+ words: List[str] = []
307
+ last_index: Optional[int] = None
308
+ for start, end in merged:
309
+ span = [t.display for t in tokens[start:end + 1]]
310
+ if not words and len(span) > _MAX_NAME_WORDS:
311
+ span = span[:_MAX_NAME_WORDS]
312
+ end = start + _MAX_NAME_WORDS - 1
313
+ if len(words) + len(span) > _MAX_NAME_WORDS:
314
+ break
315
+ words.extend(span)
316
+ last_index = end
317
+
318
+ # Readability extension: pull in one attached plural noun ("Slack replies").
319
+ if words and len(words) < _MAX_NAME_WORDS and last_index is not None:
320
+ nxt = tokens[last_index + 1] if last_index + 1 < len(tokens) else None
321
+ if (
322
+ nxt is not None
323
+ and not tokens[last_index].breaks_after
324
+ and not nxt.is_anchor
325
+ and nxt.display.islower()
326
+ and nxt.display.endswith("s")
327
+ and nxt.lower not in _ANCHOR_STOPWORDS
328
+ ):
329
+ words.append(nxt.display)
330
+
331
+ return words or None
332
+
333
+
334
+ def _finalize(words: List[str]) -> str:
335
+ """Join to a query-safe name: char cap, no trailing punctuation or quotes."""
336
+ name = " ".join(w for w in words if w).strip()
337
+ if len(name) > _MAX_NAME_CHARS:
338
+ name = name[:_MAX_NAME_CHARS].rstrip()
339
+ name = name.strip(_TRAILING_JUNK)
340
+ return " ".join(name.split())
@@ -24,7 +24,7 @@ import tempfile
24
24
  from dataclasses import dataclass, field
25
25
  from typing import Optional
26
26
 
27
- from . import env, health, subproc
27
+ from . import env, health, http, log, subproc, usage
28
28
 
29
29
  # Whisper's documented upload ceiling. We compress to stay under it and chunk
30
30
  # when a single clip still exceeds it.
@@ -177,7 +177,11 @@ def _transcribe_chunk(
177
177
  for name, key in providers:
178
178
  try:
179
179
  text = _post_audio(name, path, key, timeout=timeout)
180
- except Exception: # noqa: BLE001 - any provider failure -> try the next
180
+ except Exception as exc: # noqa: BLE001 - any provider failure -> try the next
181
+ # Surface the failing provider + error (e.g. Groq's Cloudflare 403,
182
+ # "error code: 1010") under LAST30DAYS_DEBUG instead of silently
183
+ # degrading to an empty transcript (#983).
184
+ log.debug(f"transcribe: provider {name} failed on a chunk: {exc!r}")
181
185
  text = None
182
186
  if text is not None:
183
187
  return text, name
@@ -214,10 +218,15 @@ def _post_audio(provider: str, path: str, api_key: str, timeout: float) -> Optio
214
218
  headers={
215
219
  "Authorization": f"Bearer {api_key}",
216
220
  "Content-Type": f"multipart/form-data; boundary={boundary}",
221
+ # A plain-urllib UA is fingerprinted and 403'd by Cloudflare-style
222
+ # edges (Groq returns "error code: 1010", #983). Send the skill's
223
+ # canonical identity instead, same as lib/http.py's get/post.
224
+ "User-Agent": http.USER_AGENT,
217
225
  },
218
226
  method="POST",
219
227
  )
220
- with urllib.request.urlopen(req, timeout=timeout) as resp:
228
+ usage.begin(provider)
229
+ with http.open_request(req, timeout) as resp:
221
230
  data = json.loads(resp.read().decode("utf-8"))
222
231
  return data.get("text")
223
232