dexterm-cli 0.0.0-stage → 0.1.2

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 (377) hide show
  1. package/LICENSE +661 -0
  2. package/dist/packages/agents/src/audit.js +122 -0
  3. package/dist/packages/agents/src/bus.js +394 -0
  4. package/dist/packages/agents/src/executor.js +202 -0
  5. package/dist/packages/agents/src/gate.js +179 -0
  6. package/dist/packages/agents/src/identity.js +218 -0
  7. package/dist/packages/agents/src/index.js +15 -0
  8. package/dist/packages/agents/src/proposals.js +153 -0
  9. package/dist/packages/agents/src/schema.js +87 -0
  10. package/dist/packages/agents/src/sdk.js +140 -0
  11. package/dist/packages/agents/src/tiers.js +119 -0
  12. package/dist/packages/agents/src/tools.js +72 -0
  13. package/dist/packages/agents/src/ws.js +158 -0
  14. package/dist/packages/cli/assets/claude/analyst-system.md +76 -0
  15. package/dist/packages/cli/assets/claude/plugin/.claude-plugin/plugin.json +5 -0
  16. package/dist/packages/cli/assets/claude/plugin/skills/REGISTRY.json +16 -0
  17. package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +75 -0
  18. package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +74 -0
  19. package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +63 -0
  20. package/dist/packages/cli/assets/claude/plugin/skills/trench-scan/SKILL.md +44 -0
  21. package/dist/packages/cli/assets/claude/schemas/larp.schema.json +117 -0
  22. package/dist/packages/cli/assets/claude/schemas/trench-scan.schema.json +71 -0
  23. package/dist/packages/cli/assets/claude/schemas/verdict.schema.json +115 -0
  24. package/dist/packages/cli/bin/dexterm.mjs +11 -0
  25. package/dist/packages/cli/src/agent-cmd.js +336 -0
  26. package/dist/packages/cli/src/agent-roster.js +26 -0
  27. package/dist/packages/cli/src/alerts/adapters/desktop.js +40 -0
  28. package/dist/packages/cli/src/alerts/adapters/discord.js +36 -0
  29. package/dist/packages/cli/src/alerts/adapters/telegram.js +43 -0
  30. package/dist/packages/cli/src/alerts/adapters/toast.js +16 -0
  31. package/dist/packages/cli/src/alerts/creds.js +83 -0
  32. package/dist/packages/cli/src/alerts/format.js +41 -0
  33. package/dist/packages/cli/src/alerts/index.js +10 -0
  34. package/dist/packages/cli/src/alerts/notifier.js +53 -0
  35. package/dist/packages/cli/src/alerts/route.js +19 -0
  36. package/dist/packages/cli/src/alerts/types.js +1 -0
  37. package/dist/packages/cli/src/alerts-cmd.js +212 -0
  38. package/dist/packages/cli/src/analyst/backend.js +526 -0
  39. package/dist/packages/cli/src/analyst/results.js +38 -0
  40. package/dist/packages/cli/src/analyst/session.js +319 -0
  41. package/dist/packages/cli/src/audit.js +51 -0
  42. package/dist/packages/cli/src/backtest-cmd.js +208 -0
  43. package/dist/packages/cli/src/boot-state.js +62 -0
  44. package/dist/packages/cli/src/coin-target.js +2 -0
  45. package/dist/packages/cli/src/custom.js +261 -0
  46. package/dist/packages/cli/src/display.js +191 -0
  47. package/dist/packages/cli/src/doctor.js +375 -0
  48. package/dist/packages/cli/src/export.js +235 -0
  49. package/dist/packages/cli/src/feed.js +634 -0
  50. package/dist/packages/cli/src/fixtures.js +275 -0
  51. package/dist/packages/cli/src/follow-cmd.js +221 -0
  52. package/dist/packages/cli/src/images-config.js +59 -0
  53. package/dist/packages/cli/src/index.js +31 -0
  54. package/dist/packages/cli/src/kb-skill-cmd.js +240 -0
  55. package/dist/packages/cli/src/larp-record.js +21 -0
  56. package/dist/packages/cli/src/live/copy.js +80 -0
  57. package/dist/packages/cli/src/live/flow.js +233 -0
  58. package/dist/packages/cli/src/live/index.js +511 -0
  59. package/dist/packages/cli/src/live/intel.js +227 -0
  60. package/dist/packages/cli/src/live/newpairs.js +334 -0
  61. package/dist/packages/cli/src/live/screens/newpairs-live.js +219 -0
  62. package/dist/packages/cli/src/live/screens/token-live.js +459 -0
  63. package/dist/packages/cli/src/live/screens/watchlist-live.js +149 -0
  64. package/dist/packages/cli/src/live/token.js +585 -0
  65. package/dist/packages/cli/src/live/trade.js +929 -0
  66. package/dist/packages/cli/src/live/watchlist.js +178 -0
  67. package/dist/packages/cli/src/main.js +5785 -0
  68. package/dist/packages/cli/src/mcp-cmd.js +326 -0
  69. package/dist/packages/cli/src/narrative-cmd.js +76 -0
  70. package/dist/packages/cli/src/pack-cmd.js +323 -0
  71. package/dist/packages/cli/src/plan-cmd.js +278 -0
  72. package/dist/packages/cli/src/popup.js +810 -0
  73. package/dist/packages/cli/src/rules-cmd.js +131 -0
  74. package/dist/packages/cli/src/rules.js +358 -0
  75. package/dist/packages/cli/src/scan/command.js +57 -0
  76. package/dist/packages/cli/src/scan/engine.js +419 -0
  77. package/dist/packages/cli/src/scan/observe.js +64 -0
  78. package/dist/packages/cli/src/scan/ui.js +98 -0
  79. package/dist/packages/cli/src/screener/filter.js +241 -0
  80. package/dist/packages/cli/src/screener/presets.js +80 -0
  81. package/dist/packages/cli/src/screener/rows.js +146 -0
  82. package/dist/packages/cli/src/screener/rug.js +50 -0
  83. package/dist/packages/cli/src/screener/sort.js +52 -0
  84. package/dist/packages/cli/src/screener/view.js +183 -0
  85. package/dist/packages/cli/src/screens/analyst-session.js +343 -0
  86. package/dist/packages/cli/src/screens/analyst.js +326 -0
  87. package/dist/packages/cli/src/screens/boot.js +74 -0
  88. package/dist/packages/cli/src/screens/bubbles.js +255 -0
  89. package/dist/packages/cli/src/screens/coin.js +33 -0
  90. package/dist/packages/cli/src/screens/follows.js +201 -0
  91. package/dist/packages/cli/src/screens/kol.js +90 -0
  92. package/dist/packages/cli/src/screens/narrative.js +90 -0
  93. package/dist/packages/cli/src/screens/newpairs.js +150 -0
  94. package/dist/packages/cli/src/screens/overlay.js +257 -0
  95. package/dist/packages/cli/src/screens/plans.js +126 -0
  96. package/dist/packages/cli/src/screens/positions.js +176 -0
  97. package/dist/packages/cli/src/screens/rules.js +170 -0
  98. package/dist/packages/cli/src/screens/screener.js +459 -0
  99. package/dist/packages/cli/src/screens/social-panel.js +107 -0
  100. package/dist/packages/cli/src/screens/ticket.js +342 -0
  101. package/dist/packages/cli/src/screens/token.js +312 -0
  102. package/dist/packages/cli/src/screens/trending.js +181 -0
  103. package/dist/packages/cli/src/screens/watchlist.js +104 -0
  104. package/dist/packages/cli/src/skill-eval-build.js +109 -0
  105. package/dist/packages/cli/src/social/demo.js +96 -0
  106. package/dist/packages/cli/src/social/view.js +250 -0
  107. package/dist/packages/cli/src/state.js +463 -0
  108. package/dist/packages/cli/src/theme-cmd.js +126 -0
  109. package/dist/packages/cli/src/trade.js +319 -0
  110. package/dist/packages/cli/src/update.js +294 -0
  111. package/dist/packages/cli/src/version.js +3 -0
  112. package/dist/packages/cli/src/voice-cmd.js +119 -0
  113. package/dist/packages/cli/src/voice-hud.js +42 -0
  114. package/dist/packages/cli/src/wallet-cmd.js +440 -0
  115. package/dist/packages/cli/src/wallet-command.js +31 -0
  116. package/dist/packages/cli/src/watch-cmd.js +190 -0
  117. package/dist/packages/cli/src/widgets.js +208 -0
  118. package/dist/packages/cli/src/workspace-cmd.js +68 -0
  119. package/dist/packages/cli/src/x-cmd.js +154 -0
  120. package/dist/packages/custom/src/config.js +213 -0
  121. package/dist/packages/custom/src/index.js +10 -0
  122. package/dist/packages/custom/src/keymap.js +107 -0
  123. package/dist/packages/custom/src/layout.js +235 -0
  124. package/dist/packages/custom/src/packs.js +633 -0
  125. package/dist/packages/custom/src/rules.js +704 -0
  126. package/dist/packages/custom/src/schema.js +538 -0
  127. package/dist/packages/custom/src/themes.js +176 -0
  128. package/dist/packages/custom/src/widget-html.js +222 -0
  129. package/dist/packages/custom/src/workspace.js +141 -0
  130. package/dist/packages/data/src/analyst-actions.js +133 -0
  131. package/dist/packages/data/src/budget.js +88 -0
  132. package/dist/packages/data/src/cache.js +142 -0
  133. package/dist/packages/data/src/candles.js +168 -0
  134. package/dist/packages/data/src/clock.js +64 -0
  135. package/dist/packages/data/src/coin-target.js +82 -0
  136. package/dist/packages/data/src/credits.js +137 -0
  137. package/dist/packages/data/src/detail.js +97 -0
  138. package/dist/packages/data/src/fast-lane.js +321 -0
  139. package/dist/packages/data/src/fixtures.js +135 -0
  140. package/dist/packages/data/src/health.js +245 -0
  141. package/dist/packages/data/src/http.js +277 -0
  142. package/dist/packages/data/src/images/cache.js +141 -0
  143. package/dist/packages/data/src/images/decode-worker.js +28 -0
  144. package/dist/packages/data/src/images/decode.js +81 -0
  145. package/dist/packages/data/src/images/fetch.js +162 -0
  146. package/dist/packages/data/src/images/gif.js +214 -0
  147. package/dist/packages/data/src/images/index.js +10 -0
  148. package/dist/packages/data/src/images/jpeg.js +544 -0
  149. package/dist/packages/data/src/images/png.js +310 -0
  150. package/dist/packages/data/src/images/service.js +334 -0
  151. package/dist/packages/data/src/images/types.js +27 -0
  152. package/dist/packages/data/src/index.js +211 -0
  153. package/dist/packages/data/src/intel/helius-history.js +70 -0
  154. package/dist/packages/data/src/intel/holder-class.js +125 -0
  155. package/dist/packages/data/src/intel/rules.js +421 -0
  156. package/dist/packages/data/src/intel/scan.js +170 -0
  157. package/dist/packages/data/src/larp/evidence.js +288 -0
  158. package/dist/packages/data/src/larp/link-baseline.js +196 -0
  159. package/dist/packages/data/src/larp/links.js +137 -0
  160. package/dist/packages/data/src/larp/robots.js +90 -0
  161. package/dist/packages/data/src/larp/safe-fetch.js +389 -0
  162. package/dist/packages/data/src/limiter.js +341 -0
  163. package/dist/packages/data/src/model.js +12 -0
  164. package/dist/packages/data/src/paper.js +331 -0
  165. package/dist/packages/data/src/rpc-config.js +126 -0
  166. package/dist/packages/data/src/rpc-pool.js +166 -0
  167. package/dist/packages/data/src/social/chain.js +83 -0
  168. package/dist/packages/data/src/social/dedupe.js +205 -0
  169. package/dist/packages/data/src/social/feed.js +462 -0
  170. package/dist/packages/data/src/social/firewall.js +110 -0
  171. package/dist/packages/data/src/social/index.js +11 -0
  172. package/dist/packages/data/src/social/kol.js +269 -0
  173. package/dist/packages/data/src/social/model.js +11 -0
  174. package/dist/packages/data/src/social/rank.js +184 -0
  175. package/dist/packages/data/src/social/rss.js +113 -0
  176. package/dist/packages/data/src/social/summary.js +104 -0
  177. package/dist/packages/data/src/social/venues.js +186 -0
  178. package/dist/packages/data/src/sources/dexscreener.js +184 -0
  179. package/dist/packages/data/src/sources/geckoterminal.js +133 -0
  180. package/dist/packages/data/src/sources/goplus.js +161 -0
  181. package/dist/packages/data/src/sources/jupiter-swap.js +236 -0
  182. package/dist/packages/data/src/sources/jupiter.js +198 -0
  183. package/dist/packages/data/src/sources/pda.js +113 -0
  184. package/dist/packages/data/src/sources/pump-curve.js +58 -0
  185. package/dist/packages/data/src/sources/pumpportal.js +252 -0
  186. package/dist/packages/data/src/sources/rugcheck.js +71 -0
  187. package/dist/packages/data/src/sources/solana-rpc.js +299 -0
  188. package/dist/packages/data/src/sources/solana-ws.js +287 -0
  189. package/dist/packages/data/src/store.js +289 -0
  190. package/dist/packages/data/src/trade/bridge.js +464 -0
  191. package/dist/packages/data/src/trade/caps.js +323 -0
  192. package/dist/packages/data/src/trade/copy/auto.js +385 -0
  193. package/dist/packages/data/src/trade/copy/follows.js +103 -0
  194. package/dist/packages/data/src/trade/copy/index.js +9 -0
  195. package/dist/packages/data/src/trade/copy/mirror.js +91 -0
  196. package/dist/packages/data/src/trade/copy/stats.js +120 -0
  197. package/dist/packages/data/src/trade/copy/tracker.js +158 -0
  198. package/dist/packages/data/src/trade/copy/trades.js +87 -0
  199. package/dist/packages/data/src/trade/copy/types.js +6 -0
  200. package/dist/packages/data/src/trade/executor.js +933 -0
  201. package/dist/packages/data/src/trade/kill-switch.js +47 -0
  202. package/dist/packages/data/src/trade/link-store.js +52 -0
  203. package/dist/packages/data/src/trade/plans/confirm.js +157 -0
  204. package/dist/packages/data/src/trade/plans/engine.js +784 -0
  205. package/dist/packages/data/src/trade/plans/executor-live.js +220 -0
  206. package/dist/packages/data/src/trade/plans/executor-paper.js +44 -0
  207. package/dist/packages/data/src/trade/plans/executor.js +11 -0
  208. package/dist/packages/data/src/trade/plans/index.js +12 -0
  209. package/dist/packages/data/src/trade/plans/journal.js +77 -0
  210. package/dist/packages/data/src/trade/plans/mandate.js +129 -0
  211. package/dist/packages/data/src/trade/plans/plan.js +179 -0
  212. package/dist/packages/data/src/trade/plans/service.js +168 -0
  213. package/dist/packages/data/src/trade/plans/types.js +6 -0
  214. package/dist/packages/data/src/trade/rpc-exec.js +229 -0
  215. package/dist/packages/data/src/trade/signer/client.js +197 -0
  216. package/dist/packages/data/src/trade/signer/fast-wallet.js +154 -0
  217. package/dist/packages/data/src/trade/signer/host.js +97 -0
  218. package/dist/packages/data/src/trade/signer/main.js +121 -0
  219. package/dist/packages/data/src/trade/signer/probe-builder.js +85 -0
  220. package/dist/packages/data/src/trade/signer/protocol.js +92 -0
  221. package/dist/packages/data/src/trade/signer/service.js +400 -0
  222. package/dist/packages/data/src/trade/signer/sol-usd-feed.js +98 -0
  223. package/dist/packages/data/src/trade/sol-usd.js +86 -0
  224. package/dist/packages/data/src/trade/swap-build.js +108 -0
  225. package/dist/packages/data/src/trade/token-delta.js +201 -0
  226. package/dist/packages/data/src/trade/tx-decode.js +632 -0
  227. package/dist/packages/data/src/util.js +24 -0
  228. package/dist/packages/data/src/watchlist.js +176 -0
  229. package/dist/packages/data/src/x/api.js +256 -0
  230. package/dist/packages/data/src/x/budget.js +66 -0
  231. package/dist/packages/data/src/x/config.js +49 -0
  232. package/dist/packages/data/src/x/index.js +10 -0
  233. package/dist/packages/data/src/x/mock-server.js +217 -0
  234. package/dist/packages/data/src/x/oauth.js +242 -0
  235. package/dist/packages/data/src/x/pkce.js +21 -0
  236. package/dist/packages/data/src/x/source.js +285 -0
  237. package/dist/packages/data/src/x/token-store.js +134 -0
  238. package/dist/packages/data/src/x/transport.js +71 -0
  239. package/dist/packages/data/vendor/solana-web3/LICENSE +20 -0
  240. package/dist/packages/data/vendor/solana-web3/NOTICE.md +13 -0
  241. package/dist/packages/data/vendor/solana-web3/index.iife.min.js +20 -0
  242. package/dist/packages/mcp/assets/cases.json +1194 -0
  243. package/dist/packages/mcp/assets/kb/entries/cabal-and-coordinated-launches.md +28 -0
  244. package/dist/packages/mcp/assets/kb/entries/common-rug-patterns.md +40 -0
  245. package/dist/packages/mcp/assets/kb/entries/dbc-mechanics.md +30 -0
  246. package/dist/packages/mcp/assets/kb/entries/dev-wallet-behaviour.md +33 -0
  247. package/dist/packages/mcp/assets/kb/entries/glossary.md +35 -0
  248. package/dist/packages/mcp/assets/kb/entries/holder-concentration.md +30 -0
  249. package/dist/packages/mcp/assets/kb/entries/jito-bundles.md +29 -0
  250. package/dist/packages/mcp/assets/kb/entries/launchlab-curve.md +31 -0
  251. package/dist/packages/mcp/assets/kb/entries/lp-burn-vs-lock.md +31 -0
  252. package/dist/packages/mcp/assets/kb/entries/metadata-and-copycats.md +29 -0
  253. package/dist/packages/mcp/assets/kb/entries/mint-freeze-authority.md +31 -0
  254. package/dist/packages/mcp/assets/kb/entries/narrative-lifecycle.md +32 -0
  255. package/dist/packages/mcp/assets/kb/entries/pump-bonding-curve.md +33 -0
  256. package/dist/packages/mcp/assets/kb/entries/pumpswap-migration.md +29 -0
  257. package/dist/packages/mcp/assets/kb/entries/sniping-and-snipers.md +29 -0
  258. package/dist/packages/mcp/assets/kb/entries/token2022-extensions-risk.md +32 -0
  259. package/dist/packages/mcp/assets/kb/entries/wash-trading-fake-volume.md +33 -0
  260. package/dist/packages/mcp/assets/skills/rug-check.md +52 -0
  261. package/dist/packages/mcp/assets/skills/token-dossier.md +57 -0
  262. package/dist/packages/mcp/assets/skills/trench-scan.md +40 -0
  263. package/dist/packages/mcp/bin/dexterm-mcp.mjs +16 -0
  264. package/dist/packages/mcp/src/dossier.js +78 -0
  265. package/dist/packages/mcp/src/firewall.js +21 -0
  266. package/dist/packages/mcp/src/index.js +22 -0
  267. package/dist/packages/mcp/src/server.js +936 -0
  268. package/dist/packages/mcp/src/user-content.js +434 -0
  269. package/dist/packages/packs/src/backtest.js +183 -0
  270. package/dist/packages/packs/src/digest.js +95 -0
  271. package/dist/packages/packs/src/firewall.js +57 -0
  272. package/dist/packages/packs/src/index.js +20 -0
  273. package/dist/packages/packs/src/larp-eval.js +100 -0
  274. package/dist/packages/packs/src/narrative.js +309 -0
  275. package/dist/packages/packs/src/reason.js +135 -0
  276. package/dist/packages/packs/src/rug-model.js +137 -0
  277. package/dist/packages/packs/src/schema.js +54 -0
  278. package/dist/packages/packs/src/skill-backtest.js +300 -0
  279. package/dist/packages/packs/src/token-dossier.js +174 -0
  280. package/dist/packages/packs/src/verdicts.js +17 -0
  281. package/dist/packages/packs/src/wallet-dossier.js +105 -0
  282. package/dist/packages/popup/src/config.js +71 -0
  283. package/dist/packages/popup/src/dock.js +271 -0
  284. package/dist/packages/popup/src/host.js +210 -0
  285. package/dist/packages/popup/src/index.js +12 -0
  286. package/dist/packages/popup/src/loopback.js +53 -0
  287. package/dist/packages/popup/src/open.js +183 -0
  288. package/dist/packages/popup/src/pages/bubbles-lib.js +160 -0
  289. package/dist/packages/popup/src/pages/bubbles.css +23 -0
  290. package/dist/packages/popup/src/pages/bubbles.html +36 -0
  291. package/dist/packages/popup/src/pages/bubbles.js +214 -0
  292. package/dist/packages/popup/src/pages/chart-lib.js +262 -0
  293. package/dist/packages/popup/src/pages/chart.css +50 -0
  294. package/dist/packages/popup/src/pages/chart.html +56 -0
  295. package/dist/packages/popup/src/pages/chart.js +554 -0
  296. package/dist/packages/popup/src/pages/common.css +34 -0
  297. package/dist/packages/popup/src/pages/common.js +187 -0
  298. package/dist/packages/popup/src/pages/feeds.html +20 -0
  299. package/dist/packages/popup/src/pages/feeds.js +77 -0
  300. package/dist/packages/popup/src/pages/narrative.html +20 -0
  301. package/dist/packages/popup/src/pages/narrative.js +61 -0
  302. package/dist/packages/popup/src/pages/plans.css +30 -0
  303. package/dist/packages/popup/src/pages/plans.html +26 -0
  304. package/dist/packages/popup/src/pages/plans.js +82 -0
  305. package/dist/packages/popup/src/pages/roster.css +41 -0
  306. package/dist/packages/popup/src/pages/roster.html +25 -0
  307. package/dist/packages/popup/src/pages/roster.js +75 -0
  308. package/dist/packages/popup/src/pages/token.html +20 -0
  309. package/dist/packages/popup/src/pages/token.js +112 -0
  310. package/dist/packages/popup/src/pages/widget-sdk.js +224 -0
  311. package/dist/packages/popup/src/pages/widget.css +5 -0
  312. package/dist/packages/popup/src/pages/widget.html +21 -0
  313. package/dist/packages/popup/src/pages/widget.js +85 -0
  314. package/dist/packages/popup/src/protocol.js +31 -0
  315. package/dist/packages/popup/src/selftest.js +156 -0
  316. package/dist/packages/popup/src/server.js +582 -0
  317. package/dist/packages/popup/src/sinkproxy.js +167 -0
  318. package/dist/packages/popup/src/widget-html.js +231 -0
  319. package/dist/packages/popup/vendor/lightweight-charts/LICENSE +201 -0
  320. package/dist/packages/popup/vendor/lightweight-charts/NOTICE +2 -0
  321. package/dist/packages/popup/vendor/lightweight-charts/README.md +30 -0
  322. package/dist/packages/popup/vendor/lightweight-charts/VERSION +1 -0
  323. package/dist/packages/popup/vendor/lightweight-charts/lightweight-charts.standalone.production.js +7 -0
  324. package/dist/packages/recorder/bin/dexterm-recorder.mjs +11 -0
  325. package/dist/packages/recorder/src/cli.js +113 -0
  326. package/dist/packages/recorder/src/export.js +71 -0
  327. package/dist/packages/recorder/src/index.js +20 -0
  328. package/dist/packages/recorder/src/labels.js +151 -0
  329. package/dist/packages/recorder/src/open.js +29 -0
  330. package/dist/packages/recorder/src/recorder.js +553 -0
  331. package/dist/packages/recorder/src/schedule.js +43 -0
  332. package/dist/packages/recorder/src/schema.js +135 -0
  333. package/dist/packages/recorder/src/snapshot.js +191 -0
  334. package/dist/packages/recorder/src/sweep.js +52 -0
  335. package/dist/packages/recorder/src/verdicts.js +52 -0
  336. package/dist/packages/tui/src/bubbles.js +415 -0
  337. package/dist/packages/tui/src/canvas.js +370 -0
  338. package/dist/packages/tui/src/chrome.js +85 -0
  339. package/dist/packages/tui/src/color.js +133 -0
  340. package/dist/packages/tui/src/flush.js +62 -0
  341. package/dist/packages/tui/src/fmt.js +188 -0
  342. package/dist/packages/tui/src/fx-trade.js +278 -0
  343. package/dist/packages/tui/src/fx.js +527 -0
  344. package/dist/packages/tui/src/images/blocks.js +141 -0
  345. package/dist/packages/tui/src/images/coin.js +22 -0
  346. package/dist/packages/tui/src/images/detect.js +140 -0
  347. package/dist/packages/tui/src/images/index.js +7 -0
  348. package/dist/packages/tui/src/images/layer.js +146 -0
  349. package/dist/packages/tui/src/images/pixels.js +157 -0
  350. package/dist/packages/tui/src/images/protocols.js +87 -0
  351. package/dist/packages/tui/src/index.js +18 -0
  352. package/dist/packages/tui/src/keymap.js +357 -0
  353. package/dist/packages/tui/src/layout.js +11 -0
  354. package/dist/packages/tui/src/raster-font.js +195 -0
  355. package/dist/packages/tui/src/raster.js +213 -0
  356. package/dist/packages/tui/src/sound.js +177 -0
  357. package/dist/packages/tui/src/theme.js +182 -0
  358. package/dist/packages/tui/src/widgets.js +598 -0
  359. package/dist/packages/voice/src/controller.js +407 -0
  360. package/dist/packages/voice/src/doctor.js +131 -0
  361. package/dist/packages/voice/src/download.js +184 -0
  362. package/dist/packages/voice/src/engines.js +84 -0
  363. package/dist/packages/voice/src/index.js +20 -0
  364. package/dist/packages/voice/src/router.js +502 -0
  365. package/dist/packages/voice/src/stt/apple-helper.swift.js +103 -0
  366. package/dist/packages/voice/src/stt/apple.js +179 -0
  367. package/dist/packages/voice/src/stt/fake.js +34 -0
  368. package/dist/packages/voice/src/stt/whisper.js +112 -0
  369. package/dist/packages/voice/src/summary.js +44 -0
  370. package/dist/packages/voice/src/tts/espeak.js +131 -0
  371. package/dist/packages/voice/src/tts/fake.js +30 -0
  372. package/dist/packages/voice/src/tts/piper.js +206 -0
  373. package/dist/packages/voice/src/tts/say.js +132 -0
  374. package/dist/packages/voice/src/tts/voices.js +206 -0
  375. package/dist/packages/voice/src/types.js +3 -0
  376. package/package.json +38 -4
  377. package/README.md +0 -3
