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
@@ -13,9 +13,15 @@ doomed *sources/tools*.
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
+ import os
16
17
  import shutil
17
18
  import subprocess
19
+ import time
18
20
  from dataclasses import dataclass
21
+ from pathlib import Path
22
+ from typing import Dict, Iterable, List, Optional, Tuple
23
+
24
+ from . import subproc
19
25
 
20
26
  # Health states, best to worst.
21
27
  OK = "ok"
@@ -25,6 +31,27 @@ BROKEN = "broken" # present but won't execute (stale shim, bad perms)
25
31
  TIMEOUT = "timeout" # exceeded the probe deadline
26
32
  ERROR = "error" # ran and failed for another reason
27
33
 
34
+ # Per-run outcomes. Doctor does not emit these: it predicts source readiness
35
+ # before retrieval, while Report.source_status records what happened in one run.
36
+ NO_RESULTS = "no-results"
37
+ PARTIAL = "partial"
38
+ RATE_LIMITED = "rate-limited"
39
+ AUTH_FAILED = "auth-failed"
40
+ PAYMENT_REQUIRED = "payment-required" # HTTP 402 / credits exhausted: top up, not re-login
41
+ UNREACHABLE = "unreachable"
42
+ SCHEMA_DRIFT = "schema-drift"
43
+ SKIPPED_UNCONFIGURED = "skipped-unconfigured"
44
+
45
+
46
+ def credits_exhausted_label(source: str) -> str:
47
+ """Human label for a ``PAYMENT_REQUIRED`` outcome on ``source``.
48
+
49
+ The X source names the API whose credits ran out; every other source
50
+ gets the generic phrasing so the same label serves render summaries and
51
+ the doctor post-mortem without each surface hand-writing its own.
52
+ """
53
+ return "X API credits exhausted" if source == "x" else "credits exhausted"
54
+
28
55
 
29
56
  @dataclass
30
57
  class SourceHealth:
