luckiest-co 1.0.46 → 1.0.48

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 (266) 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/pricing/CHANGELOG.md +5 -0
  81. package/skills/pricing/SKILL.md +4 -4
  82. package/skills/pricing/evals/evals.json +12 -0
  83. package/skills/pricing/references/research-methods.md +8 -4
  84. package/skills/programmatic-seo/CHANGELOG.md +5 -0
  85. package/skills/programmatic-seo/SKILL.md +8 -4
  86. package/skills/programmatic-seo/evals/evals.json +13 -0
  87. package/skills/programmatic-seo/references/implementation-platforms.md +37 -0
  88. package/skills/prospecting/CHANGELOG.md +7 -0
  89. package/skills/prospecting/SKILL.md +27 -13
  90. package/skills/prospecting/evals/evals.json +41 -0
  91. package/skills/prospecting/references/account-research.md +59 -0
  92. package/skills/prospecting/references/b2b-prospecting.md +1 -1
  93. package/skills/prospecting/references/compliance.md +44 -6
  94. package/skills/prospecting/references/data-sources.md +13 -15
  95. package/skills/prospecting/references/enrichment-playbook.md +61 -0
  96. package/skills/prospecting/references/saas-prospecting.md +1 -1
  97. package/skills/prospecting/references/signal-plays.md +99 -0
  98. package/skills/prospecting/references/sourcing-and-enrichment.md +89 -0
  99. package/skills/referrals/CHANGELOG.md +5 -0
  100. package/skills/referrals/SKILL.md +10 -4
  101. package/skills/referrals/evals/evals.json +99 -0
  102. package/skills/referrals/references/affiliate-operations.md +64 -0
  103. package/skills/referrals/references/affiliate-programs.md +2 -2
  104. package/skills/referrals/references/program-examples.md +14 -6
  105. package/skills/referrals/references/review-platform-rules.md +36 -0
  106. package/skills/referrals/references/review-program-operations.md +62 -0
  107. package/skills/referrals/references/review-programs.md +96 -0
  108. package/skills/revops/CHANGELOG.md +5 -0
  109. package/skills/revops/SKILL.md +10 -6
  110. package/skills/revops/evals/evals.json +13 -0
  111. package/skills/revops/references/automation-playbooks.md +1 -1
  112. package/skills/revops/references/inbound-routing.md +43 -0
  113. package/skills/revops/references/lifecycle-definitions.md +31 -0
  114. package/skills/sales-enablement/CHANGELOG.md +5 -0
  115. package/skills/sales-enablement/SKILL.md +6 -4
  116. package/skills/sales-enablement/evals/evals.json +14 -0
  117. package/skills/sales-enablement/references/cold-call-scripts.md +68 -0
  118. package/skills/schema/CHANGELOG.md +5 -0
  119. package/skills/schema/SKILL.md +20 -8
  120. package/skills/schema/evals/evals.json +40 -0
  121. package/skills/schema/references/schema-examples.md +9 -15
  122. package/skills/seo-audit/CHANGELOG.md +5 -0
  123. package/skills/seo-audit/SKILL.md +10 -16
  124. package/skills/seo-audit/evals/evals.json +68 -0
  125. package/skills/seo-audit/references/international-seo.md +9 -11
  126. package/skills/seo-audit/references/local-seo-pages-and-measurement.md +39 -0
  127. package/skills/seo-audit/references/local-seo-profile-and-citations.md +41 -0
  128. package/skills/seo-audit/references/local-seo.md +86 -0
  129. package/skills/seo-audit/references/rankings-push.md +62 -0
  130. package/skills/seo-audit/references/title-tags.md +89 -0
  131. package/skills/signup/CHANGELOG.md +5 -0
  132. package/skills/signup/SKILL.md +7 -3
  133. package/skills/site-architecture/CHANGELOG.md +5 -0
  134. package/skills/site-architecture/SKILL.md +5 -4
  135. package/skills/site-architecture/evals/evals.json +25 -0
  136. package/skills/site-architecture/references/platforms-and-migration.md +84 -0
  137. package/skills/social/CHANGELOG.md +5 -0
  138. package/skills/social/SKILL.md +4 -4
  139. package/skills/trends/.skillignore +3 -0
  140. package/skills/trends/CHANGELOG.md +20 -0
  141. package/skills/trends/SKILL.md +12 -8
  142. package/skills/trends/references/agent-mode.md +36 -0
  143. package/skills/trends/references/category-peers.md +38 -0
  144. package/skills/trends/references/comparison.md +227 -0
  145. package/skills/trends/references/competitors.md +29 -0
  146. package/skills/trends/references/discovery.md +90 -0
  147. package/skills/trends/references/engine-contract.md +111 -1889
  148. package/skills/trends/references/followups.md +112 -0
  149. package/skills/trends/references/grok-bot-x.md +55 -0
  150. package/skills/trends/references/hiring-signals.md +19 -0
  151. package/skills/trends/references/library-queue.md +33 -0
  152. package/skills/trends/references/recommendations.md +86 -0
  153. package/skills/trends/references/research-runbook.md +781 -0
  154. package/skills/trends/references/runtime.md +165 -0
  155. package/skills/trends/references/save-html-brief.md +39 -20
  156. package/skills/trends/references/setup-wizard.md +253 -0
  157. package/skills/trends/references/synthesis.md +289 -0
  158. package/skills/trends/scripts/box_chrome_login.py +209 -0
  159. package/skills/trends/scripts/briefing.py +9 -17
  160. package/skills/trends/scripts/last30days.py +3247 -280
  161. package/skills/trends/scripts/lib/agentcookie.py +196 -0
  162. package/skills/trends/scripts/lib/amazon.py +877 -0
  163. package/skills/trends/scripts/lib/arxiv.py +42 -8
  164. package/skills/trends/scripts/lib/backends.py +769 -0
  165. package/skills/trends/scripts/lib/bird_x.py +340 -57
  166. package/skills/trends/scripts/lib/bluesky.py +84 -10
  167. package/skills/trends/scripts/lib/bounded_get.py +156 -0
  168. package/skills/trends/scripts/lib/brightdata.py +291 -0
  169. package/skills/trends/scripts/lib/categories.py +7 -1
  170. package/skills/trends/scripts/lib/chrome_cdp.py +455 -0
  171. package/skills/trends/scripts/lib/chrome_cookies.py +132 -39
  172. package/skills/trends/scripts/lib/cluster.py +31 -54
  173. package/skills/trends/scripts/lib/competitors.py +10 -1
  174. package/skills/trends/scripts/lib/cookie_extract.py +221 -44
  175. package/skills/trends/scripts/lib/cookie_paths.py +27 -0
  176. package/skills/trends/scripts/lib/corpus.py +439 -0
  177. package/skills/trends/scripts/lib/devto.py +313 -0
  178. package/skills/trends/scripts/lib/discovery_handoff.py +988 -0
  179. package/skills/trends/scripts/lib/doctor.py +2166 -0
  180. package/skills/trends/scripts/lib/dripstack.py +203 -0
  181. package/skills/trends/scripts/lib/entity_extract.py +37 -0
  182. package/skills/trends/scripts/lib/env.py +1092 -138
  183. package/skills/trends/scripts/lib/fanout.py +11 -1
  184. package/skills/trends/scripts/lib/feed.py +61 -0
  185. package/skills/trends/scripts/lib/fetch_memo.py +135 -0
  186. package/skills/trends/scripts/lib/freshness.py +566 -0
  187. package/skills/trends/scripts/lib/fusion.py +314 -28
  188. package/skills/trends/scripts/lib/github.py +438 -24
  189. package/skills/trends/scripts/lib/grok_x.py +1237 -0
  190. package/skills/trends/scripts/lib/grounding.py +129 -11
  191. package/skills/trends/scripts/lib/hackernews.py +61 -4
  192. package/skills/trends/scripts/lib/health.py +373 -0
  193. package/skills/trends/scripts/lib/hosted.py +333 -0
  194. package/skills/trends/scripts/lib/html_publish.py +34 -0
  195. package/skills/trends/scripts/lib/html_render.py +169 -3
  196. package/skills/trends/scripts/lib/http.py +1215 -70
  197. package/skills/trends/scripts/lib/instagram.py +132 -9
  198. package/skills/trends/scripts/lib/jobs.py +4 -2
  199. package/skills/trends/scripts/lib/library.py +308 -0
  200. package/skills/trends/scripts/lib/library_index.py +477 -0
  201. package/skills/trends/scripts/lib/linkedin.py +271 -24
  202. package/skills/trends/scripts/lib/log.py +5 -2
  203. package/skills/trends/scripts/lib/meta_ads.py +982 -0
  204. package/skills/trends/scripts/lib/normalize.py +382 -28
  205. package/skills/trends/scripts/lib/parallel_mcp.py +234 -0
  206. package/skills/trends/scripts/lib/permission_preflight.py +42 -3
  207. package/skills/trends/scripts/lib/perplexity.py +1080 -468
  208. package/skills/trends/scripts/lib/pipeline.py +5079 -211
  209. package/skills/trends/scripts/lib/planner.py +396 -53
  210. package/skills/trends/scripts/lib/polymarket.py +285 -23
  211. package/skills/trends/scripts/lib/prescriptions.py +432 -0
  212. package/skills/trends/scripts/lib/private_files.py +21 -0
  213. package/skills/trends/scripts/lib/providers.py +231 -10
  214. package/skills/trends/scripts/lib/quality_nudge.py +122 -37
  215. package/skills/trends/scripts/lib/reddit.py +177 -12
  216. package/skills/trends/scripts/lib/reddit_arctic.py +146 -5
  217. package/skills/trends/scripts/lib/reddit_keyless.py +121 -60
  218. package/skills/trends/scripts/lib/reddit_listing.py +215 -14
  219. package/skills/trends/scripts/lib/reddit_public.py +12 -13
  220. package/skills/trends/scripts/lib/reddit_search.py +304 -0
  221. package/skills/trends/scripts/lib/reddit_shreddit.py +51 -7
  222. package/skills/trends/scripts/lib/registers.py +134 -0
  223. package/skills/trends/scripts/lib/relevance.py +17 -0
  224. package/skills/trends/scripts/lib/render.py +2161 -271
  225. package/skills/trends/scripts/lib/rerank.py +623 -55
  226. package/skills/trends/scripts/lib/resolve.py +50 -1
  227. package/skills/trends/scripts/lib/safari_cookies.py +42 -15
  228. package/skills/trends/scripts/lib/schema.py +782 -1
  229. package/skills/trends/scripts/lib/setup_wizard.py +779 -120
  230. package/skills/trends/scripts/lib/signals.py +206 -7
  231. package/skills/trends/scripts/lib/stocktwits.py +374 -0
  232. package/skills/trends/scripts/lib/subproc.py +307 -53
  233. package/skills/trends/scripts/lib/techmeme.py +89 -64
  234. package/skills/trends/scripts/lib/telegram.py +317 -0
  235. package/skills/trends/scripts/lib/tiktok.py +13 -9
  236. package/skills/trends/scripts/lib/topic_shape.py +340 -0
  237. package/skills/trends/scripts/lib/transcribe.py +12 -3
  238. package/skills/trends/scripts/lib/trustpilot.py +256 -5
  239. package/skills/trends/scripts/lib/truthsocial.py +9 -1
  240. package/skills/trends/scripts/lib/ui.py +6 -0
  241. package/skills/trends/scripts/lib/usage.py +132 -0
  242. package/skills/trends/scripts/lib/web_search_keyless.py +63 -4
  243. package/skills/trends/scripts/lib/windows_job.py +138 -0
  244. package/skills/trends/scripts/lib/x_api.py +824 -0
  245. package/skills/trends/scripts/lib/x_envelope.py +733 -0
  246. package/skills/trends/scripts/lib/x_judge.py +292 -0
  247. package/skills/trends/scripts/lib/xai_x.py +26 -3
  248. package/skills/trends/scripts/lib/xquik.py +89 -15
  249. package/skills/trends/scripts/lib/xurl_x.py +284 -91
  250. package/skills/trends/scripts/lib/youtube_yt.py +570 -119
  251. package/skills/trends/scripts/setup-keychain.sh +12 -4
  252. package/skills/trends/scripts/setup-pass.sh +3 -0
  253. package/skills/trends/scripts/store.py +444 -50
  254. package/skills/trends/scripts/watchlist.py +38 -11
  255. package/skills/trends/scripts/x_lane_parity.py +107 -0
  256. package/skills/video-studio/CHANGELOG.md +8 -0
  257. package/skills/video-studio/SKILL.md +6 -5
  258. package/skills/video-studio/references/styles/product-flight.md +45 -0
  259. package/skills/video-studio/references/styles.md +3 -1
  260. package/skills/video-studio/scripts/qa.mjs +64 -2
  261. package/skills/video-studio/scripts/qa.test.mjs +29 -1
  262. package/skills/video-studio-motion/ATTRIBUTION.md +1 -1
  263. package/skills/video-studio-motion/CHANGELOG.md +4 -0
  264. package/skills/video-studio-motion/SKILL.md +8 -5
  265. package/skills/coder-brainstorming/spec-document-reviewer-prompt.md +0 -49
  266. package/skills/trends/scripts/lib/reddit_rss.py +0 -224
