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,234 @@
1
+ """Opt-in Parallel Search MCP adapter using stdlib Streamable HTTP."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import urllib.error
7
+ import urllib.request
8
+ from typing import Any
9
+ from urllib.parse import urlparse
10
+
11
+ from . import dates, http, usage
12
+
13
+ PARALLEL_MCP_URL = "https://search.parallel.ai/mcp"
14
+ _PROTOCOL_VERSION = "2025-03-26"
15
+ _MAX_RESPONSE_BYTES = 4 * 1024 * 1024
16
+
17
+
18
+ class _NoRedirect(urllib.request.HTTPRedirectHandler):
19
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
20
+ # The endpoint is fixed. Never forward credentials, session IDs, or
21
+ # search data to a redirect target, including an HTTPS downgrade.
22
+ return None
23
+
24
+
25
+ def _matching_response(payload: bytes, request_id: int | None) -> dict[str, Any] | None:
26
+ value = json.loads(payload)
27
+ # The negotiated 2025-03-26 protocol permits batched SSE messages.
28
+ for message in value if isinstance(value, list) else [value]:
29
+ if not isinstance(message, dict) or message.get("jsonrpc") != "2.0":
30
+ raise RuntimeError("Parallel MCP returned an invalid JSON-RPC response")
31
+ if message.get("id") == request_id and ("result" in message or "error" in message):
32
+ return message
33
+ return None
34
+
35
+
36
+ def _read_response(response: Any, request_id: int | None) -> dict[str, Any]:
37
+ if "text/event-stream" not in response.headers.get("Content-Type", "").lower():
38
+ payload = response.read(_MAX_RESPONSE_BYTES + 1)
39
+ if len(payload) > _MAX_RESPONSE_BYTES:
40
+ raise RuntimeError("Parallel MCP response exceeded 4 MiB")
41
+ if request_id is None and not payload:
42
+ return {}
43
+ result = _matching_response(payload, request_id) if payload else None
44
+ if result is not None:
45
+ return result
46
+ else:
47
+ data = []
48
+ total = 0
49
+ while True:
50
+ line = response.readline(_MAX_RESPONSE_BYTES - total + 1)
51
+ total += len(line)
52
+ if total > _MAX_RESPONSE_BYTES:
53
+ raise RuntimeError("Parallel MCP response exceeded 4 MiB")
54
+ if not line:
55
+ break
56
+ line = line.rstrip(b"\r\n")
57
+ if not line and data:
58
+ result = _matching_response(b"\n".join(data), request_id)
59
+ if result is not None:
60
+ # A valid response completes the request, even if the
61
+ # server keeps the stream open or sends more notifications.
62
+ return result
63
+ data = []
64
+ elif line.startswith(b"data:"):
65
+ value = line[5:]
66
+ data.append(value[1:] if value.startswith(b" ") else value)
67
+ raise RuntimeError("Parallel MCP response is missing the requested JSON-RPC result")
68
+
69
+
70
+ def _request(
71
+ message: dict[str, Any] | None, api_key: str | None, session_id: str | None = None,
72
+ ) -> tuple[dict[str, Any], str | None]:
73
+ headers = {
74
+ "Accept": "application/json, text/event-stream",
75
+ "Content-Type": "application/json",
76
+ "User-Agent": http.USER_AGENT,
77
+ }
78
+ if api_key:
79
+ headers["Authorization"] = f"Bearer {api_key}"
80
+ if session_id:
81
+ headers["Mcp-Session-Id"] = session_id
82
+ if message is None or message.get("method") != "initialize":
83
+ headers["MCP-Protocol-Version"] = _PROTOCOL_VERSION
84
+ request = urllib.request.Request(
85
+ PARALLEL_MCP_URL,
86
+ data=json.dumps(message, separators=(",", ":")).encode("utf-8") if message is not None else None,
87
+ headers=headers,
88
+ method="POST" if message is not None else "DELETE",
89
+ )
90
+ if api_key and message is not None and message.get("method") == "tools/call":
91
+ usage.begin("parallel")
92
+ with urllib.request.build_opener(_NoRedirect()).open(request, timeout=http.DEFAULT_TIMEOUT) as response:
93
+ result = _read_response(response, message.get("id")) if message is not None else {}
94
+ return result, response.headers.get("Mcp-Session-Id") or session_id
95
+
96
+
97
+ def _result(response: dict[str, Any]) -> dict[str, Any]:
98
+ if response.get("error"):
99
+ error = response["error"]
100
+ detail = error.get("message") if isinstance(error, dict) else str(error)
101
+ raise RuntimeError(f"Parallel MCP error: {detail}")
102
+ result = response.get("result")
103
+ if not isinstance(result, dict):
104
+ raise RuntimeError("Parallel MCP response is missing its result")
105
+ return result
106
+
107
+
108
+ def _search_rows(tool_result: dict[str, Any]) -> list[dict[str, Any]]:
109
+ payloads = [tool_result.get("structuredContent")]
110
+ for content in tool_result.get("content") or []:
111
+ if not isinstance(content, dict) or content.get("type") != "text":
112
+ continue
113
+ try:
114
+ payloads.append(json.loads(content.get("text") or ""))
115
+ except (TypeError, ValueError):
116
+ continue
117
+ for candidates in payloads:
118
+ if isinstance(candidates, dict) and "data" in candidates and "results" not in candidates:
119
+ candidates = candidates["data"]
120
+ if isinstance(candidates, dict):
121
+ candidates = candidates.get("results")
122
+ if isinstance(candidates, list):
123
+ return [row for row in candidates if isinstance(row, dict)]
124
+ raise RuntimeError("Parallel MCP web_search returned no results array")
125
+
126
+
127
+ def search(
128
+ query: str, date_range: tuple[str, str], api_key: str | None = None, count: int = 5,
129
+ ) -> tuple[list[dict[str, Any]], dict[str, Any]]:
130
+ """Discover and invoke hosted ``web_search`` after explicit dispatcher opt-in."""
131
+ initialized, session_id = _request(
132
+ {
133
+ "jsonrpc": "2.0",
134
+ "id": 1,
135
+ "method": "initialize",
136
+ "params": {
137
+ "protocolVersion": _PROTOCOL_VERSION,
138
+ "capabilities": {},
139
+ "clientInfo": {"name": "last30days-skill", "version": "3"},
140
+ },
141
+ },
142
+ api_key,
143
+ )
144
+ try:
145
+ if _result(initialized).get("protocolVersion") != _PROTOCOL_VERSION:
146
+ raise RuntimeError("Parallel MCP negotiated an unsupported protocol version")
147
+ _request(
148
+ {"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}},
149
+ api_key,
150
+ session_id,
151
+ )
152
+ request_id = 2
153
+ params = {}
154
+ seen_cursors = set()
155
+ while True:
156
+ tools_response, _ = _request(
157
+ {"jsonrpc": "2.0", "id": request_id, "method": "tools/list", "params": params},
158
+ api_key,
159
+ session_id,
160
+ )
161
+ request_id += 1
162
+ page = _result(tools_response)
163
+ tools = page.get("tools") or []
164
+ if any(isinstance(tool, dict) and tool.get("name") == "web_search" for tool in tools):
165
+ break
166
+ cursor = page.get("nextCursor")
167
+ if not isinstance(cursor, str) or not cursor or cursor in seen_cursors or len(seen_cursors) >= 20:
168
+ raise RuntimeError("Parallel MCP did not advertise web_search")
169
+ seen_cursors.add(cursor)
170
+ params = {"cursor": cursor}
171
+ objective = f"Find useful public web evidence about {query} from {date_range[0]} through {date_range[1]}."
172
+ called, _ = _request(
173
+ {
174
+ "jsonrpc": "2.0",
175
+ "id": request_id,
176
+ "method": "tools/call",
177
+ "params": {
178
+ "name": "web_search",
179
+ "arguments": {"objective": objective, "search_queries": [query]},
180
+ },
181
+ },
182
+ api_key,
183
+ session_id,
184
+ )
185
+ finally:
186
+ if session_id:
187
+ try:
188
+ _request(None, api_key, session_id)
189
+ except urllib.error.HTTPError as error:
190
+ error.close()
191
+ except (OSError, RuntimeError, ValueError):
192
+ # Cleanup is best-effort: unsupported DELETEs or a network
193
+ # failure must not hide search results or the original error.
194
+ pass
195
+ tool_result = _result(called)
196
+ if tool_result.get("isError"):
197
+ raise RuntimeError("Parallel MCP web_search reported an error")
198
+ items = []
199
+ for row in _search_rows(tool_result):
200
+ if len(items) >= count:
201
+ break
202
+ url = str(row.get("url") or "")
203
+ try:
204
+ parsed_url = urlparse(url)
205
+ except ValueError:
206
+ continue
207
+ if parsed_url.scheme not in ("http", "https") or not parsed_url.netloc:
208
+ continue
209
+ raw_date = row.get("publish_date")
210
+ parsed_date = dates.parse_date(raw_date[:10]) if isinstance(raw_date, str) else None
211
+ pub_date = parsed_date.date().isoformat() if parsed_date else None
212
+ # Match the paid grounding backends: only dated evidence inside the
213
+ # requested window qualifies, including when --as-of is historical.
214
+ if not pub_date or not date_range[0] <= pub_date <= date_range[1]:
215
+ continue
216
+ excerpts = row.get("excerpts") or []
217
+ if isinstance(excerpts, str):
218
+ excerpts = [excerpts]
219
+ snippet = "\n".join(str(value) for value in excerpts if value) if isinstance(excerpts, list) else ""
220
+ items.append({
221
+ "id": f"WPM{len(items) + 1}",
222
+ "title": row.get("title") or parsed_url.netloc,
223
+ "url": url,
224
+ "source_domain": parsed_url.netloc.strip().lower(),
225
+ "snippet": snippet[:500],
226
+ "date": pub_date,
227
+ "relevance": 0.8,
228
+ "why_relevant": "Parallel Search MCP result",
229
+ })
230
+ return items, {
231
+ "label": "parallel-mcp",
232
+ "webSearchQueries": [query],
233
+ "resultCount": len(items),
234
+ }
@@ -4,12 +4,15 @@ from __future__ import annotations
4
4
 
5
5
  from typing import Any
6
6
 
7
+ from . import env, providers as provider_endpoints
8
+
7
9
 
8
10
  ENDPOINT_OVERRIDE_KEYS = {
9
11
  "BSKY_SEARCH_HOST",
10
12
  "LAST30DAYS_SEARXNG_URL",
11
13
  "LAST30DAYS_YOUTUBE_SSH_HOST",
12
14
  "OPENAI_BASE_URL",
15
+ "OPENROUTER_BASE_URL",
13
16
  "XAI_BASE_URL",
14
17
  "XIAOHONGSHU_API_BASE",
15
18
  }
@@ -22,6 +25,10 @@ PROVIDER_CREDENTIALS = {
22
25
  "perplexity": "Perplexity API key",
23
26
  "scrapecreators": "ScrapeCreators API key",
24
27
  "github": "GitHub token or gh auth",
28
+ # X API v2 app-only bearer (X_BEARER_TOKEN). Presence is computed from
29
+ # config inside build(), never through diagnose.providers, whose key set
30
+ # is frozen by tests/test_diagnose_compat.py.
31
+ "x_bearer": "X API bearer token",
25
32
  }
26
33
 
27
34
 
@@ -100,12 +107,28 @@ def build(
100
107
  "label": PROVIDER_CREDENTIALS["scrapecreators"],
101
108
  },
102
109
  "github": {"present": bool(diagnose.get("has_github")), "label": PROVIDER_CREDENTIALS["github"]},
110
+ "x_bearer": {
111
+ "present": bool(str(config.get("X_BEARER_TOKEN") or "").strip()),
112
+ "label": PROVIDER_CREDENTIALS["x_bearer"],
113
+ },
103
114
  }
104
115
 
105
- active_endpoint_overrides = sorted(
106
- key for key in ENDPOINT_OVERRIDE_KEYS if config.get(key)
116
+ active_endpoint_overrides: list[str] = []
117
+ rejected_provider_overrides: list[str] = []
118
+ for key in sorted(ENDPOINT_OVERRIDE_KEYS):
119
+ value = str(config.get(key) or "").strip()
120
+ if not value:
121
+ continue
122
+ if (
123
+ key in provider_endpoints.PROVIDER_BASE_URL_KEYS
124
+ and not provider_endpoints.allowed_base_url_override(value)
125
+ ):
126
+ rejected_provider_overrides.append(key)
127
+ else:
128
+ active_endpoint_overrides.append(key)
129
+ ignored_endpoint_overrides = sorted(
130
+ set(diagnose.get("ignored_endpoint_overrides") or []) | set(rejected_provider_overrides)
107
131
  )
108
- ignored_endpoint_overrides = sorted(diagnose.get("ignored_endpoint_overrides") or [])
109
132
  external_commands = {
110
133
  name: {"status": _status(bool(available))}
111
134
  for name, available in sorted((diagnose.get("external_commands") or {}).items())
@@ -114,6 +137,22 @@ def build(
114
137
  action_items: list[str] = []
115
138
  if ignored_project_config:
116
139
  action_items.append("Project config was ignored; set LAST30DAYS_TRUST_PROJECT_CONFIG=1 to trust it.")
140
+ # get_config() already emptied these, so the provider flags above read them
141
+ # as absent. Name them anyway: the user's setup is broken in a way the
142
+ # presence booleans alone describe as "nothing configured".
143
+ templated_keys = env.templated_config_keys(config)
144
+ if templated_keys:
145
+ action_items.append(
146
+ "Unsubstituted config template(s) count as unset: "
147
+ + _format_names(templated_keys)
148
+ + ". Replace each with a real value or remove it."
149
+ )
150
+ if rejected_provider_overrides:
151
+ action_items.append(
152
+ "Provider endpoint override(s) ignored: "
153
+ + _format_names(rejected_provider_overrides)
154
+ + ". Use HTTPS, or HTTP on loopback."
155
+ )
117
156
 
118
157
  return {
119
158
  "status": "action_needed" if action_items else "ready",