@@ -90,3 +117,349 @@ def probe_command(
90
117
  detail = (proc.stderr or proc.stdout or "").strip().splitlines()
91
118
  first = detail[0] if detail else f"exit {proc.returncode}"
92
119
  return SourceHealth(name=name, state=ERROR, reason=f"{name}: {first}")
120
+
121
+
122
+ # ---------------------------------------------------------------------------
123
+ # Dependency probes (doctor command, issue #692).
124
+ #
125
+ # ``probe_dependency`` generalizes ``probe_command`` for the skill's external
126
+ # binaries (yt-dlp, Printing Press CLIs, node for the vendored bird client,
127
+ # ffmpeg). It answers three questions the bare shutil.which gate cannot:
128
+ # - Is the binary genuinely runnable (a stale shim that resolves on PATH but
129
+ # cannot exec is BROKEN, not available)?
130
+ # - If not, WHICH fix applies (install vs reinstall vs a PATH edit), keyed to
131
+ # the package manager that owns the binary on this machine?
132
+ # - Is an on-disk binary merely off the agent-subprocess PATH (the Digg
133
+ # ~/.local/bin case) — MISSING with a PATH-fix, never "installed"?
134
+ #
135
+ # Semantics follow the engine gate: availability means PATH-resolvable in THIS
136
+ # process, not present-on-disk. Probes are one short-timeout version exec each
137
+ # and memoized per process, so doctor and setup can consult them freely.
138
+ # ---------------------------------------------------------------------------
139
+
140
+ # Per-probe budget in seconds: a healthy --version exec is near-instant, so a
141
+ # slow probe is itself a diagnostic (network-mounted shim, hung interpreter).
142
+ PROBE_TIMEOUT = 5.0
143
+
144
+ _PP_CLI_SUFFIX = "-pp-cli"
145
+ # Matches setup_wizard.PRINTING_PRESS_NPM (pinned catalog installer).
146
+ _PRINTING_PRESS_NPM = "@mvanhorn/printing-press-library@0.1.16"
147
+
148
+ # Dependencies the doctor probes by default.
149
+ KNOWN_DEPENDENCIES: Tuple[str, ...] = ("yt-dlp", "digg-pp-cli", "node", "ffmpeg", "grok")
150
+
151
+ # Cheap side-effect-free version invocation per dependency (default --version).
152
+ _VERSION_ARGS: Dict[str, List[str]] = {
153
+ "ffmpeg": ["-version"],
154
+ }
155
+
156
+ # Package managers each dependency may be owned by, in preference order, and
157
+ # the (install, reinstall) prescription for each. "reinstall" wording matters:
158
+ # a BROKEN binary is present, so telling the user to "install" it reads as a
159
+ # no-op ("it's already installed") — the stale-shim trap this module exists
160
+ # to name.
161
+ _MANAGER_PRESCRIPTIONS: Dict[str, Dict[str, Tuple[str, str]]] = {
162
+ "yt-dlp": {
163
+ "brew": ("brew install yt-dlp", "brew reinstall yt-dlp"),
164
+ "pipx": ("pipx install yt-dlp", "pipx reinstall yt-dlp"),
165
+ },
166
+ "node": {
167
+ "brew": ("brew install node", "brew reinstall node"),
168
+ "nvm": ("nvm install --lts", "reinstall node via nvm: nvm install --lts && nvm use --lts"),
169
+ },
170
+ "ffmpeg": {
171
+ "brew": ("brew install ffmpeg", "brew reinstall ffmpeg"),
172
+ "apt": ("sudo apt-get install -y ffmpeg", "sudo apt-get install -y --reinstall ffmpeg"),
173
+ },
174
+ # The official installer is the primary path; npm is a real alternative
175
+ # (the package is published as @xai-official/grok) and fits the existing
176
+ # manager-preference machinery.
177
+ "grok": {
178
+ "npm": (
179
+ "npm install -g @xai-official/grok",
180
+ "reinstall the Grok CLI: npm install -g @xai-official/grok@latest",
181
+ ),
182
+ },
183
+ }
184
+
185
+ # Last-resort prescriptions when no known package manager is detected.
186
+ _FALLBACK_PRESCRIPTIONS: Dict[str, Tuple[str, str]] = {
187
+ "yt-dlp": (
188
+ "install yt-dlp (https://github.com/yt-dlp/yt-dlp#installation) and ensure it is on PATH",
189
+ "reinstall yt-dlp (https://github.com/yt-dlp/yt-dlp#installation); the current binary won't run",
190
+ ),
191
+ "node": (
192
+ "install Node.js 22+ (https://nodejs.org) and ensure `node` is on PATH",
193
+ "reinstall Node.js 22+ (https://nodejs.org); the current binary won't run",
194
+ ),
195
+ "ffmpeg": (
196
+ "install ffmpeg (https://ffmpeg.org/download.html) and ensure it is on PATH",
197
+ "reinstall ffmpeg (https://ffmpeg.org/download.html); the current binary won't run",
198
+ ),
199
+ "grok": (
200
+ "install the Grok CLI: curl -fsSL https://x.ai/cli/install.sh | bash, then run `grok login`",
201
+ "reinstall the Grok CLI: curl -fsSL https://x.ai/cli/install.sh | bash; the current binary won't run",
202
+ ),
203
+ }
204
+
205
+
206
+ @dataclass
207
+ class DependencyProbe:
208
+ """Uniform probe result for one external dependency.
209
+
210
+ ``status`` is one of the module-level constants (OK/MISSING/BROKEN/TIMEOUT).
211
+ ``detail`` says what was observed (version string, exec error, off-PATH
212
+ location). ``prescription`` is the copy-pasteable fix, empty when OK.
213
+ ``owner_pkg_manager`` names the manager the prescription targets
214
+ ("brew", "pipx", "apt", "nvm", "npx"), or "" for PATH fixes / fallbacks.
215
+ """
216
+
217
+ name: str
218
+ status: str
219
+ detail: str = ""
220
+ prescription: str = ""
221
+ owner_pkg_manager: str = ""
222
+ # True for the on-disk-but-off-PATH case: MISSING (the engine gate would
223
+ # not pass) but the fix is a PATH edit, not an install.
224
+ off_path: bool = False
225
+
226
+ @property
227
+ def ok(self) -> bool:
228
+ return self.status == OK
229
+
230
+
231
+ # Safe under the GIL (dict get/set are atomic) and each dependency name is
232
+ # probed from a single builder today; worst case is one redundant probe.
233
+ _dependency_probe_cache: Dict[str, DependencyProbe] = {}
234
+
235
+
236
+ def clear_dependency_probe_cache() -> None:
237
+ """Reset memoized probes (tests, or a doctor re-run after a fix)."""
238
+ _dependency_probe_cache.clear()
239
+
240
+
241
+ def _nvm_present() -> bool:
242
+ return bool(os.environ.get("NVM_DIR")) or (Path.home() / ".nvm").is_dir()
243
+
244
+
245
+ def _manager_available(manager: str) -> bool:
246
+ if manager == "nvm":
247
+ return _nvm_present()
248
+ if manager == "apt":
249
+ return shutil.which("apt-get") is not None
250
+ return shutil.which(manager) is not None
251
+
252
+
253
+ def _is_pp_cli(name: str) -> bool:
254
+ return name.endswith(_PP_CLI_SUFFIX) and len(name) > len(_PP_CLI_SUFFIX)
255
+
256
+
257
+ def _pp_install_cmd(name: str) -> str:
258
+ slug = name[: -len(_PP_CLI_SUFFIX)]
259
+ return f"npx -y {_PRINTING_PRESS_NPM} install {slug} --cli-only"
260
+
261
+
262
+ def pp_install_cmd(slug: str) -> str:
263
+ """Public catalog-install command for the Printing Press CLI ``<slug>-pp-cli``."""
264
+ return _pp_install_cmd(f"{slug}{_PP_CLI_SUFFIX}")
265
+
266
+
267
+ def static_prescription(name: str, manager: str) -> Tuple[str, str]:
268
+ """Public ``(install, reinstall)`` strings for one dependency/manager pair.
269
+
270
+ Reads the static table without probing manager availability; raises
271
+ KeyError for unknown pairs so consumers fail loudly at import time.
272
+ """
273
+ return _MANAGER_PRESCRIPTIONS[name][manager]
274
+
275
+
276
+ def _prescription(name: str, kind: str) -> Tuple[str, str]:
277
+ """Return ``(prescription, owner_pkg_manager)`` for install/reinstall.
278
+
279
+ ``kind`` is "install" (MISSING) or "reinstall" (BROKEN). Printing Press
280
+ CLIs always re-run the catalog installer; other deps pick the first
281
+ detected manager from their preference table, falling back to a generic
282
+ but still actionable instruction.
283
+ """
284
+ idx = 0 if kind == "install" else 1
285
+ if _is_pp_cli(name):
286
+ cmd = _pp_install_cmd(name)
287
+ if kind == "reinstall":
288
+ return f"re-run the Printing Press install: {cmd}", "npx"
289
+ return cmd, "npx"
290
+ for manager, prescriptions in _MANAGER_PRESCRIPTIONS.get(name, {}).items():
291
+ if _manager_available(manager):
292
+ return prescriptions[idx], manager
293
+ fallback = _FALLBACK_PRESCRIPTIONS.get(name)
294
+ if fallback:
295
+ return fallback[idx], ""
296
+ verb = "install" if kind == "install" else "reinstall"
297
+ return f"{verb} {name} and ensure it is on PATH", ""
298
+
299
+
300
+ def windows_printing_press_bin_dir() -> Optional[Path]:
301
+ """Windows managed install dir for Printing Press CLIs, when applicable.
302
+
303
+ Returns ``%LOCALAPPDATA%/Programs/PrintingPress/bin`` on Windows when
304
+ LOCALAPPDATA is set; ``None`` otherwise.
305
+ """
306
+ if os.name != "nt":
307
+ return None
308
+ local_app = os.environ.get("LOCALAPPDATA") or os.environ.get("LocalAppData")
309
+ if not local_app:
310
+ return None
311
+ return Path(local_app) / "Programs" / "PrintingPress" / "bin"
312
+
313
+
314
+ def installer_bin_dirs() -> List[Path]:
315
+ """Installer-managed bin dirs shared with setup_wizard's Digg candidates.
316
+
317
+ Single source of truth for where installers drop binaries: the Printing
318
+ Press library default (~/.local/bin), Go bins, and — on Windows — the
319
+ managed %LOCALAPPDATA%/Programs/PrintingPress/bin dir.
320
+ ``setup_wizard._digg_bin_candidate_paths`` derives its Digg-specific
321
+ paths from this list; keep the two in lockstep by editing only here.
322
+ """
323
+ home = Path.home()
324
+ dirs = [home / ".local" / "bin"]
325
+ gopath = os.environ.get("GOPATH")
326
+ if gopath:
327
+ dirs.append(Path(gopath) / "bin")
328
+ dirs.append(home / "go" / "bin")
329
+ win_dir = windows_printing_press_bin_dir()
330
+ if win_dir is not None:
331
+ dirs.append(win_dir)
332
+ return dirs
333
+
334
+
335
+ def _off_path_candidate_dirs() -> List[Path]:
336
+ """Directories where installers drop binaries that PATH may not cover.
337
+
338
+ The shared installer dirs (``installer_bin_dirs``, which also backs
339
+ setup_wizard's Digg candidates) plus the Homebrew prefixes (an agent
340
+ subprocess PATH sometimes omits even those).
341
+ """
342
+ dirs = installer_bin_dirs()
343
+ dirs.extend([Path("/opt/homebrew/bin"), Path("/usr/local/bin")])
344
+ return dirs
345
+
346
+
347
+ def _off_path_binary(name: str) -> Optional[Path]:
348
+ """Return an executable for ``name`` in a known dir that PATH misses."""
349
+ names = [name, f"{name}.exe"] if os.name == "nt" else [name]
350
+ for directory in _off_path_candidate_dirs():
351
+ for candidate_name in names:
352
+ candidate = directory / candidate_name
353
+ if candidate.is_file() and os.access(candidate, os.X_OK):
354
+ return candidate
355
+ return None
356
+
357
+
358
+ def _path_hint(directory: Path) -> str:
359
+ """Render a bin dir with $HOME substituted for copy-pasteable PATH edits."""
360
+ raw = str(directory)
361
+ if os.name == "nt":
362
+ return raw
363
+ home = str(Path.home())
364
+ if raw == home:
365
+ return "$HOME"
366
+ if raw.startswith(home + os.sep):
367
+ return "$HOME/" + raw[len(home) + 1:].replace(os.sep, "/")
368
+ return raw
369
+
370
+
371
+ def probe_dependency(name: str, timeout: float = PROBE_TIMEOUT) -> DependencyProbe:
372
+ """Probe one external dependency: OK | MISSING | BROKEN | TIMEOUT.
373
+
374
+ - MISSING: not resolvable on this process's PATH. If the binary exists in
375
+ a known install dir, the prescription is a PATH edit, not an install —
376
+ installing again would not fix anything.
377
+ - BROKEN: shutil.which resolves it but a cheap version exec fails
378
+ (OSError/exec-format, or any non-zero exit). Prescription says
379
+ *reinstall* — the #692 stale-shim class must never read as available.
380
+ - TIMEOUT: the version exec exceeded the per-probe budget.
381
+ - OK: version exec exited 0; ``detail`` carries the version line.
382
+
383
+ Memoized per process; ``clear_dependency_probe_cache()`` resets.
384
+ """
385
+ cached = _dependency_probe_cache.get(name)
386
+ if cached is not None:
387
+ return cached
388
+ probe = _probe_dependency_uncached(name, timeout)
389
+ _dependency_probe_cache[name] = probe
390
+ return probe
391
+
392
+
393
+ def _probe_dependency_uncached(name: str, timeout: float) -> DependencyProbe:
394
+ deadline = time.monotonic() + timeout
395
+ resolved = shutil.which(name)
396
+ if resolved is None:
397
+ off_path = _off_path_binary(name)
398
+ if off_path is not None:
399
+ hint = _path_hint(off_path.parent)
400
+ return DependencyProbe(
401
+ name=name,
402
+ status=MISSING,
403
+ detail=f"{name} is installed at {off_path} but that directory is not on this process's PATH",
404
+ prescription=f'add {hint} to PATH (e.g. export PATH="{hint}:$PATH") so {name} resolves',
405
+ owner_pkg_manager="",
406
+ off_path=True,
407
+ )
408
+ prescription, manager = _prescription(name, "install")
409
+ return DependencyProbe(
410
+ name=name,
411
+ status=MISSING,
412
+ detail=f"{name} not found on PATH",
413
+ prescription=prescription,
414
+ owner_pkg_manager=manager,
415
+ )
416
+
417
+ command = [name] + _VERSION_ARGS.get(name, ["--version"])
418
+ try:
419
+ cleanup_grace = 0.1
420
+ proc = subproc.run_with_timeout(
421
+ command,
422
+ timeout=max(0, deadline - time.monotonic() - 2 * cleanup_grace),
423
+ deadline_monotonic=deadline - 2 * cleanup_grace,
424
+ cleanup_grace=cleanup_grace,
425
+ capture_limit_bytes=64 * 1024,
426
+ )
427
+ except (FileNotFoundError, OSError) as exc:
428
+ prescription, manager = _prescription(name, "reinstall")
429
+ return DependencyProbe(
430
+ name=name,
431
+ status=BROKEN,
432
+ detail=f"{name} resolves to {resolved} but won't execute: {exc}",
433
+ prescription=prescription,
434
+ owner_pkg_manager=manager,
435
+ )
436
+ except subproc.SubprocTimeout:
437
+ prescription, manager = _prescription(name, "reinstall")
438
+ return DependencyProbe(
439
+ name=name,
440
+ status=TIMEOUT,
441
+ detail=f"{name} version probe timed out after {timeout:g}s",
442
+ prescription=f"re-run doctor; if the timeout persists: {prescription}",
443
+ owner_pkg_manager=manager,
444
+ )
445
+
446
+ if proc.returncode == 0:
447
+ lines = (proc.stdout or proc.stderr or "").strip().splitlines()
448
+ version = lines[0].strip() if lines else ""
449
+ return DependencyProbe(name=name, status=OK, detail=version)
450
+
451
+ lines = (proc.stderr or proc.stdout or "").strip().splitlines()
452
+ why = lines[0].strip() if lines else f"exit {proc.returncode}"
453
+ prescription, manager = _prescription(name, "reinstall")
454
+ return DependencyProbe(
455
+ name=name,
456
+ status=BROKEN,
457
+ detail=f"{name} resolves to {resolved} but the version probe failed: {why}",
458
+ prescription=prescription,
459
+ owner_pkg_manager=manager,
460
+ )
461
+
462
+
463
+ def probe_dependencies(names: Optional[Iterable[str]] = None) -> Dict[str, DependencyProbe]:
464
+ """Probe every known dependency (or ``names``), memoized per process."""
465
+ return {name: probe_dependency(name) for name in (names or KNOWN_DEPENDENCIES)}