@@ -1,224 +0,0 @@
1
- """Keyless Reddit discovery via public RSS/Atom feeds.
2
-
3
- Reddit's ``.json`` search endpoints now return HTTP 403 (shreddit anti-bot).
4
- RSS feeds still serve HTTP 200 with no API key, so this module uses them for
5
- post discovery, replacing ``reddit_public.search`` as the free search path.
6
-
7
- Two feed families are combined and deduped:
8
- - search: /search.rss?q=... and /r/{sub}/search.rss?q=...&restrict_sr=on
9
- - listing: /r/{sub}/{top,hot}.rss?t=month
10
-
11
- RSS entries carry no engagement score, so ``score``/``num_comments`` start at 0
12
- and are backfilled during shreddit enrichment (see reddit_shreddit.py). Output
13
- dicts match the normalized shape emitted by ``reddit_public._parse_posts`` so
14
- downstream code (pipeline, renderer) is unaffected.
15
- """
16
-
17
- import sys
18
- import xml.etree.ElementTree as ET
19
- from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeoutError
20
- from datetime import datetime, timezone
21
- from typing import Any, Dict, List, Optional
22
- from urllib.parse import quote_plus
23
-
24
- from . import http
25
- from .relevance import token_overlap_relevance
26
-
27
- ATOM = "{http://www.w3.org/2005/Atom}"
28
-
29
- # Mirror reddit_public depth-aware limits so the two free paths behave alike.
30
- DEPTH_LIMITS = {
31
- "quick": 10,
32
- "default": 25,
33
- "deep": 50,
34
- }
35
-
36
- # Listing sorts pulled per subreddit (in addition to search), for volume.
37
- LISTING_SORTS = {
38
- "quick": ["top"],
39
- "default": ["top", "hot"],
40
- "deep": ["top", "hot", "new"],
41
- }
42
-
43
- MAX_WORKERS = 4
44
- FEED_TIMEOUT = 15
45
-
46
-
47
- def _log(msg: str) -> None:
48
- sys.stderr.write(f"[RedditRSS] {msg}\n")
49
- sys.stderr.flush()
50
-
51
-
52
- def _iso_to_date(value: Optional[str]) -> Optional[str]:
53
- """Parse an ISO-8601 timestamp (e.g. 2026-05-20T18:48:31+00:00) to YYYY-MM-DD."""
54
- if not value:
55
- return None
56
- try:
57
- dt = datetime.fromisoformat(value.strip())
58
- return dt.date().isoformat()
59
- except (ValueError, TypeError):
60
- return None
61
-
62
-
63
- def _iso_to_epoch(value: Optional[str]) -> Optional[float]:
64
- if not value:
65
- return None
66
- try:
67
- dt = datetime.fromisoformat(value.strip())
68
- if dt.tzinfo is None:
69
- dt = dt.replace(tzinfo=timezone.utc)
70
- return dt.timestamp()
71
- except (ValueError, TypeError):
72
- return None
73
-
74
-
75
- def _subreddit_from(category: str, url: str) -> str:
76
- """Derive subreddit name from the entry category or, failing that, the URL."""
77
- if category:
78
- return category
79
- # URL form: https://www.reddit.com/r/{sub}/comments/{id}/...
80
- parts = url.split("/r/", 1)
81
- if len(parts) == 2:
82
- return parts[1].split("/", 1)[0]
83
- return ""
84
-
85
-
86
- def _parse_feed(xml_text: str, query: str = "") -> List[Dict[str, Any]]:
87
- """Parse an Atom feed string into normalized post dicts. Never raises."""
88
- if not xml_text:
89
- return []
90
- try:
91
- root = ET.fromstring(xml_text)
92
- except ET.ParseError as e:
93
- _log(f"feed parse error: {e}")
94
- return []
95
-
96
- posts: List[Dict[str, Any]] = []
97
- for entry in root.iter(f"{ATOM}entry"):
98
- link_el = entry.find(f"{ATOM}link")
99
- url = link_el.get("href", "").strip() if link_el is not None else ""
100
- if not url or "/comments/" not in url:
101
- continue
102
-
103
- title_el = entry.find(f"{ATOM}title")
104
- title = (title_el.text or "").strip() if title_el is not None else ""
105
-
106
- author = ""
107
- author_el = entry.find(f"{ATOM}author/{ATOM}name")
108
- if author_el is not None and author_el.text:
109
- author = author_el.text.strip().removeprefix("/u/").removeprefix("u/")
110
- if author in ("[deleted]", "[removed]", ""):
111
- author = "[deleted]"
112
-
113
- cat_el = entry.find(f"{ATOM}category")
114
- category = cat_el.get("term", "").strip() if cat_el is not None else ""
115
- subreddit = _subreddit_from(category, url)
116
-
117
- updated_el = entry.find(f"{ATOM}updated")
118
- updated = (updated_el.text or "").strip() if updated_el is not None else ""
119
-
120
- content_el = entry.find(f"{ATOM}content")
121
- selftext = ""
122
- if content_el is not None and content_el.text:
123
- # Strip the simplest HTML; renderer only needs an excerpt.
124
- import re as _re
125
- selftext = _re.sub(r"<[^>]+>", " ", content_el.text)
126
- selftext = _re.sub(r"\s+", " ", selftext).strip()[:500]
127
-
128
- relevance = round(token_overlap_relevance(query, title), 3) if query else 0.0
129
-
130
- posts.append({
131
- "id": "", # assigned after dedup
132
- "title": title,
133
- "url": url,
134
- "score": 0, # backfilled by shreddit enrichment
135
- "num_comments": 0, # backfilled by shreddit enrichment
136
- "subreddit": subreddit,
137
- "created_utc": _iso_to_epoch(updated),
138
- "author": author,
139
- "selftext": selftext,
140
- "date": _iso_to_date(updated),
141
- "engagement": {
142
- "score": 0,
143
- "num_comments": 0,
144
- "upvote_ratio": None,
145
- },
146
- "relevance": relevance,
147
- "why_relevant": "Reddit RSS",
148
- "metadata": {},
149
- })
150
-
151
- return posts
152
-
153
-
154
- def _build_urls(query: str, depth: str, subreddits: Optional[List[str]]) -> List[str]:
155
- """Build the keyless RSS feed URLs to fan out across."""
156
- q = quote_plus(query)
157
- urls: List[str] = [
158
- f"https://www.reddit.com/search.rss?q={q}&sort=relevance&t=month"
159
- ]
160
- for raw_sub in (subreddits or []):
161
- sub = raw_sub.removeprefix("r/").strip()
162
- if not sub:
163
- continue
164
- urls.append(
165
- f"https://www.reddit.com/r/{sub}/search.rss"
166
- f"?q={q}&restrict_sr=on&sort=relevance&t=month"
167
- )
168
- for sort in LISTING_SORTS.get(depth, LISTING_SORTS["default"]):
169
- urls.append(f"https://www.reddit.com/r/{sub}/{sort}.rss?t=month")
170
- return urls
171
-
172
-
173
- def _fetch_feed(url: str, query: str) -> List[Dict[str, Any]]:
174
- """Fetch and parse one feed. Never raises."""
175
- try:
176
- text = http.reddit_keyless_get_text(url, timeout=FEED_TIMEOUT, accept="application/atom+xml")
177
- return _parse_feed(text, query) if text else []
178
- except Exception as e: # defensive: a single bad feed must not sink the run
179
- _log(f"feed fetch failed for {url}: {e}")
180
- return []
181
-
182
-
183
- def search_rss(
184
- query: str,
185
- depth: str = "default",
186
- subreddits: Optional[List[str]] = None,
187
- ) -> List[Dict[str, Any]]:
188
- """Discover Reddit posts for a query via keyless RSS feeds.
189
-
190
- Args:
191
- query: Search query string
192
- depth: 'quick', 'default', or 'deep' — controls result limit and feeds
193
- subreddits: Optional pre-resolved subreddit names (without r/) to target
194
-
195
- Returns:
196
- List of normalized post dicts (deduped by URL, capped by depth),
197
- with placeholder scores to be backfilled during enrichment.
198
- Empty list on any failure.
199
- """
200
- limit = DEPTH_LIMITS.get(depth, DEPTH_LIMITS["default"])
201
- urls = _build_urls(query, depth, subreddits)
202
-
203
- all_posts: List[Dict[str, Any]] = []
204
- workers = min(MAX_WORKERS, len(urls)) or 1
205
- with ThreadPoolExecutor(max_workers=workers) as executor:
206
- futures = {executor.submit(_fetch_feed, url, query): url for url in urls}
207
- for future in futures:
208
- try:
209
- all_posts.extend(future.result(timeout=FEED_TIMEOUT + 5))
210
- except (Exception, FuturesTimeoutError) as e:
211
- _log(f"feed future failed: {e}")
212
-
213
- # Dedupe by URL (first occurrence wins).
214
- seen: set = set()
215
- unique: List[Dict[str, Any]] = []
216
- for post in all_posts:
217
- if post["url"] not in seen:
218
- seen.add(post["url"])
219
- unique.append(post)
220
-
221
- for i, post in enumerate(unique):
222
- post["id"] = f"R{i + 1}"
223
-
224
- return unique[:limit]