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,170 @@
1
+ // dexterm — RULES screen (ctrl+r, task-1358 Tier 3). The user's rules (`config.rules`): enabled, name, the
2
+ // condition, the actions, the source (user / pack / jarvis), fires in the last hour, last fired. Below it the
3
+ // PROPOSALS pane (ticket/ask cards a rule produced — inert until the user acts on them, and even then the paper
4
+ // ticket editor / the analyst is where anything happens) or the LOG of recent fires. Typing a rule shows a live
5
+ // dry-run against the rows on screen: "would match N of M rows right now". Nothing on this screen arms or signs.
6
+ import { Canvas, header, footer, breakpoint, fmtAgeSec } from '../../../tui/src/index.js';
7
+ import { describeActions } from '../../../custom/src/index.js';
8
+ import { headerModel } from "../state.js";
9
+ import { describeCard } from "../rules.js";
10
+ function fit(s, w) { return s.length > w ? (w > 1 ? s.slice(0, w - 1) + '…' : s.slice(0, w)) : s; }
11
+ export function renderRules(c, state) {
12
+ const t = c.theme;
13
+ const bp = breakpoint(c.w);
14
+ header(c, headerModel(state));
15
+ const ui = state.rulesUi ?? null;
16
+ const rules = ui?.engine.rules ?? [];
17
+ const now = state.now.getTime();
18
+ const top = 2, bottom = c.h - 1;
19
+ const typing = ui?.input !== null && ui?.input !== undefined;
20
+ const enabled = rules.filter((r) => r.enabled).length;
21
+ const title = `RULES · ${rules.length} (${enabled} on) · ${ui?.fires ?? 0} fires this session · ${ui?.proposals.length ?? 0} proposal${(ui?.proposals.length ?? 0) === 1 ? '' : 's'}`;
22
+ const caption = bp === 'compact' ? undefined : 'when <cond> [for n ticks] then alert|ticket|ask|popup|tag|sound|watch · parsed, never eval\'d · rules only alert or PROPOSE';
23
+ const lowerH = Math.max(6, Math.min(14, Math.floor((bottom - top) * 0.4)));
24
+ const listH = bottom - top - lowerH;
25
+ const rulesFocus = !typing && (ui?.pane ?? 'rules') === 'rules';
26
+ const box = c.box(0, top, c.w, listH, { title, caption, focus: rulesFocus });
27
+ const bg = box.bg;
28
+ let cy = box.y;
29
+ if (ui?.errors.length) {
30
+ for (const e of ui.errors.slice(0, 2)) {
31
+ c.text(box.x + 1, cy, `⚠ ${e}`, { fg: t.warn, bg }, box.w - 2);
32
+ cy += 1;
33
+ }
34
+ }
35
+ if (!rules.length) {
36
+ c.text(box.x + 1, cy + 1, 'no rules yet.', { fg: t.muted, bg });
37
+ cy += 3;
38
+ c.text(box.x + 1, cy, 'i type a rule, e.g. vol5m.velocity > 3 and cluster < 20 and liq > 25K for 2 ticks then alert(up, "surge") popup(chart)', { fg: t.dim, bg }, box.w - 2);
39
+ cy += 1;
40
+ c.text(box.x + 1, cy, 'dexterm rules add "<rule>" the same from a shell · rules list | check | dry-run | remove | enable | disable', { fg: t.dim, bg }, box.w - 2);
41
+ cy += 1;
42
+ c.text(box.x + 1, cy, 'fields: price mcap fdv liq vol5m vol1h vol24 holders org top10 dev lplocked bundled snipers cluster age m5 h1 h6 h24 buys sells bonding', { fg: t.dim, bg }, box.w - 2);
43
+ cy += 1;
44
+ c.text(box.x + 1, cy, 'signals: <field>.velocity (× its own pace) · <field>.delta (change in the window) · tags: launchpad: flag: verified: sus:', { fg: t.dim, bg }, box.w - 2);
45
+ cy += 2;
46
+ c.text(box.x + 1, cy, 'every rule has a cooldown (10m) and a max/h (12, cap 120) · ticket/ask make PROPOSAL cards: you still size + confirm in the paper ticket', { fg: t.dim, bg, i: true }, box.w - 2);
47
+ }
48
+ else {
49
+ const wide = bp !== 'compact';
50
+ const xs = wide ? [1, 5, 24, 0, 0, 0, 0] : [1, 5, 20, 0, 0, 0, 0];
51
+ // right-anchored columns: LAST(6) F/H(4) SRC(9) THEN(28 wide / 18)
52
+ const lastW = 6, fhW = 4, srcW = wide ? 9 : 6, thenW = wide ? 30 : 18;
53
+ xs[6] = box.w - 1 - lastW;
54
+ xs[5] = xs[6] - 1 - fhW;
55
+ xs[4] = xs[5] - 1 - srcW;
56
+ xs[3] = xs[4] - 1 - thenW;
57
+ const whenW = Math.max(8, xs[3] - xs[2] - 1);
58
+ const heads = ['ON', 'RULE', 'WHEN', 'THEN', 'SRC', 'F/H', 'LAST'];
59
+ heads.forEach((h, i) => c.text(box.x + xs[i], cy, h, { fg: t.dim, bg }));
60
+ cy += 1;
61
+ const sel = Math.max(0, Math.min(rules.length - 1, ui?.selected ?? 0));
62
+ const scroll = Math.max(0, sel - (box.h - 4));
63
+ for (let i = scroll; i < rules.length; i++) {
64
+ if (cy >= box.y + box.h - 1)
65
+ break;
66
+ const r = rules[i];
67
+ const selected = i === sel && rulesFocus;
68
+ const rb = selected ? t.selBg : bg;
69
+ c.fill(box.x, cy, box.w, 1, ' ', { bg: rb });
70
+ if (selected)
71
+ c.put(box.x, cy, '▎', { fg: t.accent, bg: rb, soft: true });
72
+ c.text(box.x + xs[0], cy, r.enabled ? 'on' : 'off', { fg: r.enabled ? t.up : t.dim, bg: rb, b: r.enabled });
73
+ c.text(box.x + xs[1], cy, fit(r.name === r.id ? r.id : r.name, xs[2] - xs[1] - 1), { fg: selected ? t.borderFocus : t.text, bg: rb });
74
+ c.text(box.x + xs[2], cy, fit(`${r.whenText}${r.for ? ` for ${'ticks' in r.for ? r.for.ticks + ' ticks' : Math.round(r.for.ms / 1000) + 's'}` : ''}`, whenW), { fg: r.enabled ? t.text : t.muted, bg: rb });
75
+ c.text(box.x + xs[3], cy, fit(describeActions(r), thenW), { fg: t.accent, bg: rb });
76
+ c.text(box.x + xs[4], cy, fit(r.source.replace(/^pack:/, 'pk:'), srcW), { fg: r.source === 'user' ? t.muted : t.warn, bg: rb });
77
+ const fh = ui.engine.firesLastHour(r.id, now);
78
+ c.text(box.x + xs[5], cy, String(fh).padStart(3), { fg: fh ? t.text : t.dim, bg: rb });
79
+ const last = ui.engine.lastFiredAt(r.id);
80
+ c.text(box.x + xs[6], cy, last ? fmtAgeSec(Math.max(0, Math.round((now - last) / 1000))) : '—', { fg: last ? t.muted : t.dim, bg: rb });
81
+ cy += 1;
82
+ }
83
+ }
84
+ if (ui?.notice && !typing)
85
+ c.textR(box.x + 1, box.y + box.h - 1, box.w - 2, ui.notice, { fg: t.warn, bg, i: true });
86
+ // lower pane: PROPOSALS or LOG
87
+ const proposalsPane = (ui?.pane ?? 'rules') === 'proposals';
88
+ const ltitle = proposalsPane
89
+ ? `PROPOSALS · ${ui?.proposals.length ?? 0} pending · inert until you act`
90
+ : `LOG · last ${Math.min(ui?.log.length ?? 0, lowerH - 2)} of ${ui?.log.length ?? 0} · tab for proposals`;
91
+ const lcaption = bp === 'compact' ? undefined : proposalsPane
92
+ ? '⏎ = open the PAPER ticket editor / ask the analyst (you confirm there) · x = dismiss · nothing here arms or signs'
93
+ : 'alerts, proposals, watch/tag, popups as they fired · reasons carry each satisfied clause with its value';
94
+ const lbox = c.box(0, top + listH, c.w, lowerH, { title: ltitle, caption: lcaption, focus: !typing && proposalsPane });
95
+ const lbg = lbox.bg;
96
+ let ly = lbox.y;
97
+ const lrows = lbox.h - (typing ? 2 : 0);
98
+ if (proposalsPane) {
99
+ const cards = ui?.proposals ?? [];
100
+ if (!cards.length)
101
+ c.text(lbox.x + 1, ly, 'no proposals pending — a rule with ticket(...) or ask(...) puts its card here when it fires', { fg: t.dim, bg: lbg, i: true });
102
+ const psel = Math.max(0, Math.min(cards.length - 1, ui?.selectedProposal ?? 0));
103
+ const pscroll = Math.max(0, psel - (lrows - 1));
104
+ for (let i = pscroll; i < cards.length; i++) {
105
+ if (ly >= lbox.y + lrows)
106
+ break;
107
+ const p = cards[i];
108
+ const selected = i === psel && !typing;
109
+ const rb = selected ? t.selBg : lbg;
110
+ c.fill(lbox.x, ly, lbox.w, 1, ' ', { bg: rb });
111
+ if (selected)
112
+ c.put(lbox.x, ly, '▎', { fg: t.accent, bg: rb, soft: true });
113
+ let cx = c.text(lbox.x + 1, ly, fmtAgeSec(Math.max(0, Math.round((now - p.at) / 1000))).padEnd(5), { fg: t.dim, bg: rb });
114
+ cx = c.text(cx + 1, ly, 'PROPOSAL', { fg: t.warn, bg: rb, b: true });
115
+ cx = c.text(cx + 1, ly, fit(describeCard(p), 34).padEnd(34), { fg: p.type === 'ticket' ? (p.side === 'buy' ? t.up : t.down) : t.accent, bg: rb });
116
+ cx = c.text(cx + 1, ly, fit(p.name, 14).padEnd(14), { fg: t.muted, bg: rb });
117
+ c.text(cx + 1, ly, fit(p.why, lbox.w - (cx + 1 - lbox.x) - 1), { fg: t.dim, bg: rb });
118
+ ly += 1;
119
+ }
120
+ }
121
+ else {
122
+ const log = ui?.log ?? [];
123
+ if (!log.length)
124
+ c.text(lbox.x + 1, ly, ui?.engine.rules.some((r) => r.enabled) ? 'nothing fired yet (rules evaluate the screener rows every few seconds)' : 'no enabled rules', { fg: t.dim, bg: lbg, i: true });
125
+ for (const e of log) {
126
+ if (ly >= lbox.y + lrows)
127
+ break;
128
+ const fg = e.kind === 'alert' ? (e.level === 'up' ? t.up : e.level === 'down' ? t.down : e.level === 'warn' ? t.warn : t.text) : e.kind === 'proposal' ? t.warn : t.muted;
129
+ let cx = c.text(lbox.x + 1, ly, fmtAgeSec(Math.max(0, Math.round((now - e.at) / 1000))).padEnd(5), { fg: t.dim, bg: lbg });
130
+ cx = c.text(cx + 1, ly, fit(e.symbol, 8).padEnd(8), { fg: t.accent, bg: lbg });
131
+ cx = c.text(cx + 1, ly, fit(e.name, 14).padEnd(14), { fg: t.muted, bg: lbg });
132
+ c.text(cx + 1, ly, fit(e.text, lbox.w - (cx + 1 - lbox.x) - 1), { fg, bg: lbg });
133
+ ly += 1;
134
+ }
135
+ }
136
+ if (typing && ui) {
137
+ const iy = lbox.y + lbox.h - 1;
138
+ c.hline(lbox.x, iy - 1, lbox.w, { fg: t.border, bg: lbg });
139
+ const label = ui.editingId ? `edit ${ui.editingId}: ` : 'rule: ';
140
+ c.text(lbox.x + 1, iy, label, { fg: t.accent, bg: lbg, b: true });
141
+ const avail = lbox.w - label.length - 3;
142
+ const txt = ui.input;
143
+ const shown = txt.length > avail ? '…' + txt.slice(txt.length - avail + 1) : txt;
144
+ c.text(lbox.x + 1 + label.length, iy, `${shown}█`, { fg: t.text, bg: lbg }, avail + 1);
145
+ // the live dry-run on the line above the input
146
+ const d = ui.dry;
147
+ let dryText;
148
+ let dryFg;
149
+ if (!txt.trim()) {
150
+ dryText = 'type a rule · ⏎ save · esc cancel';
151
+ dryFg = t.dim;
152
+ }
153
+ else if (!d) {
154
+ dryText = 'checking…';
155
+ dryFg = t.dim;
156
+ }
157
+ else if (d.error) {
158
+ dryText = `✗ ${d.error}`;
159
+ dryFg = t.down;
160
+ }
161
+ else {
162
+ const names = d.matches.slice(0, 6).map((m) => m.symbol).join(' ');
163
+ dryText = `✓ would match ${d.matches.length} of ${d.considered} rows right now${names ? ` (${names}${d.matches.length > 6 ? ' …' : ''})` : ''}${d.noHistory ? ` · ${d.noHistory} without history yet (velocity/delta need two earlier samples)` : ''} · ${describeActions(d.rule)} · cooldown ${Math.round(d.rule.cooldownMs / 60000)}m · max ${d.rule.maxPerHour}/h`;
164
+ dryFg = d.matches.length ? t.up : t.muted;
165
+ }
166
+ c.text(lbox.x + 1, iy - 1, ` ${fit(dryText, lbox.w - 4)} `, { fg: dryFg, bg: lbg });
167
+ c.textR(lbox.x + 1, iy, lbox.w - 2, '⏎ save · esc cancel', { fg: t.dim, bg: lbg });
168
+ }
169
+ footer(c, [['↑↓', 'move'], ['i', 'new rule'], ['⏎', proposalsPane ? 'act on proposal' : 'edit rule'], ['space', 'on/off'], ['d', 'delete'], ['tab', proposalsPane ? 'log' : 'proposals'], ['x', 'dismiss'], ['esc', 'back'], ['q', 'quit']], t.name);
170
+ }
@@ -0,0 +1,459 @@
1
+ // dexterm — Screener screen: ranked token table + neon sparklines + side spotlight at wide widths.
2
+ // Rows are `ScreenerRow`s (fixtures or the data store); cells the health registry cannot vouch
3
+ // for render as a dim dash with the reason in the panel caption, never as a number.
4
+ // Cyberspace styling (task-1297): rain zones in the filter-bar gap, the table/spotlight gutter and the
5
+ // spotlight's blank middle; the selected ticker in phosphor; ▲/▼ on every direction (VISUAL-V2 §6.5).
6
+ import { Canvas, table, sparkline, sparkArea, heatPct, launchpadBadge, ratioBar, badge, pctStyle, statGrid, fmtUsd, fmtPrice, fmtAge, fmtNum, fmtPct, fmtPctDir, fmtDeltaDir, shortAddr, header, footer, breakpoint, mix, isCalm, onAccent, } from '../../../tui/src/index.js';
7
+ import { layoutFor } from "../custom.js";
8
+ import { headerModel } from "../state.js";
9
+ import { screenerView, gateCaption, feedCondition } from "../screener/view.js";
10
+ import { SORT_KEYS, sortSpec } from "../screener/sort.js";
11
+ import { RUG_FLAG_THRESHOLD, RUG_MODEL_LABEL, fmtRugP, rugDataAge, rugP } from "../screener/rug.js";
12
+ import { drawCoin, imagesOn } from "./coin.js";
13
+ import { COIN_ICON_W } from '../../../tui/src/index.js';
14
+ import { priceCell, priceModeOf, promoteMcap, fallbackCount, fallbackCaption, FALLBACK_MARK, MCAP_FALLBACK_NOTE } from "../display.js";
15
+ /** Table column key → health gate id (COLUMN_SOURCES). Columns without an entry are always shown. */
16
+ export const COLUMN_GATE = {
17
+ lp: 'launchpad', age: 'age', price: 'price', m5: '5m', h1: '1h', h6: '6h', h24: '24h', vol: 'vol', liq: 'liq', mcap: 'mcap',
18
+ holders: 'holders', org: 'org', txns: 'bs', spark: 'sparkline', flags: 'flags',
19
+ };
20
+ const DEFAULT_QUERY = 'vol24 > 250K liq > 50K age < 30d chain:solana';
21
+ /**
22
+ * LARP-detector badge for a verdict (task-1315): a short label + colour, or null for a verdict with
23
+ * no badge. Only ever called for a mint whose verdict was actually computed (present in larpByMint).
24
+ */
25
+ export function larpBadge(verdict, t) {
26
+ switch (verdict) {
27
+ case 'LARP': return { text: 'LARP', fg: t.down };
28
+ case 'REAL_PRODUCT': return { text: 'REAL', fg: t.up };
29
+ case 'EARLY_BUT_REAL': return { text: 'REAL?', fg: mix(t.bg, t.up, 0.8) };
30
+ case 'UNVERIFIABLE':
31
+ case 'AMBIGUOUS': return { text: 'UNKNOWN', fg: t.warn };
32
+ default: return null;
33
+ }
34
+ }
35
+ export function screenerColumns(c, bp, gates = null, larp = null, icon = null, mode = 'price') {
36
+ const t = c.theme;
37
+ const muted = (bg) => ({ fg: t.muted, bg });
38
+ const dash = (cv, x, y, w, bg, align = 'right') => {
39
+ if (align === 'right')
40
+ cv.textR(x, y, w, '—', { fg: t.dim, bg });
41
+ else
42
+ cv.text(x, y, '—', { fg: t.dim, bg });
43
+ };
44
+ void gates; // the gate drives the panel caption (gateCaption); cells only ever dash a null
45
+ const cols = [
46
+ { key: 'rank', label: '#', w: 3, align: 'right', prio: 0, render: (cv, x, y, w, _r, bg, m) => { cv.textR(x, y, w, String(m.index + 1), { fg: t.dim, bg }); } },
47
+ // task-1326: the coin image (2 cells, quadrant art or real pixels). Drops before the numbers when narrow.
48
+ ...(icon ? [{ key: 'img', label: '', w: COIN_ICON_W, prio: 2, render: (cv, x, y, w, r, bg) => { if (!r.skeleton)
49
+ icon(cv, x, y, w, r, bg); } }] : []),
50
+ { key: 'token', label: 'TOKEN', w: bp === 'compact' ? 12 : 22, prio: 0, flex: bp !== 'compact', render: (cv, x, y, w, r, bg, m) => {
51
+ const cx = cv.text(x, y, r.symbol, { fg: m.selected ? t.borderFocus : t.text, bg, b: true }, Math.min(10, w));
52
+ if (w > 14)
53
+ cv.text(cx + 1, y, r.name, muted(bg), w - (cx - x) - 1);
54
+ } },
55
+ { key: 'lp', label: 'LP', w: 6, prio: 4, render: (cv, x, y, w, r, bg) => { if (r.launchpad === 'unknown')
56
+ dash(cv, x, y, w, bg, 'left');
57
+ else
58
+ launchpadBadge(cv, x, y, r.launchpad, { on: bg }); } },
59
+ { key: 'age', label: 'AGE', w: 5, align: 'right', prio: 2, render: (cv, x, y, w, r, bg) => { if (r.ageMin === null)
60
+ dash(cv, x, y, w, bg);
61
+ else
62
+ cv.textR(x, y, w, fmtAge(r.ageMin), muted(bg)); } },
63
+ { key: 'price', label: 'PRICE', w: 11, align: 'right', prio: 0, render: (cv, x, y, w, r, bg) => { if (r.price === null)
64
+ dash(cv, x, y, w, bg);
65
+ else
66
+ cv.textR(x, y, w, fmtPrice(r.price), { fg: t.text, bg }); } },
67
+ { key: 'm5', label: '5M', w: 7, align: 'right', prio: 2, render: (cv, x, y, w, r, bg) => heatPct(cv, x, y, w, r.chg.m5, bg) },
68
+ { key: 'h1', label: '1H', w: 7, align: 'right', prio: 1, render: (cv, x, y, w, r, bg) => heatPct(cv, x, y, w, r.chg.h1, bg) },
69
+ { key: 'h6', label: '6H', w: 7, align: 'right', prio: 6, render: (cv, x, y, w, r, bg) => heatPct(cv, x, y, w, r.chg.h6, bg) },
70
+ { key: 'h24', label: '24H', w: 8, align: 'right', prio: 0, render: (cv, x, y, w, r, bg) => heatPct(cv, x, y, w, r.chg.h24, bg) },
71
+ { key: 'vol', label: 'VOL 24H', w: 9, align: 'right', prio: 0, render: (cv, x, y, w, r, bg) => { if (r.vol24 === null)
72
+ dash(cv, x, y, w, bg);
73
+ else
74
+ cv.textR(x, y, w, fmtUsd(r.vol24, { digits: 1 }), { fg: t.text, bg }); } },
75
+ { key: 'liq', label: 'LIQ', w: 8, align: 'right', prio: 1, render: (cv, x, y, w, r, bg) => { if (r.liq === null)
76
+ dash(cv, x, y, w, bg);
77
+ else
78
+ cv.textR(x, y, w, fmtUsd(r.liq, { digits: 1 }), { fg: r.liq !== null && r.liq < 50e3 ? t.warn : t.text, bg }); } },
79
+ { key: 'mcap', label: 'MCAP', w: mode === 'mcap' ? 9 : 8, align: 'right', prio: 0, render: (cv, x, y, w, r, bg) => {
80
+ // task-1366: in mcap mode a token without a supply shows its PRICE here, marked ᵖ in amber (the caption counts them)
81
+ if (r.mcap === null && mode === 'mcap' && r.price !== null) {
82
+ cv.textR(x, y, w, fmtPrice(r.price) + FALLBACK_MARK, { fg: t.warn, bg });
83
+ return;
84
+ }
85
+ if (r.mcap === null) {
86
+ dash(cv, x, y, w, bg);
87
+ return;
88
+ }
89
+ // sources disagree (D-06 rule): amber with a trailing ? so the plain render carries the doubt too
90
+ if (r.mcapFlagged)
91
+ cv.textR(x, y, w, fmtUsd(r.mcap, { digits: 1 }) + '?', { fg: t.warn, bg });
92
+ else
93
+ cv.textR(x, y, w, fmtUsd(r.mcap, { digits: 1 }), { fg: t.text, bg });
94
+ } },
95
+ { key: 'holders', label: 'HOLDERS', w: 8, align: 'right', prio: 2, render: (cv, x, y, w, r, bg) => { if (r.holders === null)
96
+ dash(cv, x, y, w, bg);
97
+ else
98
+ cv.textR(x, y, w, fmtNum(r.holders), { fg: t.text, bg }); } },
99
+ { key: 'org', label: 'ORG', w: 4, align: 'right', prio: 2, render: (cv, x, y, w, r, bg) => {
100
+ if (r.org === null) {
101
+ dash(cv, x, y, w, bg);
102
+ return;
103
+ }
104
+ cv.textR(x, y, w, String(Math.round(r.org)), { fg: r.org >= 70 ? t.up : r.org >= 40 ? t.warn : t.down, bg });
105
+ } },
106
+ // task-1361: p(rug) from the H1-v0.1 heuristic (screener/rug.ts). A model output: the panel caption names
107
+ // the model, the spotlight adds the data age. Dim when the row is stale; dashed when nothing is scoreable.
108
+ { key: 'rug', label: 'P(RUG)', w: 6, align: 'right', prio: 3, render: (cv, x, y, w, r, bg) => {
109
+ const p = rugP(r);
110
+ if (p === null) {
111
+ dash(cv, x, y, w, bg);
112
+ return;
113
+ }
114
+ cv.textR(x, y, w, fmtRugP(p), { fg: r.stale ? t.dim : p >= RUG_FLAG_THRESHOLD ? t.down : p >= 0.35 ? t.warn : t.text, bg });
115
+ } },
116
+ { key: 'txns', label: 'B/S 24H', w: 10, prio: 5, render: (cv, x, y, w, r, bg) => { if (r.buys === null || r.sells === null)
117
+ dash(cv, x, y, w, bg, 'left');
118
+ else
119
+ ratioBar(cv, x, y, w, r.buys, r.sells, { bg }); } },
120
+ { key: 'spark', label: '24H TREND', w: 16, prio: 1, render: (cv, x, y, w, r, bg) => sparkline(cv, x, y, w, r.spark, { bg }) },
121
+ { key: 'flags', label: 'FLAGS', w: 14, prio: 3, render: (cv, x, y, w, r, bg) => {
122
+ let cx = x;
123
+ // task-1315: lead with the LARP-detector verdict, but only for a mint we actually vetted.
124
+ const lb = larpBadge(larp?.get(r.mint)?.verdict, t);
125
+ if (lb && cx + lb.text.length + 2 <= x + w)
126
+ cx = badge(cv, cx, y, lb.text, lb.fg, { on: bg }) + 1;
127
+ for (const f of r.flags.slice(0, 2)) {
128
+ if (cx + f.length + 2 > x + w)
129
+ break;
130
+ cx = badge(cv, cx, y, f, f === 'MINT' || f === 'FREEZE' ? t.down : t.warn, { on: bg }) + 1;
131
+ }
132
+ if (!r.flags.length && !lb) {
133
+ // No flags is only "clean" when the audit actually came back; otherwise say so.
134
+ if (r.mintAuth === null && r.freezeAuth === null)
135
+ cv.text(x, y, '? unaudited', { fg: t.dim, bg });
136
+ else
137
+ cv.text(x, y, '✓ clean', { fg: mix(bg, t.up, 0.7), bg });
138
+ }
139
+ } },
140
+ ];
141
+ return promoteMcap(cols, mode); // task-1366: MCAP takes PRICE's slot in mcap mode
142
+ }
143
+ export function renderScreener(c, state) {
144
+ const t = c.theme;
145
+ const bp = breakpoint(c.w);
146
+ const sc = state.screener;
147
+ header(c, headerModel(state));
148
+ const top = 2, bottom = c.h - 1;
149
+ // task-1358: the spotlight width comes from the active workspace's solved layout (44 at ≥ 140 cols by default).
150
+ const sideW = layoutFor(state.custom, c.w, c.h).spotlightW;
151
+ const tableW = c.w - sideW - (sideW ? 1 : 0);
152
+ filterBar(c, sc, top, tableW);
153
+ const rows = screenerView(sc);
154
+ const sort = sortSpec(sc.sort.key);
155
+ const total = sc.rows.length;
156
+ const gated = gateCaption(sc.gates, Object.values(COLUMN_GATE));
157
+ const mode = priceModeOf(state);
158
+ const fallback = mode === 'mcap' ? fallbackCaption(fallbackCount(rows, (r) => r)) : null;
159
+ const sel0 = Math.min(state.selected || 0, Math.max(0, rows.length - 1));
160
+ // "N of M tokens" when a filter hides some; "M tokens" otherwise. When the feed scanned a wider
161
+ // candidate pool than it enriched into rows, trail the real pool size (task-1303). Row position
162
+ // trails so hundreds read at a glance.
163
+ const shown = rows.length === total ? `${total} tokens` : `${rows.length} of ${total} tokens`;
164
+ const count = (sc.pool ?? 0) > total ? `${shown} · pool ${sc.pool}` : shown;
165
+ const pos = rows.length > 0 ? `row ${sel0 + 1}/${rows.length}` : null;
166
+ // Ordered by priority: the box drops its caption whole when it overflows (canvas.box), so when the
167
+ // pool size (task-1303) makes the line too long we shed the low-value tail (position, then refresh
168
+ // age) rather than losing the pool/sort/gate facts. Budget mirrors box()'s title+caption fit test.
169
+ const segs = [
170
+ count,
171
+ `sorted by ${sort?.label ?? sc.sort.key}${sc.sort.dir === 'asc' ? ' asc' : ''}`,
172
+ gated || null,
173
+ fallback, // task-1366: rows whose MCAP cell is a price (supply unknown)
174
+ // task-1361: the P(RUG) column is a model output, so the panel says which model (sheds before the facts above).
175
+ sc.sort.key === 'rug' ? null : `p(rug) = ${RUG_MODEL_LABEL}`,
176
+ // The wide pool is still trickling in under the DexScreener budget (task-1303): tell the user the
177
+ // tail is slowing, not stalled. Ranks above the refresh age / row position so it survives a trim.
178
+ sc.slowing ? '◔ rate-limited, slowing down' : null,
179
+ `refreshed ${state.refreshedAgo || '2s'} ago`,
180
+ pos,
181
+ ].filter((s) => !!s);
182
+ const capBudget = tableW - ' SCREENER '.length - 8;
183
+ let caption = segs.join(' · ');
184
+ while (caption.length > capBudget && segs.length > 1) {
185
+ segs.pop();
186
+ caption = segs.join(' · ');
187
+ }
188
+ const box = c.box(0, top + 1, tableW, bottom - top - 1, { title: 'SCREENER', caption });
189
+ const sel = Math.min(state.selected || 0, Math.max(0, rows.length - 1));
190
+ const rowsVisible = box.h - 2;
191
+ const scroll = Math.max(0, Math.min(sel - rowsVisible + 2, rows.length - rowsVisible));
192
+ // task-1326: a 2-cell coin icon per row when images are on and the frame is not compact (never on fixtures).
193
+ const icon = imagesOn(state) && bp !== 'compact'
194
+ ? (cv, x, y, w, r, bg) => { drawCoin(cv, state, { x, y, w, h: 1, url: r.image, symbol: r.symbol, bg, prio: 1, mode: 'quad' }); }
195
+ : null;
196
+ table(c, box.x + 1, box.y, box.w - 2, box.h, {
197
+ columns: screenerColumns(c, bp, sc.gates, state.larpByMint ?? null, icon, mode), rows, selected: sel, scroll: Math.max(0, scroll), sortKey: sc.sort.key, sortDir: sc.sort.dir, bg: box.bg,
198
+ });
199
+ if (rows.length === 0)
200
+ emptyState(c, box, sc);
201
+ if (sideW) {
202
+ c.rainZone(tableW, top, 1, bottom - top, 1); // the gutter between table and spotlight
203
+ const row = rows[sel];
204
+ if (row)
205
+ spotlight(c, c.w - sideW, top, sideW, bottom - top, row, sc.gates, imagesOn(state) ? (cv, x, y, w, h) => { drawCoin(cv, state, { x, y, w, h, url: row.image, symbol: row.symbol, bg: c.theme.panel, prio: 2 }); } : null, state.now.getTime(), mode);
206
+ else
207
+ c.box(c.w - sideW, top, sideW, bottom - top, { title: 'SPOTLIGHT' });
208
+ }
209
+ if (sc.mode === 'sort')
210
+ sortMenu(c, sc, box);
211
+ const hints = sc.mode === 'filter'
212
+ ? [['⏎', 'apply'], ['esc', 'cancel'], ['ctrl+u', 'clear']]
213
+ : sc.mode === 'sort'
214
+ ? [['j/k', 'move'], ['⏎', 'pick / flip'], ['esc', 'close']]
215
+ : sc.query
216
+ ? [['j/k', 'move'], ['⏎', 'open'], ['g', 'go to'], ['/', 'filter'], ['\\', 'clear filter'], ['s', 'sort'], ['w', 'watch'], ['a', 'ask'], ['q', 'quit']]
217
+ : [['j/k', 'move'], ['PgUp/Dn·G', 'page'], ['[ ]', 'feed'], ['⏎', 'open'], ['g', 'go to'], ['/', 'filter'], ['s', 'sort'], ['w', 'watch'], ['a', 'ask'], ['q', 'quit']];
218
+ footer(c, hints, state.theme.name, isCalm(state.vfx));
219
+ }
220
+ /** Row 2: the `⌕` query (or the editor) on the left, presets on the right; the gap between them rains. */
221
+ function filterBar(c, sc, fy, tableW) {
222
+ const t = c.theme;
223
+ c.fill(0, fy, tableW, 1, ' ', { bg: t.bg });
224
+ let x = c.text(1, fy, '⌕ ', { fg: t.accent, bg: t.bg, b: true });
225
+ if (sc.mode === 'filter') {
226
+ const text = sc.edit.text;
227
+ x = c.text(x, fy, text, { fg: t.text, bg: t.bg }, Math.max(0, tableW - x - 2));
228
+ c.put(x, fy, '▏', { fg: t.accent, bg: t.bg, b: true, soft: true });
229
+ x += 1;
230
+ const err = sc.edit.error;
231
+ const msg = err ? `✗ ${err.error}` : text.trim() ? '✓ ⏎ to apply' : 'type a filter · fields: vol24 liq mcap age holders org … · tags: launchpad: flag: chain:';
232
+ const mx = x + 2;
233
+ if (mx < tableW - 8)
234
+ c.text(mx, fy, msg, { fg: err ? t.down : t.dim, bg: t.bg, i: !err }, tableW - mx - 1);
235
+ return;
236
+ }
237
+ if (sc.notice) {
238
+ x = c.text(x, fy, sc.notice, { fg: t.warn, bg: t.bg, i: true }, tableW - x - 2);
239
+ }
240
+ else {
241
+ x = c.text(x, fy, sc.query || DEFAULT_QUERY, { fg: sc.query ? t.text : t.muted, bg: t.bg, i: !sc.query }, tableW - x - 2);
242
+ }
243
+ // presets, right-aligned; disabled ones dim with their caption ('needs key', 'D-08', …). The
244
+ // task-1303 feed switcher can outgrow a narrow frame, so when the full set won't fit we window
245
+ // around the active preset — reading forward from it, counting each disabled preset's caption so
246
+ // the window never hides why a preset is off — and mark the trimmed sides with ‹ / › instead of
247
+ // dropping the whole bar.
248
+ const items = sc.presets.map((p) => ({ p, label: ` ${p.label} `, cap: (!p.available || p.approx) && p.caption ? `${p.caption} ` : '' }));
249
+ const avail = tableW - 2 - (x + 4);
250
+ const w = (it) => it.label.length + 1 + it.cap.length;
251
+ if (avail > 6 && items.length) {
252
+ let lo = 0, hi = items.length;
253
+ if (items.reduce((s, it) => s + w(it), 0) > avail) {
254
+ // Something is always hidden here, so budget for the trailing › (1 col); the leading ‹ (2 cols)
255
+ // only costs once we have actually trimmed the left (lo > 0), so account for it per step.
256
+ const budget = avail - 1;
257
+ const ai = Math.max(0, items.findIndex((it) => it.p.id === sc.preset));
258
+ lo = ai;
259
+ hi = ai + 1;
260
+ let used = w(items[ai]);
261
+ for (;;) {
262
+ const canR = hi < items.length && used + w(items[hi]) + (lo > 0 ? 2 : 0) <= budget;
263
+ const canL = lo > 0 && used + w(items[lo - 1]) + (lo - 1 > 0 ? 2 : 0) <= budget;
264
+ if (!canR && !canL)
265
+ break;
266
+ if (canR)
267
+ used += w(items[hi++]);
268
+ else
269
+ used += w(items[--lo]);
270
+ }
271
+ }
272
+ const win = items.slice(lo, hi);
273
+ const total = win.reduce((s, it) => s + w(it), 0) + (lo > 0 ? 2 : 0) + (hi < items.length ? 1 : 0);
274
+ let px = tableW - 2 - total;
275
+ if (px > x + 2) {
276
+ c.rainZone(x + 1, fy, px - x - 2, 1, 1);
277
+ if (lo > 0)
278
+ px = c.text(px, fy, '‹ ', { fg: t.dim, bg: t.bg });
279
+ for (const it of win) {
280
+ const active = it.p.id === sc.preset;
281
+ if (active)
282
+ px = c.text(px, fy, it.label, { fg: onAccent(t.borderFocus), bg: t.borderFocus, b: true }) + 1;
283
+ else if (it.p.available)
284
+ px = c.text(px, fy, it.label, { fg: t.muted, bg: t.bg }) + 1;
285
+ else
286
+ px = c.text(px, fy, it.label, { fg: t.dim, bg: t.bg, d: true }) + 1;
287
+ // An approximation preset is available but still shows its caption ('approx'), so the UI never
288
+ // implies the keyed signal; a disabled preset shows its reason ('needs key', 'D-08', …).
289
+ if (it.cap)
290
+ px = c.text(px - 1, fy, it.cap, { fg: t.dim, bg: t.bg, i: true }) + 1;
291
+ }
292
+ if (hi < items.length)
293
+ c.text(px, fy, '›', { fg: t.dim, bg: t.bg });
294
+ }
295
+ }
296
+ }
297
+ function emptyState(c, box, sc) {
298
+ const t = c.theme;
299
+ const y = box.y + Math.min(4, Math.floor(box.h / 2));
300
+ // task-1305 (M6): when the live feed has no rows, say *why* — connecting / offline / rate-limited —
301
+ // instead of an indefinite "waiting for the feed". A degraded color flags the two actionable states.
302
+ let msg;
303
+ let fg = t.muted;
304
+ if (sc.rows.length === 0) {
305
+ if (sc.source === 'fixtures')
306
+ msg = 'no rows';
307
+ else {
308
+ const cond = feedCondition(sc);
309
+ msg = cond.message;
310
+ if (cond.kind === 'offline' || cond.kind === 'ratelimited')
311
+ fg = t.warn;
312
+ }
313
+ }
314
+ else {
315
+ msg = `no tokens match "${sc.query}" · / to edit, ctrl+u then ⏎ to clear`;
316
+ }
317
+ // Keep the message legible: rain must not write into the spaces inside it (task-1305, QA M7).
318
+ c.noRain(box.x, y, box.w, 1);
319
+ c.textC(box.x, y, box.w, msg, { fg, bg: box.bg, i: true });
320
+ }
321
+ /** The `s` sort menu: a small centered list over the table. */
322
+ function sortMenu(c, sc, over) {
323
+ const t = c.theme;
324
+ const w = 30, h = SORT_KEYS.length + 2;
325
+ const x = over.x + Math.max(0, Math.floor((over.w - w) / 2));
326
+ const y = over.y + Math.max(0, Math.floor((over.h - h) / 2));
327
+ const box = c.box(x, y, w, h, { title: 'SORT BY', caption: '⏎ pick · again flips', focus: true, bg: t.panelAlt });
328
+ c.noRain(x, y, w, h);
329
+ SORT_KEYS.forEach((k, i) => {
330
+ const cur = k.key === sc.sort.key;
331
+ const hot = i === sc.sortCursor;
332
+ const bg = hot ? t.selBg : box.bg;
333
+ c.fill(box.x, box.y + i, box.w, 1, ' ', { bg });
334
+ c.text(box.x + 1, box.y + i, hot ? '▸' : ' ', { fg: t.accent, bg });
335
+ c.text(box.x + 3, box.y + i, k.label, { fg: cur ? t.accent : t.text, bg, b: cur });
336
+ const dir = cur ? (sc.sort.dir === 'desc' ? '▼' : '▲') : (k.defaultDir === 'desc' ? '▽' : '△');
337
+ c.textR(box.x + 1, box.y + i, box.w - 2, dir, { fg: cur ? t.accent : t.dim, bg });
338
+ });
339
+ }
340
+ /** Right-hand spotlight card for the selected row (wide layouts only). */
341
+ export function spotlight(c, x, y, w, h, tk, gates = null, hero = null, now = null, mode = 'price') {
342
+ const t = c.theme;
343
+ const off = (id) => !!gates && gates[id]?.available === false;
344
+ const gateWord = (id) => (gates?.[id]?.state === 'needs-key' ? 'needs key' : gates?.[id]?.state === 'disabled' ? 'off' : 'degraded');
345
+ const box = c.box(x, y, w, h, { title: `$${tk.symbol}`, caption: shortAddr(tk.mint, 5), focus: true });
346
+ const bg = box.bg;
347
+ let cy = box.y;
348
+ // task-1326: a 6×3 coin image in the card's top-right corner (name / price / gap rows); text keeps clear of it.
349
+ const heroW = hero ? 6 : 0;
350
+ c.text(box.x + 1, cy, tk.name, { fg: t.muted, bg }, box.w - 2 - (heroW ? heroW + 1 : 0));
351
+ cy += 1;
352
+ // task-1366: the headline number follows the price mode (mcap first, the price beside it; a price + note without a supply)
353
+ const head = priceCell(mode, tk.price, tk.mcap);
354
+ let cx = c.text(box.x + 1, cy, head.text, { fg: head.fallback ? t.warn : t.text, bg, b: true });
355
+ if (head.kind === 'mcap' && tk.price !== null)
356
+ cx = c.text(cx + 1, cy, `(${fmtPrice(tk.price)})`, { fg: t.dim, bg });
357
+ cx = c.text(cx + 1, cy, fmtPctDir(tk.chg.h24), pctStyle(c, tk.chg.h24, bg));
358
+ c.text(cx + 1, cy, '24h', { fg: t.dim, bg });
359
+ if (head.fallback)
360
+ c.text(box.x + 1, cy + 1, MCAP_FALLBACK_NOTE, { fg: t.warn, bg, i: true }, box.w - 2 - (heroW ? heroW + 1 : 0));
361
+ if (tk.launchpad !== 'unknown')
362
+ launchpadBadge(c, box.x + box.w - 8 - (heroW ? heroW + 1 : 0), cy, tk.launchpad, { on: bg });
363
+ if (hero)
364
+ hero(c, box.x + box.w - 1 - heroW, box.y, heroW, 3);
365
+ cy += 2;
366
+ sparkArea(c, box.x + 1, cy, box.w - 2, 4, tk.spark, { bg });
367
+ cy += 5;
368
+ const holdersOff = off('holders');
369
+ const orgOff = off('org');
370
+ const holdersSub = tk.holders24 !== null ? fmtDeltaDir(tk.holders24)
371
+ : tk.holdersChgPct !== null ? fmtPctDir(tk.holdersChgPct) + ' 24h' : undefined;
372
+ cy += statGrid(c, box.x + 1, cy, box.w - 2, [
373
+ tk.mcapFlagged
374
+ ? { label: 'mcap', value: fmtUsd(tk.mcap, { digits: 1 }) + '?', sub: 'sources differ', style: { fg: t.warn, bg, b: true } }
375
+ : { label: 'mcap', value: fmtUsd(tk.mcap, { digits: 1 }) },
376
+ { label: 'liquidity', value: fmtUsd(tk.liq, { digits: 1 }) },
377
+ { label: 'vol 24h', value: fmtUsd(tk.vol24, { digits: 1 }) },
378
+ holdersOff
379
+ ? { label: 'holders', value: '—', sub: gateWord('holders'), style: { fg: t.dim, bg } }
380
+ : { label: 'holders', value: fmtNum(tk.holders), sub: tk.holders === null ? undefined : holdersSub, style: tk.holders === null ? { fg: t.dim, bg } : undefined },
381
+ orgOff || tk.org === null
382
+ ? { label: 'organic', value: '—', sub: orgOff ? gateWord('org') : undefined, style: { fg: t.dim, bg } }
383
+ : { label: 'organic', value: `${Math.round(tk.org)}/100`, style: { fg: tk.org >= 70 ? t.up : tk.org >= 40 ? t.warn : t.down, b: true } },
384
+ tk.top10 === null
385
+ ? { label: 'top 10', value: '—', sub: holdersOff ? gateWord('holders') : 'no audit', style: { fg: t.dim, bg } }
386
+ : { label: 'top 10', value: `${tk.top10.toFixed(1)}%`, style: { fg: tk.top10 > 30 ? t.warn : t.text, b: true } },
387
+ ], { bg, cols: 2 });
388
+ cy += 1;
389
+ c.text(box.x + 1, cy, 'BUYS / SELLS 24H', { fg: t.dim, bg });
390
+ cy += 1;
391
+ if (tk.buys !== null && tk.sells !== null && !off('bs')) {
392
+ ratioBar(c, box.x + 1, cy, box.w - 2, tk.buys, tk.sells, { bg });
393
+ cy += 1;
394
+ c.text(box.x + 1, cy, '▲' + fmtNum(tk.buys), { fg: t.up, bg });
395
+ c.textR(box.x + 1, cy, box.w - 2, '▼' + fmtNum(tk.sells), { fg: t.down, bg });
396
+ }
397
+ else {
398
+ c.text(box.x + 1, cy, '—', { fg: t.dim, bg });
399
+ cy += 1;
400
+ c.text(box.x + 1, cy, off('bs') ? gateWord('bs') : 'no txn counts', { fg: t.dim, bg });
401
+ }
402
+ cy += 2;
403
+ c.text(box.x + 1, cy, 'SAFETY', { fg: t.dim, bg });
404
+ cy += 1;
405
+ // null = the keyless sources did not say: shown as an open circle, never as a pass or a fail
406
+ const checks = [
407
+ ['mint authority', tk.mintAuth === null ? null : !tk.mintAuth],
408
+ ['freeze authority', tk.freezeAuth === null ? null : !tk.freezeAuth],
409
+ [tk.lpLocked === null ? 'LP locked' : `LP locked ${Math.round(tk.lpLocked)}%`, tk.lpLocked === null ? null : tk.lpLocked >= 90],
410
+ [tk.dev === null ? 'dev holds' : `dev holds ${tk.dev.toFixed(2)}%`, tk.dev === null ? null : tk.dev < 3],
411
+ [tk.bundled === null ? 'bundled' : `bundled ${tk.bundled.toFixed(0)}%`, tk.bundled === null ? null : tk.bundled < 10],
412
+ [tk.snipers === null ? 'snipers' : `snipers ${tk.snipers}`, tk.snipers === null ? null : tk.snipers < 5],
413
+ ];
414
+ for (const [label, ok] of checks) {
415
+ if (cy >= box.y + box.h - 5)
416
+ break;
417
+ if (ok === null) {
418
+ c.text(box.x + 1, cy, '○', { fg: t.dim, bg });
419
+ const lx = c.text(box.x + 3, cy, label, { fg: t.dim, bg }, box.w - 4);
420
+ c.text(lx + 1, cy, 'needs key', { fg: t.dim, bg, i: true }, Math.max(0, box.x + box.w - 1 - (lx + 1)));
421
+ }
422
+ else {
423
+ c.text(box.x + 1, cy, ok ? '✓' : '✗', { fg: ok ? t.up : t.down, bg, b: true });
424
+ c.text(box.x + 3, cy, label, { fg: ok ? t.text : t.warn, bg }, box.w - 4);
425
+ }
426
+ cy += 1;
427
+ }
428
+ // task-1361: p(rug) is a MODEL OUTPUT, so the card says so: value, model id + kind, and how old the scored data is.
429
+ const ay = box.y + box.h - 3;
430
+ if (cy < ay - 3) {
431
+ const p = rugP(tk);
432
+ const age = now === null ? null : rugDataAge(tk, now);
433
+ if (p === null) {
434
+ c.text(box.x + 1, cy, '○', { fg: t.dim, bg });
435
+ c.text(box.x + 3, cy, `p(rug) — · ${RUG_MODEL_LABEL}`, { fg: t.dim, bg }, box.w - 4);
436
+ c.text(box.x + 3, cy + 1, 'model output · no data to score', { fg: t.dim, bg, i: true }, box.w - 4);
437
+ }
438
+ else {
439
+ const risky = p >= RUG_FLAG_THRESHOLD;
440
+ c.text(box.x + 1, cy, risky ? '✗' : p >= 0.35 ? '△' : '✓', { fg: risky ? t.down : p >= 0.35 ? t.warn : t.up, bg, b: true });
441
+ const vx = c.text(box.x + 3, cy, `p(rug) ${fmtRugP(p)}`, { fg: risky ? t.down : p >= 0.35 ? t.warn : t.text, bg, b: true });
442
+ c.text(vx + 1, cy, `· ${RUG_MODEL_LABEL}`, { fg: t.dim, bg, i: true }, Math.max(0, box.x + box.w - 1 - (vx + 1)));
443
+ c.text(box.x + 3, cy + 1, `model output · data ${age ? `${age} old` : 'age unknown'}`, { fg: t.dim, bg, i: true }, box.w - 4);
444
+ }
445
+ cy += 2;
446
+ }
447
+ // the blank middle of the card is a quiet rain zone
448
+ if (ay - 2 > cy)
449
+ c.rainZone(box.x + 1, cy + 1, box.w - 2, ay - 2 - cy, 2);
450
+ // AI one-liner (coral is the analyst's voice: title and verdict, VISUAL-V2 §2)
451
+ c.hline(box.x, ay - 1, box.w, { fg: mix(t.border, t.ai, 0.4), bg, soft: true });
452
+ c.text(box.x + 1, ay, '✦ claude', { fg: t.ai, bg, b: true });
453
+ c.heading(box.x + 1, ay, 8);
454
+ if (tk.aiNote)
455
+ c.text(box.x + 1, ay + 1, tk.aiNote, { fg: t.ai, bg }, box.w - 2);
456
+ else
457
+ c.text(box.x + 1, ay + 1, 'press a for a claude analysis', { fg: t.dim, bg, i: true }, box.w - 2);
458
+ c.textR(box.x + 1, ay, box.w - 2, 'a → full analysis', { fg: t.dim, bg });
459
+ }