@@ -0,0 +1,634 @@
1
+ // dexterm — the screener's feed on top of @dexterm/data (D-02/D-04/D-06): discovery per preset,
2
+ // enrichment of the mints, health → header status, column gates. Live (keyless, read-only),
3
+ // recorded while live, or replayed from a recorded fixture file (offline, same code path, no network).
4
+ import { readFileSync } from 'node:fs';
5
+ import { createDataLayer, FixtureStore, replayTransport, recordingTransport, liveTransport, gateAll, COLUMN_SOURCES, DEFAULT_BUCKETS, } from '../../data/src/index.js';
6
+ import { redactSecrets } from '../../data/src/index.js';
7
+ import { fromSnapshot } from "./screener/rows.js";
8
+ import { PRESETS, presetSpec, presetViews, bakedFilter } from "./screener/presets.js";
9
+ import { applyFilter } from "./screener/filter.js";
10
+ /** Feed snapshot rows (task-1323): cache namespace, rows kept, retention and write throttle. */
11
+ export const SNAPSHOT_NS = 'feed:snapshot';
12
+ export const SNAPSHOT_MAX_ROWS = 200;
13
+ export const SNAPSHOT_TTL_MS = 7 * 86_400_000;
14
+ export const SNAPSHOT_SAVE_MS = 30_000;
15
+ /** Boot fallback budget (task-1323): the default feed gets this long to show a row before Trending takes over. */
16
+ export const FALLBACK_AFTER_MS = 2_500;
17
+ /** Per-list API limit (Jupiter toptrending/organic/traded accept up to 100). */
18
+ export const PER_LIST = 100;
19
+ /** De-duplicated candidate-pool cap per refresh: hundreds of rows without hammering the sources. */
20
+ export const POOL_MAX = 500;
21
+ /** Enrich the pool in batches this big so rows paint progressively instead of in one jump. */
22
+ export const ENRICH_CHUNK = 75;
23
+ /**
24
+ * Self-imposed DexScreener budget for the wide-pool background fill (task-1303). The FAST
25
+ * tokens/v1 bucket is documented at 300 rpm, but a real interactive run (live feeds, 134 s idle)
26
+ * drove it to ~58 req/min — too close to the 60 rpm the SLOW endpoints share and enough to earn a
27
+ * 429. The background fill paces itself to this ceiling (well under the fast bucket's budget), so
28
+ * the visible rows paint at full speed while the long tail trickles in. Phase-1 (compact) and
29
+ * visible-first batches are NOT paced — the user always gets a fast first screen.
30
+ */
31
+ export const DS_TARGET_RPM = 24;
32
+ /** Each DexScreener token call covers 30 mints; one ENRICH_CHUNK is ceil(75/30)=3 calls. */
33
+ const DS_TOKENS_BATCH = 30;
34
+ function chunkMints(xs, size) {
35
+ const out = [];
36
+ for (let i = 0; i < xs.length; i += size)
37
+ out.push(xs.slice(i, i + size));
38
+ return out;
39
+ }
40
+ /** `42s` / `4m` / `3h` / `2d` for an age in ms. */
41
+ function agoLabel(ms) {
42
+ const s = Math.max(0, Math.round(ms / 1000));
43
+ return s < 60 ? `${s}s` : s < 3600 ? `${Math.floor(s / 60)}m` : s < 86_400 ? `${Math.floor(s / 3600)}h` : `${Math.floor(s / 86_400)}d`;
44
+ }
45
+ /**
46
+ * A minimal rolling-window pacer for the wide-pool background fill. It is NOT the data layer's rate
47
+ * limiter (that enforces the hard documented ceilings and honours Retry-After); this is a softer,
48
+ * self-imposed DexScreener budget layered on top so the wide pool cannot creep toward the 60 rpm
49
+ * shared cap. `wait()` returns the ms to sleep before issuing `cost` more DexScreener calls.
50
+ */
51
+ export class DsPacer {
52
+ stamps = [];
53
+ rpm;
54
+ now;
55
+ constructor(rpm, now) { this.rpm = rpm; this.now = now; }
56
+ /** Record `n` DexScreener calls as issued at the current clock. */
57
+ spent(n) {
58
+ const t = this.now();
59
+ for (let i = 0; i < n; i++)
60
+ this.stamps.push(t);
61
+ }
62
+ /** ms to wait before issuing `cost` more calls without exceeding the rolling 60 s budget. */
63
+ wait(cost) {
64
+ const t = this.now();
65
+ while (this.stamps.length && this.stamps[0] + 60_000 <= t)
66
+ this.stamps.shift();
67
+ const budget = this.rpm;
68
+ if (this.stamps.length + cost <= budget)
69
+ return 0;
70
+ // Need (stamps + cost - budget) slots to free up; wait until that many oldest fall out of the window.
71
+ const need = this.stamps.length + cost - budget;
72
+ const idx = Math.min(need - 1, this.stamps.length - 1);
73
+ return idx >= 0 ? Math.max(0, this.stamps[idx] + 60_000 - t) : 0;
74
+ }
75
+ /** Calls issued in the trailing 60 s (for the measured req/min the handoff reports). */
76
+ rate() {
77
+ const t = this.now();
78
+ while (this.stamps.length && this.stamps[0] + 60_000 <= t)
79
+ this.stamps.shift();
80
+ return this.stamps.length;
81
+ }
82
+ }
83
+ const SHORT = { dexscreener: 'dexscreener', 'jupiter-tokens': 'jup', 'jupiter-price': 'jup', geckoterminal: 'gecko', rugcheck: 'rugcheck', helius: 'helius', pumpfun: 'pump.fun' };
84
+ export class ScreenerFeed {
85
+ dl;
86
+ now;
87
+ limit;
88
+ preset = 'trending';
89
+ rows = [];
90
+ errors = {};
91
+ /** Epoch ms of the last completed refresh (null before the first). */
92
+ refreshedAt = null;
93
+ refreshing = false;
94
+ /** Called after each incremental batch of rows lands during a refresh, so the UI can paint the fill. */
95
+ onProgress = null;
96
+ /**
97
+ * Extra candidate mints to fold into the wide pool for live-backed presets (task-1303): the
98
+ * interactive loop points this at the PumpPortal new-pairs stream so "New"/"All" surface the
99
+ * freshest launches the REST feeds have not indexed yet. Off for replay/fixtures.
100
+ */
101
+ extraMints = null;
102
+ /** Size of the de-duplicated candidate pool the last refresh considered (for the "N of M" header). */
103
+ pool = 0;
104
+ /** Live requests attempted through this feed's data layer (counted by the HTTP client). */
105
+ requests = 0;
106
+ /**
107
+ * Mints the UI currently shows (task-1303): the interactive loop sets this to the on-screen rows so
108
+ * the wide-pool fill enriches what the user can see FIRST (unpaced, fast), then trickles the rest.
109
+ */
110
+ visibleMints = null;
111
+ /**
112
+ * True while the wide-pool background fill is deliberately throttling DexScreener — either our own
113
+ * soft budget (DS_TARGET_RPM) is holding it back, or the data layer paused the bucket after a 429 /
114
+ * Retry-After. Drives the header's "slowing down" hint. Never set during the fast visible-first paint.
115
+ */
116
+ slowing = false;
117
+ /** DexScreener calls the wide-pool fill issued in the trailing 60 s (reported in the handoff). */
118
+ dsRate = 0;
119
+ /**
120
+ * Running count of DexScreener requests (both the fast tokens bucket and the slow boosts/profiles
121
+ * bucket) the data layer has made, fed by `createLiveFeed`'s onRequest hook. The pacer differences
122
+ * it per chunk so the budget counts real calls, not an estimate (cache hits cost nothing).
123
+ */
124
+ dsCalls = 0;
125
+ pacer;
126
+ /** True while the rows on screen are the restored snapshot and no fresh batch has landed yet (task-1323). */
127
+ fromSnapshot = false;
128
+ /** When the restored snapshot was written (epoch ms); null when the rows are fresh. */
129
+ snapshotAt = null;
130
+ /** Set when the boot fallback switched presets (task-1323): from which preset and why. */
131
+ fellBack = null;
132
+ /** Epoch ms when this run's first network rows landed (null until then); drives the boot trace. */
133
+ firstFreshAt = null;
134
+ snapshot;
135
+ fastFirstPaint;
136
+ fallback;
137
+ /** Bumped by setPreset/close: a refresh started under an older generation stops writing rows. */
138
+ gen = 0;
139
+ lastSnapshotSave = 0;
140
+ fallbackArmed;
141
+ fallbackTimer = null;
142
+ /** Recording store when the feed was created with `record`; save() writes it. */
143
+ recording = null;
144
+ /** The fixture store behind a replay feed (its `streams` carry recorded PumpPortal sessions). */
145
+ replayStore = null;
146
+ poolMax;
147
+ /** Wide-pool discovery (task-1303); off for replays/recordings so fixtures stay identical. */
148
+ wide;
149
+ /**
150
+ * close() flips this so an in-flight refresh stops issuing work and the paced background sleep
151
+ * resolves immediately (task-1303 defect #1): a quit during a paced wide-pool fill must let the
152
+ * process exit at once, not hang for the rolling-window wait (which can approach 60 s).
153
+ */
154
+ closed = false;
155
+ /** The pending paced-fill sleep's timer + resolver, so close() can cancel/resolve it. */
156
+ sleepTimer = null;
157
+ sleepResolve = null;
158
+ constructor(dl, opts = {}) {
159
+ this.dl = dl;
160
+ this.now = opts.now ?? (() => Date.now());
161
+ this.limit = opts.limit ?? 30;
162
+ this.poolMax = opts.poolMax ?? POOL_MAX;
163
+ this.wide = opts.wide ?? false;
164
+ if (opts.preset)
165
+ this.preset = opts.preset;
166
+ this.pacer = new DsPacer(opts.dsTargetRpm ?? DS_TARGET_RPM, this.now);
167
+ this.snapshot = opts.snapshot ?? false;
168
+ this.fastFirstPaint = opts.fastFirstPaint ?? false;
169
+ this.fallback = opts.fallback ?? null;
170
+ this.fallbackArmed = !!this.fallback;
171
+ if (this.snapshot)
172
+ this.restoreSnapshot();
173
+ }
174
+ /** Rows the screener would SHOW for the current preset (its baked filter applied; user filter aside). */
175
+ shownCount() {
176
+ const spec = presetSpec(this.preset) ?? PRESETS[0];
177
+ return spec.baked ? applyFilter(this.rows, bakedFilter(spec)).length : this.rows.length;
178
+ }
179
+ /**
180
+ * Restore the last persisted rows of the current preset (task-1323). Only into an empty feed; rows
181
+ * come back flagged stale with their ages advanced by the time elapsed, `fromSnapshot` set until the
182
+ * first fresh batch lands. Never throws: a missing/corrupt snapshot just means a cold paint.
183
+ */
184
+ restoreSnapshot() {
185
+ if (!this.snapshot || this.rows.length)
186
+ return false;
187
+ try {
188
+ const hit = this.dl.cache.get(SNAPSHOT_NS, this.preset);
189
+ const snap = hit?.value;
190
+ if (!hit || !snap || snap.v !== 1 || !Array.isArray(snap.rows))
191
+ return false;
192
+ const elapsedMin = Math.max(0, (this.now() - hit.fetchedAt) / 60_000);
193
+ const rows = snap.rows.filter((r) => r && typeof r.mint === 'string' && r.mint && typeof r.symbol === 'string')
194
+ .map((r) => ({ ...r, stale: true, ageMin: typeof r.ageMin === 'number' ? r.ageMin + elapsedMin : null }));
195
+ if (!rows.length)
196
+ return false;
197
+ this.rows = rows;
198
+ this.pool = Math.max(rows.length, snap.pool || 0);
199
+ this.fromSnapshot = true;
200
+ this.snapshotAt = hit.fetchedAt;
201
+ return true;
202
+ }
203
+ catch {
204
+ return false;
205
+ }
206
+ }
207
+ /**
208
+ * Persist the current rows for this preset (task-1323): what the user sees first (the visible rows),
209
+ * then the rest, capped at SNAPSHOT_MAX_ROWS. Throttled to SNAPSHOT_SAVE_MS unless `force`. Rows that
210
+ * are themselves a restored snapshot are never re-saved (that would refresh a stale snapshot's clock).
211
+ */
212
+ saveSnapshot(force = false) {
213
+ if (!this.snapshot || this.fromSnapshot || this.closed || this.rows.length === 0)
214
+ return false;
215
+ const now = this.now();
216
+ if (!force && now - this.lastSnapshotSave < SNAPSHOT_SAVE_MS)
217
+ return false;
218
+ try {
219
+ const vis = new Set(this.visibleMints ? this.visibleMints() : []);
220
+ const ordered = [...this.rows.filter((r) => vis.has(r.mint)), ...this.rows.filter((r) => !vis.has(r.mint))];
221
+ const rows = ordered.slice(0, SNAPSHOT_MAX_ROWS).map((r) => { const { fixture: _f, ...rest } = r; return rest; });
222
+ const snap = { v: 1, preset: this.preset, pool: this.pool, rows };
223
+ this.dl.cache.set(SNAPSHOT_NS, this.preset, snap, { ttlMs: SNAPSHOT_TTL_MS, fetchedAt: now });
224
+ this.lastSnapshotSave = now;
225
+ return true;
226
+ }
227
+ catch {
228
+ return false;
229
+ }
230
+ }
231
+ /** Boot fallback check (task-1323): nothing on screen for the starting preset → switch to the fallback. */
232
+ maybeFallback(reason) {
233
+ const fb = this.fallback;
234
+ if (!fb || this.closed || this.fellBack || this.preset === fb.preset)
235
+ return;
236
+ if (this.shownCount() > 0)
237
+ return; // cached or fresh rows are on screen: never yank them away
238
+ const from = this.preset;
239
+ if (!this.setPreset(fb.preset))
240
+ return;
241
+ this.fellBack = { from, reason };
242
+ this.onProgress?.();
243
+ void this.refresh();
244
+ }
245
+ clearFallbackTimer() {
246
+ if (this.fallbackTimer) {
247
+ clearTimeout(this.fallbackTimer);
248
+ this.fallbackTimer = null;
249
+ }
250
+ }
251
+ presets() {
252
+ return presetViews(this.dl.health);
253
+ }
254
+ setPreset(id) {
255
+ const v = this.presets().find((p) => p.id === id);
256
+ if (!v || !v.available)
257
+ return false;
258
+ if (this.preset !== id) {
259
+ this.saveSnapshot(true); // keep the preset we are leaving for an instant switch back
260
+ this.preset = id;
261
+ this.rows = [];
262
+ this.refreshedAt = null;
263
+ this.pool = 0;
264
+ this.fromSnapshot = false;
265
+ this.snapshotAt = null;
266
+ // task-1323: orphan any in-flight refresh of the old preset (it stops writing rows at its next
267
+ // checkpoint and its pace sleep is woken) so the new preset refreshes now, not after the old fill.
268
+ this.gen++;
269
+ this.refreshing = false;
270
+ this.wakeSleep();
271
+ this.restoreSnapshot();
272
+ }
273
+ return true;
274
+ }
275
+ /** Resolve a pending paced-fill sleep now (preset switch / close). */
276
+ wakeSleep() {
277
+ if (this.sleepTimer) {
278
+ clearTimeout(this.sleepTimer);
279
+ this.sleepTimer = null;
280
+ }
281
+ if (this.sleepResolve) {
282
+ const r = this.sleepResolve;
283
+ this.sleepResolve = null;
284
+ r();
285
+ }
286
+ }
287
+ /**
288
+ * A cancellable, unref'd sleep for the paced background fill (task-1303 defect #1). Unref'd so a
289
+ * pending pace wait never keeps the process alive on its own; close() clears the timer and resolves
290
+ * it at once so a quit mid-fill exits immediately instead of waiting out the rolling window.
291
+ */
292
+ sleep(ms) {
293
+ if (this.closed || ms <= 0)
294
+ return Promise.resolve();
295
+ return new Promise((resolve) => {
296
+ this.sleepResolve = resolve;
297
+ const t = setTimeout(() => { this.sleepTimer = null; this.sleepResolve = null; resolve(); }, ms);
298
+ t.unref?.();
299
+ this.sleepTimer = t;
300
+ });
301
+ }
302
+ /**
303
+ * Discover the preset's candidate mints (store caches lists 15 s) and enrich them into rows.
304
+ * Two phases so the first rows paint fast and the pool then fills to hundreds (task-1303):
305
+ * 1. the compact feed (one or two lists) → paint right away;
306
+ * 2. the wide pool (toptrending/organic/traded across windows + boosts + profiles) → fill in
307
+ * enrichment-sized batches, painting each via `onProgress`.
308
+ * Jupiter list calls return fully-enriched token records (seeded into the cache), so enrichment of
309
+ * those mints is served from cache; DexScreener pair data is the only per-batch network cost.
310
+ * Never throws: source errors land in `errors` and degrade rows instead of failing the refresh.
311
+ */
312
+ async refresh() {
313
+ if (this.refreshing)
314
+ return this.rows;
315
+ this.refreshing = true;
316
+ const gen = this.gen;
317
+ /** This refresh was orphaned (preset switched / feed closed): stop issuing work and writing rows. */
318
+ const orphaned = () => this.closed || gen !== this.gen;
319
+ // Boot fallback (task-1323): armed once, for the first refresh only.
320
+ if (this.fallbackArmed && this.fallback) {
321
+ this.fallbackArmed = false;
322
+ const t = setTimeout(() => { this.fallbackTimer = null; this.maybeFallback('slow'); }, this.fallback.afterMs);
323
+ t.unref?.();
324
+ this.fallbackTimer = t;
325
+ }
326
+ // First-paint mode (task-1323): nothing fresh on screen yet (empty, or only the restored snapshot).
327
+ const quick = this.fastFirstPaint && (this.rows.length === 0 || this.fromSnapshot);
328
+ try {
329
+ const spec = presetSpec(this.preset) ?? PRESETS[0];
330
+ const errors = {};
331
+ const seen = new Set();
332
+ const order = [];
333
+ const rowByMint = new Map();
334
+ // Seed the render set from the rows already on screen (task-1303 defect #4). A refresh rebuilds
335
+ // its pool in two phases; without this seed, phase 1 (the compact feed, ~100 mints) would
336
+ // overwrite `this.rows` and the visible count would collapse to 100 on every cycle before the
337
+ // wide pool refilled it. Carrying the current rows forward means rows only grow/update during a
338
+ // refresh and stale mints are pruned once, at the end, after the pool is full again. (A restored
339
+ // snapshot is seeded the same way: its rows are replaced as fresh data lands, pruned at the end.)
340
+ for (const r of this.rows) {
341
+ order.push(r.mint);
342
+ rowByMint.set(r.mint, r);
343
+ }
344
+ // Monotonic baseline: every DexScreener call this refresh makes (discovery boosts/profiles AND
345
+ // enrichment token batches) is recorded against the budget exactly once, when `recordDs` runs.
346
+ let dsBaseline = this.dsCalls;
347
+ const recordDs = () => { this.pacer.spent(this.dsCalls - dsBaseline); dsBaseline = this.dsCalls; this.dsRate = this.pacer.rate(); };
348
+ /** Mints the quick first paint served DexScreener-only: re-enriched with Jupiter in phase 1b. */
349
+ const needJup = [];
350
+ // Enrich one chunk and fold the rows in. `pace` gates the chunk behind the soft DexScreener
351
+ // budget BEFORE issuing it (background tail only). Either way the ACTUAL DexScreener calls the
352
+ // chunk made — counted by the data layer's onRequest hook, so cached mints cost nothing — are
353
+ // recorded against the budget, so the unpaced visible-first chunks still make the tail back off.
354
+ const enrichBatch = async (batch, pace, jupCacheOnly = false) => {
355
+ // Fold any DexScreener calls made since the last record (discovery, or the previous chunk)
356
+ // into the budget before deciding whether to wait, so the count is always current.
357
+ recordDs();
358
+ if (pace) {
359
+ // One chunk is at most ceil(batch/30) DexScreener calls; wait if that would break the budget,
360
+ // or if the data layer paused the bucket after a 429 / Retry-After.
361
+ const cost = Math.ceil(batch.length / DS_TOKENS_BATCH);
362
+ const paused = this.dl.limiter.bucket('dexscreener').stats().pausedUntil;
363
+ const wait = Math.max(this.pacer.wait(cost), paused ? paused - this.now() : 0);
364
+ if (wait > 0) {
365
+ this.slowing = true;
366
+ this.onProgress?.();
367
+ await this.sleep(wait);
368
+ }
369
+ }
370
+ if (orphaned())
371
+ return; // a quit / preset switch during the pace wait: stop before issuing more calls
372
+ // Wide pool skips the per-mint Jupiter price call (list + DexScreener already carry price) to
373
+ // respect the shared 0.5 rps Jupiter bucket; the compact feed keeps it (and matches recordings).
374
+ const res = await this.dl.store.enrich(batch, jupCacheOnly ? { price: false, jup: 'cache' } : { price: !this.wide });
375
+ recordDs();
376
+ if (orphaned())
377
+ return;
378
+ this.slowing = this.dl.limiter.bucket('dexscreener').stats().pausedUntil > 0;
379
+ Object.assign(errors, res.errors);
380
+ const now = this.now();
381
+ for (const r of res.rows) {
382
+ if (!rowByMint.has(r.mint))
383
+ order.push(r.mint);
384
+ rowByMint.set(r.mint, fromSnapshot(r, now));
385
+ if (jupCacheOnly && !r.sources.includes('jupiter-tokens'))
386
+ needJup.push(r.mint);
387
+ }
388
+ if (jupCacheOnly)
389
+ needJup.push(...res.missing);
390
+ this.rows = order.map((m) => rowByMint.get(m)).filter(Boolean);
391
+ this.errors = errors;
392
+ this.refreshedAt = now;
393
+ if (res.rows.length)
394
+ this.landedFresh(now);
395
+ this.onProgress?.();
396
+ };
397
+ const pending = [];
398
+ const absorb = (mints) => {
399
+ for (const m of mints)
400
+ if (!seen.has(m)) {
401
+ seen.add(m);
402
+ pending.push(m);
403
+ }
404
+ this.pool = Math.max(seen.size, this.fromSnapshot ? this.pool : 0);
405
+ };
406
+ /** Enrich everything collected so far. `pace` throttles the DexScreener calls (background fill). */
407
+ const flush = async (pace, jupCacheOnly = false) => {
408
+ while (pending.length) {
409
+ if (orphaned())
410
+ return;
411
+ const batch = pending.splice(0, ENRICH_CHUNK);
412
+ await enrichBatch(batch, pace, jupCacheOnly);
413
+ }
414
+ };
415
+ this.slowing = false;
416
+ // Phase 1 — the compact feed, for a fast first paint (never paced).
417
+ const feeds = spec.feeds;
418
+ if (quick && feeds.length) {
419
+ // Phase 1a (task-1323): ONE list, enriched DexScreener-only (Jupiter records come from the list
420
+ // call's cache seed), then paint — the first screen never waits on the Jupiter bucket.
421
+ const d = await this.dl.store.discover(feeds[0], this.limit);
422
+ if (orphaned())
423
+ return this.rows;
424
+ Object.assign(errors, d.errors);
425
+ absorb(d.mints);
426
+ await flush(false, true);
427
+ if (orphaned())
428
+ return this.rows;
429
+ // Phase 1b: the rows that painted without Jupiter fields get them now (one batched search).
430
+ pending.push(...needJup.splice(0));
431
+ }
432
+ for (const feed of quick ? feeds.slice(1) : feeds) {
433
+ const d = await this.dl.store.discover(feed, this.limit);
434
+ if (orphaned())
435
+ return this.rows;
436
+ Object.assign(errors, d.errors);
437
+ absorb(d.mints);
438
+ }
439
+ await flush(false);
440
+ if (orphaned())
441
+ return this.rows;
442
+ // Phase 2 — the wide pool (live only). Discover the whole candidate net, then enrich
443
+ // VISIBLE rows first (fast, unpaced) so what the user sees fills immediately, and trickle the
444
+ // rest under the soft DexScreener budget so the pool cannot creep toward the shared rate cap.
445
+ if (this.wide) {
446
+ for (const feed of feeds) {
447
+ const d = await this.dl.store.discover(feed, this.limit, { max: this.poolMax });
448
+ if (orphaned())
449
+ return this.rows;
450
+ Object.assign(errors, d.errors);
451
+ absorb(d.mints);
452
+ }
453
+ // Phase 2a — fold the live PumpPortal new-pairs stream into the candidate pool for live-backed
454
+ // presets. Brand-new mints with no pair yet degrade to dashes like any other row.
455
+ if (this.extraMints && spec.live)
456
+ absorb(this.extraMints().slice(0, this.poolMax));
457
+ // Phase 2b — visible rows first (unpaced), then the long tail paced under the soft budget.
458
+ // Reorder `pending` so on-screen mints lead; enrich that many unpaced, the remainder paced.
459
+ const visible = new Set(this.visibleMints ? this.visibleMints() : []);
460
+ const front = pending.filter((m) => visible.has(m));
461
+ const back = pending.filter((m) => !visible.has(m));
462
+ pending.length = 0;
463
+ pending.push(...front, ...back);
464
+ let remainingFront = front.length;
465
+ while (pending.length) {
466
+ if (orphaned())
467
+ break;
468
+ const batch = pending.splice(0, Math.min(ENRICH_CHUNK, remainingFront > 0 ? remainingFront : ENRICH_CHUNK));
469
+ const paced = remainingFront <= 0;
470
+ remainingFront -= batch.length;
471
+ await enrichBatch(batch, paced);
472
+ }
473
+ }
474
+ if (orphaned())
475
+ return this.rows;
476
+ // Prune rows that fell out of this refresh's candidate pool (task-1303 defect #4): done once,
477
+ // now that the pool is full, so the visible count never dips mid-refresh. Skipped on an aborted
478
+ // (closed) or empty-discovery refresh so a quit/transient miss cannot wipe the rows on screen.
479
+ if (seen.size) {
480
+ this.rows = order.filter((m) => seen.has(m)).map((m) => rowByMint.get(m)).filter(Boolean);
481
+ this.pool = seen.size;
482
+ // A completed refresh replaced the snapshot wholesale (even if every list came back empty).
483
+ if (this.fromSnapshot) {
484
+ this.fromSnapshot = false;
485
+ this.snapshotAt = null;
486
+ }
487
+ }
488
+ this.errors = errors;
489
+ this.refreshedAt = this.now();
490
+ this.slowing = false;
491
+ this.saveSnapshot();
492
+ // Boot fallback (task-1323): the first refresh came back with nothing to show.
493
+ if (this.fallback && !this.fellBack && this.fallbackTimer !== null) {
494
+ this.clearFallbackTimer();
495
+ if (this.shownCount() === 0)
496
+ this.maybeFallback('empty');
497
+ }
498
+ return this.rows;
499
+ }
500
+ catch (err) {
501
+ if (!orphaned())
502
+ this.errors = { ...this.errors, dexscreener: redactSecrets(err instanceof Error ? err.message : String(err)) };
503
+ return this.rows;
504
+ }
505
+ finally {
506
+ if (gen === this.gen)
507
+ this.refreshing = false;
508
+ }
509
+ }
510
+ /** The first network rows of this run landed (task-1323): drop the snapshot marker, persist, disarm. */
511
+ landedFresh(now) {
512
+ if (this.firstFreshAt === null)
513
+ this.firstFreshAt = now;
514
+ if (this.fromSnapshot) {
515
+ this.fromSnapshot = false;
516
+ this.snapshotAt = null;
517
+ }
518
+ if (this.shownCount() > 0)
519
+ this.clearFallbackTimer();
520
+ if (this.lastSnapshotSave === 0)
521
+ this.saveSnapshot(true);
522
+ }
523
+ /** Column gates from the health registry (SPEC §3.5): columns whose only source is down render dim. */
524
+ gates() {
525
+ return gateAll(this.dl.health, COLUMN_SOURCES);
526
+ }
527
+ status() {
528
+ const h = this.dl.health;
529
+ const used = ['dexscreener', 'jupiter-tokens', 'jupiter-price'];
530
+ const bad = used.filter((s) => { const st = h.state(s); return st === 'degraded' || st === 'probing'; });
531
+ const names = [...new Set(bad.map((s) => SHORT[s] ?? s))];
532
+ const label = [...new Set(used.filter((s) => h.isAvailable(s)).map((s) => SHORT[s] ?? s))].join(' · ') || 'no sources';
533
+ // task-1323: restored rows say so until the first fresh batch replaces them.
534
+ const cached = this.fromSnapshot && this.snapshotAt !== null ? `◌ cached ${agoLabel(this.now() - this.snapshotAt)} old · refreshing` : undefined;
535
+ const fell = this.fellBack ? `◐ ${this.fellBack.from} ${this.fellBack.reason === 'slow' ? 'slow' : 'empty'} → ${this.preset}` : undefined;
536
+ const degraded = names.length ? `◐ degraded: ${names.join(', ')}` : undefined;
537
+ return { headerStatus: degraded ?? cached ?? fell, feedLabel: label, errors: this.errors };
538
+ }
539
+ refreshedAgo() {
540
+ if (this.fromSnapshot && this.snapshotAt !== null)
541
+ return agoLabel(this.now() - this.snapshotAt);
542
+ if (this.refreshedAt === null)
543
+ return this.refreshing ? '⌛' : '—';
544
+ const s = Math.max(0, Math.round((this.now() - this.refreshedAt) / 1000));
545
+ return s < 60 ? `${s}s` : s < 3600 ? `${Math.floor(s / 60)}m` : `${Math.floor(s / 3600)}h`;
546
+ }
547
+ /** Write the recording (when created with `record`) in the data layer's fixture format. */
548
+ save(path, note) {
549
+ if (!this.recording)
550
+ throw new Error('feed was not created with a recorder');
551
+ this.recording.save(path, note ?? `recorded by dexterm --record; ${this.requests} live requests; preset ${this.preset}`);
552
+ }
553
+ close() {
554
+ // Abort any in-flight paced fill first (task-1303 defect #1): mark closed so refresh() bails, then
555
+ // clear and resolve the pending pace sleep so the event loop is not held for the rolling window.
556
+ // (No snapshot write here: quit must stay instant; refresh() persists at first paint + every 30 s.)
557
+ this.closed = true;
558
+ this.gen++;
559
+ this.clearFallbackTimer();
560
+ this.wakeSleep();
561
+ this.dl.close();
562
+ }
563
+ }
564
+ /**
565
+ * Keyless live feed: public GET endpoints only, through the documented rate limits (the data
566
+ * layer's limiter paces calls and honours Retry-After). pump.fun stays off until D-08 lands.
567
+ */
568
+ export function createLiveFeed(opts = {}) {
569
+ let store = null;
570
+ // task-1319 (AC1): the live transport the app actually runs must be aborted on quit. Build it with a
571
+ // shutdown controller and hand the same controller to the data layer so feed.close() -> dl.close()
572
+ // aborts THIS transport's in-flight fetch (the signal only reaches a caller-built transport this way).
573
+ const shutdown = new AbortController();
574
+ let transport = liveTransport({ shutdownSignal: shutdown.signal });
575
+ if (opts.record || opts.maxRequests) {
576
+ store = new FixtureStore();
577
+ transport = recordingTransport(transport, store, { maxRequests: opts.maxRequests || Number.MAX_SAFE_INTEGER });
578
+ }
579
+ let feed;
580
+ const dl = createDataLayer({
581
+ dbPath: opts.dbPath,
582
+ transport,
583
+ shutdownController: shutdown,
584
+ disabled: ['pumpfun'],
585
+ rpcUrl: opts.rpcUrl,
586
+ rpcFallbackUrl: opts.rpcFallbackUrl,
587
+ rpcExtraUrls: opts.rpcExtraUrls,
588
+ rpcPublicPool: opts.rpcPublicPool,
589
+ credits: opts.maxCredits ? { runCap: opts.maxCredits } : undefined,
590
+ onRequest: (req, meta, o) => {
591
+ feed.requests++;
592
+ if (meta.bucket === 'dexscreener' || meta.bucket === 'dexscreener-slow')
593
+ feed.dsCalls++;
594
+ opts.onRequest?.(req, meta, o);
595
+ },
596
+ });
597
+ // task-1311 (QA M4): the live app opens on the trench feed (fresh pump.fun + young memecoins,
598
+ // blue chips / stocks / stables filtered out) unless the caller asked for a specific preset.
599
+ // task-1323: paint the last cached feed instantly, first paint from one list, Trending fallback at 2.5 s.
600
+ feed = new ScreenerFeed(dl, {
601
+ ...opts, preset: opts.preset ?? 'trench', limit: opts.limit ?? PER_LIST, wide: true,
602
+ snapshot: opts.snapshot ?? true, fastFirstPaint: opts.fastFirstPaint ?? true,
603
+ fallback: opts.fallback === undefined ? { preset: 'trending', afterMs: FALLBACK_AFTER_MS } : opts.fallback,
604
+ });
605
+ // the recorder keeps every response (for --record) or just counts (for --max-requests)
606
+ feed.recording = store;
607
+ return feed;
608
+ }
609
+ /** Fixture clock from the recording so ages and the header clock are reproducible. */
610
+ export function fixtureRecordedAt(path) {
611
+ const file = JSON.parse(readFileSync(path, 'utf8'));
612
+ const t = Date.parse(file.recordedAt ?? '');
613
+ return Number.isFinite(t) ? t : Date.now();
614
+ }
615
+ /** Rate buckets with nothing to be polite to (replays and tests). */
616
+ export function unlimitedBuckets() {
617
+ return Object.fromEntries(Object.entries(DEFAULT_BUCKETS).map(([k, v]) => [k, { ...v, rpm: 1e6, burst: 1e6 }]));
618
+ }
619
+ /**
620
+ * Offline feed over a recorded fixture file (the data layer's recorder format). Same store, cache,
621
+ * health registry and adapters as live; the transport is the replayer, so a miss throws instead of
622
+ * touching the network. Rate buckets are widened: there is nothing to be polite to.
623
+ */
624
+ export function createReplayFeed(path, opts = {}) {
625
+ const store = FixtureStore.load(path);
626
+ const recordedAt = fixtureRecordedAt(path);
627
+ // A recording made with a Helius key has `mainnet.helius-rpc.com/?api-key=REDACTED` entries: replay against that host.
628
+ const heliusEntry = store.list().find((e) => e.url.includes('mainnet.helius-rpc.com'));
629
+ const rpcUrl = opts.rpcUrl ?? (heliusEntry ? heliusEntry.url : undefined);
630
+ const dl = createDataLayer({ transport: replayTransport(store), buckets: unlimitedBuckets(), disabled: ['pumpfun'], rpcUrl, rpcFallbackUrl: null });
631
+ const feed = new ScreenerFeed(dl, { now: opts.now ?? (() => recordedAt + 2000), limit: opts.limit });
632
+ feed.replayStore = store;
633
+ return feed;
634
+ }