dexterm-cli 0.0.0-stage → 0.1.1

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 (349) 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 +379 -0
  4. package/dist/packages/agents/src/executor.js +177 -0
  5. package/dist/packages/agents/src/gate.js +176 -0
  6. package/dist/packages/agents/src/identity.js +179 -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 +114 -0
  12. package/dist/packages/agents/src/tools.js +69 -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 +67 -0
  18. package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +64 -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 +303 -0
  26. package/dist/packages/cli/src/agent-roster.js +26 -0
  27. package/dist/packages/cli/src/analyst/backend.js +526 -0
  28. package/dist/packages/cli/src/analyst/results.js +38 -0
  29. package/dist/packages/cli/src/analyst/session.js +319 -0
  30. package/dist/packages/cli/src/audit.js +51 -0
  31. package/dist/packages/cli/src/backtest-cmd.js +149 -0
  32. package/dist/packages/cli/src/boot-state.js +62 -0
  33. package/dist/packages/cli/src/custom.js +261 -0
  34. package/dist/packages/cli/src/display.js +191 -0
  35. package/dist/packages/cli/src/doctor.js +361 -0
  36. package/dist/packages/cli/src/feed.js +634 -0
  37. package/dist/packages/cli/src/fixtures.js +275 -0
  38. package/dist/packages/cli/src/follow-cmd.js +221 -0
  39. package/dist/packages/cli/src/images-config.js +59 -0
  40. package/dist/packages/cli/src/index.js +31 -0
  41. package/dist/packages/cli/src/kb-skill-cmd.js +240 -0
  42. package/dist/packages/cli/src/larp-record.js +21 -0
  43. package/dist/packages/cli/src/live/copy.js +80 -0
  44. package/dist/packages/cli/src/live/flow.js +233 -0
  45. package/dist/packages/cli/src/live/index.js +509 -0
  46. package/dist/packages/cli/src/live/intel.js +227 -0
  47. package/dist/packages/cli/src/live/newpairs.js +334 -0
  48. package/dist/packages/cli/src/live/screens/newpairs-live.js +219 -0
  49. package/dist/packages/cli/src/live/screens/token-live.js +459 -0
  50. package/dist/packages/cli/src/live/screens/watchlist-live.js +149 -0
  51. package/dist/packages/cli/src/live/token.js +585 -0
  52. package/dist/packages/cli/src/live/trade.js +929 -0
  53. package/dist/packages/cli/src/live/watchlist.js +178 -0
  54. package/dist/packages/cli/src/main.js +5583 -0
  55. package/dist/packages/cli/src/narrative-cmd.js +76 -0
  56. package/dist/packages/cli/src/pack-cmd.js +323 -0
  57. package/dist/packages/cli/src/plan-cmd.js +278 -0
  58. package/dist/packages/cli/src/popup.js +810 -0
  59. package/dist/packages/cli/src/rules-cmd.js +131 -0
  60. package/dist/packages/cli/src/rules.js +339 -0
  61. package/dist/packages/cli/src/scan/command.js +57 -0
  62. package/dist/packages/cli/src/scan/engine.js +419 -0
  63. package/dist/packages/cli/src/scan/observe.js +64 -0
  64. package/dist/packages/cli/src/scan/ui.js +98 -0
  65. package/dist/packages/cli/src/screener/filter.js +241 -0
  66. package/dist/packages/cli/src/screener/presets.js +80 -0
  67. package/dist/packages/cli/src/screener/rows.js +146 -0
  68. package/dist/packages/cli/src/screener/rug.js +50 -0
  69. package/dist/packages/cli/src/screener/sort.js +52 -0
  70. package/dist/packages/cli/src/screener/view.js +183 -0
  71. package/dist/packages/cli/src/screens/analyst-session.js +343 -0
  72. package/dist/packages/cli/src/screens/analyst.js +326 -0
  73. package/dist/packages/cli/src/screens/boot.js +74 -0
  74. package/dist/packages/cli/src/screens/bubbles.js +255 -0
  75. package/dist/packages/cli/src/screens/coin.js +33 -0
  76. package/dist/packages/cli/src/screens/follows.js +201 -0
  77. package/dist/packages/cli/src/screens/kol.js +90 -0
  78. package/dist/packages/cli/src/screens/narrative.js +90 -0
  79. package/dist/packages/cli/src/screens/newpairs.js +150 -0
  80. package/dist/packages/cli/src/screens/overlay.js +257 -0
  81. package/dist/packages/cli/src/screens/plans.js +126 -0
  82. package/dist/packages/cli/src/screens/positions.js +176 -0
  83. package/dist/packages/cli/src/screens/rules.js +170 -0
  84. package/dist/packages/cli/src/screens/screener.js +459 -0
  85. package/dist/packages/cli/src/screens/social-panel.js +107 -0
  86. package/dist/packages/cli/src/screens/ticket.js +342 -0
  87. package/dist/packages/cli/src/screens/token.js +312 -0
  88. package/dist/packages/cli/src/screens/trending.js +181 -0
  89. package/dist/packages/cli/src/screens/watchlist.js +104 -0
  90. package/dist/packages/cli/src/social/demo.js +96 -0
  91. package/dist/packages/cli/src/social/view.js +250 -0
  92. package/dist/packages/cli/src/state.js +463 -0
  93. package/dist/packages/cli/src/theme-cmd.js +126 -0
  94. package/dist/packages/cli/src/trade.js +319 -0
  95. package/dist/packages/cli/src/voice-hud.js +42 -0
  96. package/dist/packages/cli/src/wallet-cmd.js +440 -0
  97. package/dist/packages/cli/src/wallet-command.js +31 -0
  98. package/dist/packages/cli/src/widgets.js +208 -0
  99. package/dist/packages/cli/src/workspace-cmd.js +68 -0
  100. package/dist/packages/cli/src/x-cmd.js +154 -0
  101. package/dist/packages/custom/src/config.js +213 -0
  102. package/dist/packages/custom/src/index.js +10 -0
  103. package/dist/packages/custom/src/keymap.js +107 -0
  104. package/dist/packages/custom/src/layout.js +235 -0
  105. package/dist/packages/custom/src/packs.js +633 -0
  106. package/dist/packages/custom/src/rules.js +704 -0
  107. package/dist/packages/custom/src/schema.js +515 -0
  108. package/dist/packages/custom/src/themes.js +176 -0
  109. package/dist/packages/custom/src/widget-html.js +222 -0
  110. package/dist/packages/custom/src/workspace.js +141 -0
  111. package/dist/packages/data/src/analyst-actions.js +133 -0
  112. package/dist/packages/data/src/budget.js +88 -0
  113. package/dist/packages/data/src/cache.js +142 -0
  114. package/dist/packages/data/src/candles.js +168 -0
  115. package/dist/packages/data/src/clock.js +64 -0
  116. package/dist/packages/data/src/credits.js +137 -0
  117. package/dist/packages/data/src/detail.js +97 -0
  118. package/dist/packages/data/src/fast-lane.js +321 -0
  119. package/dist/packages/data/src/fixtures.js +135 -0
  120. package/dist/packages/data/src/health.js +245 -0
  121. package/dist/packages/data/src/http.js +277 -0
  122. package/dist/packages/data/src/images/cache.js +141 -0
  123. package/dist/packages/data/src/images/decode-worker.js +28 -0
  124. package/dist/packages/data/src/images/decode.js +81 -0
  125. package/dist/packages/data/src/images/fetch.js +162 -0
  126. package/dist/packages/data/src/images/gif.js +214 -0
  127. package/dist/packages/data/src/images/index.js +10 -0
  128. package/dist/packages/data/src/images/jpeg.js +544 -0
  129. package/dist/packages/data/src/images/png.js +310 -0
  130. package/dist/packages/data/src/images/service.js +334 -0
  131. package/dist/packages/data/src/images/types.js +27 -0
  132. package/dist/packages/data/src/index.js +208 -0
  133. package/dist/packages/data/src/intel/helius-history.js +59 -0
  134. package/dist/packages/data/src/intel/holder-class.js +125 -0
  135. package/dist/packages/data/src/intel/rules.js +421 -0
  136. package/dist/packages/data/src/intel/scan.js +170 -0
  137. package/dist/packages/data/src/larp/evidence.js +288 -0
  138. package/dist/packages/data/src/larp/links.js +137 -0
  139. package/dist/packages/data/src/larp/robots.js +90 -0
  140. package/dist/packages/data/src/larp/safe-fetch.js +389 -0
  141. package/dist/packages/data/src/limiter.js +341 -0
  142. package/dist/packages/data/src/model.js +12 -0
  143. package/dist/packages/data/src/paper.js +331 -0
  144. package/dist/packages/data/src/rpc-config.js +126 -0
  145. package/dist/packages/data/src/rpc-pool.js +166 -0
  146. package/dist/packages/data/src/social/chain.js +83 -0
  147. package/dist/packages/data/src/social/dedupe.js +205 -0
  148. package/dist/packages/data/src/social/feed.js +462 -0
  149. package/dist/packages/data/src/social/firewall.js +110 -0
  150. package/dist/packages/data/src/social/index.js +11 -0
  151. package/dist/packages/data/src/social/kol.js +269 -0
  152. package/dist/packages/data/src/social/model.js +11 -0
  153. package/dist/packages/data/src/social/rank.js +184 -0
  154. package/dist/packages/data/src/social/rss.js +113 -0
  155. package/dist/packages/data/src/social/summary.js +104 -0
  156. package/dist/packages/data/src/social/venues.js +186 -0
  157. package/dist/packages/data/src/sources/dexscreener.js +170 -0
  158. package/dist/packages/data/src/sources/geckoterminal.js +133 -0
  159. package/dist/packages/data/src/sources/goplus.js +161 -0
  160. package/dist/packages/data/src/sources/jupiter-swap.js +236 -0
  161. package/dist/packages/data/src/sources/jupiter.js +198 -0
  162. package/dist/packages/data/src/sources/pda.js +113 -0
  163. package/dist/packages/data/src/sources/pump-curve.js +58 -0
  164. package/dist/packages/data/src/sources/pumpportal.js +247 -0
  165. package/dist/packages/data/src/sources/rugcheck.js +71 -0
  166. package/dist/packages/data/src/sources/solana-rpc.js +299 -0
  167. package/dist/packages/data/src/sources/solana-ws.js +287 -0
  168. package/dist/packages/data/src/store.js +289 -0
  169. package/dist/packages/data/src/trade/bridge.js +464 -0
  170. package/dist/packages/data/src/trade/caps.js +323 -0
  171. package/dist/packages/data/src/trade/copy/auto.js +385 -0
  172. package/dist/packages/data/src/trade/copy/follows.js +103 -0
  173. package/dist/packages/data/src/trade/copy/index.js +9 -0
  174. package/dist/packages/data/src/trade/copy/mirror.js +91 -0
  175. package/dist/packages/data/src/trade/copy/stats.js +120 -0
  176. package/dist/packages/data/src/trade/copy/tracker.js +158 -0
  177. package/dist/packages/data/src/trade/copy/trades.js +87 -0
  178. package/dist/packages/data/src/trade/copy/types.js +6 -0
  179. package/dist/packages/data/src/trade/executor.js +933 -0
  180. package/dist/packages/data/src/trade/kill-switch.js +47 -0
  181. package/dist/packages/data/src/trade/link-store.js +52 -0
  182. package/dist/packages/data/src/trade/plans/confirm.js +157 -0
  183. package/dist/packages/data/src/trade/plans/engine.js +784 -0
  184. package/dist/packages/data/src/trade/plans/executor-live.js +220 -0
  185. package/dist/packages/data/src/trade/plans/executor-paper.js +44 -0
  186. package/dist/packages/data/src/trade/plans/executor.js +11 -0
  187. package/dist/packages/data/src/trade/plans/index.js +12 -0
  188. package/dist/packages/data/src/trade/plans/journal.js +77 -0
  189. package/dist/packages/data/src/trade/plans/mandate.js +129 -0
  190. package/dist/packages/data/src/trade/plans/plan.js +179 -0
  191. package/dist/packages/data/src/trade/plans/service.js +168 -0
  192. package/dist/packages/data/src/trade/plans/types.js +6 -0
  193. package/dist/packages/data/src/trade/rpc-exec.js +229 -0
  194. package/dist/packages/data/src/trade/signer/client.js +197 -0
  195. package/dist/packages/data/src/trade/signer/fast-wallet.js +154 -0
  196. package/dist/packages/data/src/trade/signer/host.js +97 -0
  197. package/dist/packages/data/src/trade/signer/main.js +121 -0
  198. package/dist/packages/data/src/trade/signer/probe-builder.js +85 -0
  199. package/dist/packages/data/src/trade/signer/protocol.js +92 -0
  200. package/dist/packages/data/src/trade/signer/service.js +400 -0
  201. package/dist/packages/data/src/trade/signer/sol-usd-feed.js +98 -0
  202. package/dist/packages/data/src/trade/sol-usd.js +86 -0
  203. package/dist/packages/data/src/trade/swap-build.js +108 -0
  204. package/dist/packages/data/src/trade/token-delta.js +201 -0
  205. package/dist/packages/data/src/trade/tx-decode.js +632 -0
  206. package/dist/packages/data/src/util.js +24 -0
  207. package/dist/packages/data/src/watchlist.js +176 -0
  208. package/dist/packages/data/src/x/api.js +256 -0
  209. package/dist/packages/data/src/x/budget.js +66 -0
  210. package/dist/packages/data/src/x/config.js +49 -0
  211. package/dist/packages/data/src/x/index.js +10 -0
  212. package/dist/packages/data/src/x/mock-server.js +217 -0
  213. package/dist/packages/data/src/x/oauth.js +242 -0
  214. package/dist/packages/data/src/x/pkce.js +21 -0
  215. package/dist/packages/data/src/x/source.js +285 -0
  216. package/dist/packages/data/src/x/token-store.js +134 -0
  217. package/dist/packages/data/src/x/transport.js +71 -0
  218. package/dist/packages/data/vendor/solana-web3/LICENSE +20 -0
  219. package/dist/packages/data/vendor/solana-web3/NOTICE.md +13 -0
  220. package/dist/packages/data/vendor/solana-web3/index.iife.min.js +20 -0
  221. package/dist/packages/mcp/assets/cases.json +1194 -0
  222. package/dist/packages/mcp/assets/kb/entries/cabal-and-coordinated-launches.md +28 -0
  223. package/dist/packages/mcp/assets/kb/entries/common-rug-patterns.md +40 -0
  224. package/dist/packages/mcp/assets/kb/entries/dbc-mechanics.md +30 -0
  225. package/dist/packages/mcp/assets/kb/entries/dev-wallet-behaviour.md +33 -0
  226. package/dist/packages/mcp/assets/kb/entries/glossary.md +35 -0
  227. package/dist/packages/mcp/assets/kb/entries/holder-concentration.md +30 -0
  228. package/dist/packages/mcp/assets/kb/entries/jito-bundles.md +29 -0
  229. package/dist/packages/mcp/assets/kb/entries/launchlab-curve.md +31 -0
  230. package/dist/packages/mcp/assets/kb/entries/lp-burn-vs-lock.md +31 -0
  231. package/dist/packages/mcp/assets/kb/entries/metadata-and-copycats.md +29 -0
  232. package/dist/packages/mcp/assets/kb/entries/mint-freeze-authority.md +31 -0
  233. package/dist/packages/mcp/assets/kb/entries/narrative-lifecycle.md +32 -0
  234. package/dist/packages/mcp/assets/kb/entries/pump-bonding-curve.md +33 -0
  235. package/dist/packages/mcp/assets/kb/entries/pumpswap-migration.md +29 -0
  236. package/dist/packages/mcp/assets/kb/entries/sniping-and-snipers.md +29 -0
  237. package/dist/packages/mcp/assets/kb/entries/token2022-extensions-risk.md +32 -0
  238. package/dist/packages/mcp/assets/kb/entries/wash-trading-fake-volume.md +33 -0
  239. package/dist/packages/mcp/assets/skills/rug-check.md +52 -0
  240. package/dist/packages/mcp/assets/skills/token-dossier.md +57 -0
  241. package/dist/packages/mcp/assets/skills/trench-scan.md +40 -0
  242. package/dist/packages/mcp/bin/dexterm-mcp.mjs +16 -0
  243. package/dist/packages/mcp/src/dossier.js +78 -0
  244. package/dist/packages/mcp/src/firewall.js +21 -0
  245. package/dist/packages/mcp/src/index.js +22 -0
  246. package/dist/packages/mcp/src/server.js +849 -0
  247. package/dist/packages/mcp/src/user-content.js +434 -0
  248. package/dist/packages/packs/src/backtest.js +183 -0
  249. package/dist/packages/packs/src/digest.js +95 -0
  250. package/dist/packages/packs/src/firewall.js +57 -0
  251. package/dist/packages/packs/src/index.js +19 -0
  252. package/dist/packages/packs/src/larp-eval.js +100 -0
  253. package/dist/packages/packs/src/narrative.js +309 -0
  254. package/dist/packages/packs/src/reason.js +135 -0
  255. package/dist/packages/packs/src/rug-model.js +137 -0
  256. package/dist/packages/packs/src/schema.js +54 -0
  257. package/dist/packages/packs/src/token-dossier.js +174 -0
  258. package/dist/packages/packs/src/verdicts.js +17 -0
  259. package/dist/packages/packs/src/wallet-dossier.js +105 -0
  260. package/dist/packages/popup/src/config.js +71 -0
  261. package/dist/packages/popup/src/dock.js +271 -0
  262. package/dist/packages/popup/src/host.js +210 -0
  263. package/dist/packages/popup/src/index.js +12 -0
  264. package/dist/packages/popup/src/loopback.js +53 -0
  265. package/dist/packages/popup/src/open.js +183 -0
  266. package/dist/packages/popup/src/pages/bubbles-lib.js +160 -0
  267. package/dist/packages/popup/src/pages/bubbles.css +23 -0
  268. package/dist/packages/popup/src/pages/bubbles.html +36 -0
  269. package/dist/packages/popup/src/pages/bubbles.js +214 -0
  270. package/dist/packages/popup/src/pages/chart-lib.js +262 -0
  271. package/dist/packages/popup/src/pages/chart.css +50 -0
  272. package/dist/packages/popup/src/pages/chart.html +56 -0
  273. package/dist/packages/popup/src/pages/chart.js +554 -0
  274. package/dist/packages/popup/src/pages/common.css +34 -0
  275. package/dist/packages/popup/src/pages/common.js +187 -0
  276. package/dist/packages/popup/src/pages/feeds.html +20 -0
  277. package/dist/packages/popup/src/pages/feeds.js +77 -0
  278. package/dist/packages/popup/src/pages/narrative.html +20 -0
  279. package/dist/packages/popup/src/pages/narrative.js +61 -0
  280. package/dist/packages/popup/src/pages/plans.css +30 -0
  281. package/dist/packages/popup/src/pages/plans.html +26 -0
  282. package/dist/packages/popup/src/pages/plans.js +82 -0
  283. package/dist/packages/popup/src/pages/roster.css +41 -0
  284. package/dist/packages/popup/src/pages/roster.html +25 -0
  285. package/dist/packages/popup/src/pages/roster.js +75 -0
  286. package/dist/packages/popup/src/pages/token.html +20 -0
  287. package/dist/packages/popup/src/pages/token.js +112 -0
  288. package/dist/packages/popup/src/pages/widget-sdk.js +224 -0
  289. package/dist/packages/popup/src/pages/widget.css +5 -0
  290. package/dist/packages/popup/src/pages/widget.html +21 -0
  291. package/dist/packages/popup/src/pages/widget.js +85 -0
  292. package/dist/packages/popup/src/protocol.js +31 -0
  293. package/dist/packages/popup/src/selftest.js +156 -0
  294. package/dist/packages/popup/src/server.js +582 -0
  295. package/dist/packages/popup/src/sinkproxy.js +167 -0
  296. package/dist/packages/popup/src/widget-html.js +231 -0
  297. package/dist/packages/popup/vendor/lightweight-charts/LICENSE +201 -0
  298. package/dist/packages/popup/vendor/lightweight-charts/NOTICE +2 -0
  299. package/dist/packages/popup/vendor/lightweight-charts/README.md +30 -0
  300. package/dist/packages/popup/vendor/lightweight-charts/VERSION +1 -0
  301. package/dist/packages/popup/vendor/lightweight-charts/lightweight-charts.standalone.production.js +7 -0
  302. package/dist/packages/recorder/bin/dexterm-recorder.mjs +11 -0
  303. package/dist/packages/recorder/src/cli.js +113 -0
  304. package/dist/packages/recorder/src/export.js +71 -0
  305. package/dist/packages/recorder/src/index.js +20 -0
  306. package/dist/packages/recorder/src/labels.js +151 -0
  307. package/dist/packages/recorder/src/open.js +29 -0
  308. package/dist/packages/recorder/src/recorder.js +553 -0
  309. package/dist/packages/recorder/src/schedule.js +43 -0
  310. package/dist/packages/recorder/src/schema.js +135 -0
  311. package/dist/packages/recorder/src/snapshot.js +191 -0
  312. package/dist/packages/recorder/src/sweep.js +52 -0
  313. package/dist/packages/recorder/src/verdicts.js +52 -0
  314. package/dist/packages/tui/src/bubbles.js +415 -0
  315. package/dist/packages/tui/src/canvas.js +370 -0
  316. package/dist/packages/tui/src/chrome.js +85 -0
  317. package/dist/packages/tui/src/color.js +133 -0
  318. package/dist/packages/tui/src/flush.js +62 -0
  319. package/dist/packages/tui/src/fmt.js +188 -0
  320. package/dist/packages/tui/src/fx-trade.js +278 -0
  321. package/dist/packages/tui/src/fx.js +527 -0
  322. package/dist/packages/tui/src/images/blocks.js +141 -0
  323. package/dist/packages/tui/src/images/coin.js +22 -0
  324. package/dist/packages/tui/src/images/detect.js +140 -0
  325. package/dist/packages/tui/src/images/index.js +7 -0
  326. package/dist/packages/tui/src/images/layer.js +146 -0
  327. package/dist/packages/tui/src/images/pixels.js +157 -0
  328. package/dist/packages/tui/src/images/protocols.js +87 -0
  329. package/dist/packages/tui/src/index.js +15 -0
  330. package/dist/packages/tui/src/keymap.js +355 -0
  331. package/dist/packages/tui/src/layout.js +11 -0
  332. package/dist/packages/tui/src/sound.js +177 -0
  333. package/dist/packages/tui/src/theme.js +182 -0
  334. package/dist/packages/tui/src/widgets.js +598 -0
  335. package/dist/packages/voice/src/controller.js +395 -0
  336. package/dist/packages/voice/src/doctor.js +63 -0
  337. package/dist/packages/voice/src/engines.js +44 -0
  338. package/dist/packages/voice/src/index.js +16 -0
  339. package/dist/packages/voice/src/router.js +502 -0
  340. package/dist/packages/voice/src/stt/apple-helper.swift.js +103 -0
  341. package/dist/packages/voice/src/stt/apple.js +179 -0
  342. package/dist/packages/voice/src/stt/fake.js +34 -0
  343. package/dist/packages/voice/src/stt/whisper.js +102 -0
  344. package/dist/packages/voice/src/summary.js +44 -0
  345. package/dist/packages/voice/src/tts/fake.js +30 -0
  346. package/dist/packages/voice/src/tts/say.js +96 -0
  347. package/dist/packages/voice/src/types.js +3 -0
  348. package/package.json +42 -4
  349. package/README.md +0 -3
@@ -0,0 +1,459 @@
1
+ // dexterm — live TOKEN screen (task-1298). Same layout as screens/token.ts (the fixture/parity renderer)
2
+ // over a TokenView: every number comes from a source named in the panel caption; nulls draw as `—`;
3
+ // a panel whose source failed says so instead of showing anything. The analyst panel is a labelled
4
+ // placeholder until the Claude lane wires `claude -p` (task-1293 skills are ready).
5
+ import { Canvas, candleChart, statGrid, bar, badge, launchpadBadge, pctStyle, tradeTape, flowPanel, fmtUsd, fmtPrice, fmtMcap, fmtAge, fmtNum, fmtPct, shortAddr, wrap, header, footer, breakpoint, mix, isCalm, } from '../../../../tui/src/index.js';
6
+ import { headerModel } from "../../state.js";
7
+ import { panelCaption, INTERVALS } from "../token.js";
8
+ import { FlowTracker } from "../flow.js";
9
+ import { positionRow } from "../trade.js";
10
+ import { renderAnalystSession } from "../../screens/analyst-session.js";
11
+ import { larpBadge } from "../../screens/screener.js";
12
+ import { signedUsd, signedPctOf, fmtUsdFine, sol as fmtSol, signedSol } from "../../screens/positions.js";
13
+ import { renderScanWhy, renderScanTicker } from "../../scan/ui.js";
14
+ import { drawCoin, imagesOn } from "../../screens/coin.js";
15
+ import { COIN_HERO_W, COIN_HERO_H } from '../../../../tui/src/index.js';
16
+ import { renderSocialPanel } from "../../screens/social-panel.js";
17
+ import { launchIntelCells, holderTags } from "../intel.js";
18
+ import { EX_POOL_TAGS } from '../../../../data/src/index.js';
19
+ import { layoutFor } from "../../custom.js";
20
+ import { chartMode, priceModeOf, mcapAt, positionPriceCells } from "../../display.js";
21
+ const dash = '—';
22
+ const EX_POOL_TAG_SET = EX_POOL_TAGS;
23
+ /** task-1367 holder-class badge colours: venues/escrows accent, the creator/linked warn, the rest muted. */
24
+ function tagColor(t, tag) {
25
+ if (tag === 'POOL' || tag === 'CURVE' || tag === 'VAULT' || tag === 'LOCK' || tag === 'LP')
26
+ return t.accent;
27
+ if (tag === 'DEV' || tag === 'LINKED')
28
+ return t.warn;
29
+ if (tag === 'SNIPER')
30
+ return t.down;
31
+ return t.muted;
32
+ }
33
+ export function renderTokenLive(c, state, v) {
34
+ const t = c.theme;
35
+ const bp = breakpoint(c.w);
36
+ header(c, headerModel(state));
37
+ const now = state.now.getTime();
38
+ // task-1324: AI scan mode's one-line strip on row 1 (the band under the header — no panel moves):
39
+ // WHY IT TRIGGERED on the coin the scan locked, the SCANNING ticker on any other coin.
40
+ if (state.scan?.engine.on) {
41
+ const ai = state.live?.scanMeterLabel() ?? '';
42
+ if (state.scan.engine.lock?.mint === v.mint)
43
+ renderScanWhy(c, state.scan, ai, now, state.timeZone);
44
+ else
45
+ renderScanTicker(c, state.scan, ai);
46
+ }
47
+ const top = 2, bottom = c.h - 1;
48
+ const lay = layoutFor(state.custom, c.w, c.h); // task-1358: analyst width + chart share from the workspace layout
49
+ const showAi = state.aiOpen !== false && bp !== 'compact' && lay.analystW > 0;
50
+ const aiW = showAi ? lay.analystW : 0;
51
+ const mainW = c.w - aiW;
52
+ // ---- title strip. task-1326: with images on, an 8×4 coin image sits at the left of the four header rows
53
+ // (title · price · CA · a blank row) and the text starts after it; off, the layout is the pre-1326 three rows.
54
+ let y = top;
55
+ const hero = imagesOn(state);
56
+ const heroW = hero ? COIN_HERO_W : 0;
57
+ const x0 = 1 + (hero ? heroW + 1 : 0);
58
+ c.fill(0, y, mainW, hero ? COIN_HERO_H : 2, ' ', { bg: t.bg });
59
+ if (hero)
60
+ drawCoin(c, state, { x: 1, y, w: heroW, h: COIN_HERO_H, url: v.image, symbol: v.symbol, bg: t.bg, prio: 2 });
61
+ let x = c.text(x0, y, '‹ ', { fg: t.muted, bg: t.bg });
62
+ x = c.text(x, y, `$${v.symbol}`, { fg: t.text, bg: t.bg, b: true });
63
+ x = c.text(x + 1, y, v.name, { fg: t.muted, bg: t.bg });
64
+ if (v.launchpad !== 'unknown')
65
+ x = launchpadBadge(c, x + 2, y, v.launchpad, { on: t.bg });
66
+ if (v.verified)
67
+ x = badge(c, x + 1, y, 'VERIFIED', t.up, { on: t.bg });
68
+ if (v.sus)
69
+ x = badge(c, x + 1, y, 'SUS', t.down, { on: t.bg });
70
+ // task-1315: LARP-detector verdict badge, only once larp-check has actually run for this mint.
71
+ const lb = larpBadge(state.larpByMint?.get(v.mint)?.verdict, t);
72
+ if (lb)
73
+ x = badge(c, x + 1, y, lb.text, lb.fg, { on: t.bg });
74
+ x = c.text(x + 2, y, shortAddr(v.mint, 6), { fg: t.dim, bg: t.bg });
75
+ x = c.text(x + 2, y, v.ageMin === null ? `age ${dash}` : `age ${fmtAge(v.ageMin)}`, { fg: t.dim, bg: t.bg });
76
+ const socials = [v.socials.x && 'X', v.socials.tg && 'TG', v.socials.web && 'WEB'].filter((s) => !!s);
77
+ let sx = mainW - 2 - socials.reduce((s, k) => s + k.length + 3, 0);
78
+ if (sx > x + 2)
79
+ for (const s of socials)
80
+ sx = badge(c, sx, y, s, t.muted, { on: t.bg }) + 1;
81
+ y += 1;
82
+ // Fast lane (task-1312): when the open token is streaming on-chain, show the push price + a LIVE badge;
83
+ // otherwise the polled price + POLL. The badge is honest — POLL whenever the stream is not delivering.
84
+ const liveBadge = state.live?.badgeFor(v.mint) ?? 'POLL';
85
+ const livePx = liveBadge === 'LIVE' ? state.live?.livePrice(v.mint) ?? null : null;
86
+ const shownPx = livePx ?? v.price;
87
+ // task-1366: mcap mode leads with the market cap (the live price × the polled supply ratio) and keeps the price beside it;
88
+ // without a supply it shows the price and says so.
89
+ const cm = chartMode(priceModeOf(state), v.price, v.mcap);
90
+ if (cm.kind === 'mcap' && shownPx !== null) {
91
+ x = c.text(x0, y, fmtMcap(mcapAt(shownPx, cm.scale)), { fg: t.text, bg: t.bg, b: true });
92
+ x = c.text(x + 1, y, `(${fmtPrice(shownPx)})`, { fg: t.dim, bg: t.bg });
93
+ }
94
+ else
95
+ x = c.text(x0, y, shownPx === null ? dash : fmtPrice(shownPx), { fg: cm.label && shownPx !== null ? t.warn : t.text, bg: t.bg, b: true });
96
+ if (state.live?.fastLane)
97
+ x = badge(c, x + 1, y, liveBadge, liveBadge === 'LIVE' ? t.up : t.muted, { on: t.bg });
98
+ const deltas = [['5m', v.chg.m5], ['1h', v.chg.h1], ['6h', v.chg.h6], ['24h', v.chg.h24]];
99
+ x += 2;
100
+ for (const [k, d] of deltas) {
101
+ x = c.text(x, y, k, { fg: t.dim, bg: t.bg });
102
+ x = c.text(x + 1, y, d === null ? dash : fmtPct(d), d === null ? { fg: t.dim, bg: t.bg } : { ...pctStyle(c, d, t.bg), b: k === '24h' });
103
+ x += 2;
104
+ }
105
+ const mk = clip(panelCaption(v.panels.market, now, 'market data unavailable'), 48);
106
+ if (mk && x + mk.length + 2 < mainW)
107
+ c.text(x + 1, y, mk, { fg: v.panels.market.state === 'error' ? t.warn : t.dim, bg: t.bg, i: true }, mainW - x - 3);
108
+ y += 1;
109
+ // ---- contract address (task-1311, QA B5/M9): the full mint is always on screen, even before a
110
+ // pool exists, with the key that copies it. A read-only screener is only useful if you can act on
111
+ // the mint in a wallet or bot. Rain must not overdraw it.
112
+ c.noRain(0, y, mainW, 1);
113
+ let cax = c.text(x0, y, 'CA ', { fg: t.dim, bg: t.bg });
114
+ cax = c.text(cax, y, v.mint, { fg: t.muted, bg: t.bg }, Math.max(0, mainW - cax - 12));
115
+ if (cax + 10 < mainW)
116
+ c.text(cax + 1, y, '· y copy', { fg: t.dim, bg: t.bg, i: true });
117
+ y += 1;
118
+ if (hero) {
119
+ c.rainZone(x0, y, Math.max(0, mainW - x0 - 1), 1, 1);
120
+ y += 1;
121
+ } // the hero's fourth row: a quiet band
122
+ // ---- position (task-1323 item 5): only when the coin on screen is HELD. Priced at the same tick the
123
+ // header shows (`shownPx`), so it moves with the header price. Flat / never held: nothing is drawn.
124
+ const src = state.live?.positionSource ?? state.live?.positions ?? null;
125
+ const held = src ? src.held(v.mint) : null;
126
+ const solUsd = state.sol?.price ?? src?.solUsd() ?? null;
127
+ const prow = held ? positionRow(held, shownPx, solUsd) : null;
128
+ const posH = prow ? POSITION_PANEL_H : 0;
129
+ // ---- chart
130
+ const chartH = Math.max(10, Math.floor((bottom - y - posH) * lay.chartFrac));
131
+ const cp = v.panels.candles;
132
+ const chartCap = clip(cp.state === 'ok' || cp.fetchedAt !== null ? `ohlcv: geckoterminal${cp.stale || cp.state === 'error' ? ' · stale' : ''}` : panelCaption(cp, now, 'ohlcv unavailable'), 40);
133
+ const cb = c.box(0, y, mainW, chartH, { title: cm.label ? (cm.kind === 'mcap' ? `${v.symbol} ${cm.label} · ${v.interval}` : `${v.symbol}/USD · ${cm.label} · ${v.interval}`) : `${v.symbol}/USD · ${v.interval}`, caption: chartCap, captionFg: cp.state === 'error' && cp.fetchedAt === null ? t.warn : undefined });
134
+ // timeframe tabs sit on the top border, left of the caption (same spot as the fixture screen)
135
+ let tx = cb.x + cb.w - 2 - (chartCap.length + 4) - INTERVALS.reduce((s, f) => s + f.length + 1, 0);
136
+ for (const f of INTERVALS)
137
+ tx = c.text(tx, cb.y - 1, f, f === v.interval ? { fg: t.accent, bg: cb.bg, b: true } : { fg: t.dim, bg: cb.bg }) + 1;
138
+ if (v.candles.length)
139
+ candleChart(c, cb.x + 1, cb.y, cb.w - 2, cb.h, v.candles, { bg: cb.bg, interval: v.interval, scale: cm.scale ?? undefined, axisFmt: cm.axisFmt });
140
+ else
141
+ c.textC(cb.x, cb.y + Math.floor(cb.h / 2), cb.w, cp.state === 'loading' ? 'loading candles…' : `no candles · ${cp.reason ?? 'source did not answer'}`, { fg: t.muted, bg: cb.bg, i: true });
142
+ y += chartH;
143
+ if (prow && src) {
144
+ positionPanel(c, 0, y, mainW, prow, held?.kind ?? src.kind, priceModeOf(state), cm.scale);
145
+ y += posH;
146
+ }
147
+ // ---- task-1330: the SOCIAL panel (`s`) replaces the ON-CHAIN / HOLDERS band; the chart + position stay.
148
+ if (state.tokenSocial) {
149
+ renderSocialPanel(c, 0, y, mainW, bottom - y, state, { mint: v.mint, symbol: v.symbol, now });
150
+ }
151
+ else {
152
+ // ---- stats + holders/tape split
153
+ const lower = bottom - y;
154
+ const statsW = bp === 'compact' ? mainW : Math.min(44, Math.floor(mainW * 0.4));
155
+ const sb = c.box(0, y, statsW, lower, { title: 'ON-CHAIN', caption: onChainCaption(v, now) });
156
+ let sy = sb.y;
157
+ const liqSub = v.liq !== null && v.mcap !== null && v.mcap > 0 ? `${(v.liq / v.mcap * 100).toFixed(1)}% of mcap` : undefined;
158
+ const holdersSub = v.holdersChgPct !== null ? `${v.holdersChgPct >= 0 ? '+' : ''}${v.holdersChgPct.toFixed(1)}% 24h` : undefined;
159
+ sy += statGrid(c, sb.x + 1, sy, sb.w - 2, [
160
+ { label: 'mcap', value: v.mcap === null ? dash : fmtUsd(v.mcap, { digits: 1 }) + (v.mcapFlagged ? '?' : ''), style: v.mcapFlagged ? { fg: t.warn } : undefined },
161
+ { label: 'fdv', value: v.fdv === null ? dash : fmtUsd(v.fdv, { digits: 1 }) },
162
+ { label: 'liquidity', value: v.liq === null ? dash : fmtUsd(v.liq, { digits: 1 }), sub: liqSub },
163
+ { label: 'vol 24h', value: v.vol24 === null ? dash : fmtUsd(v.vol24, { digits: 1 }) },
164
+ { label: 'holders', value: v.holders === null ? dash : fmtNum(v.holders), sub: holdersSub },
165
+ { label: 'organic', value: v.org === null ? dash : `${Math.round(v.org)}/100`, style: v.org === null ? { fg: t.dim } : { fg: v.org >= 70 ? t.up : v.org >= 40 ? t.warn : t.down, b: true } },
166
+ ], { bg: sb.bg, cols: 2 });
167
+ sy += 1;
168
+ const na = (why) => [why, 'na'];
169
+ // task-1340: snipers / bundled come from the wallet-intel scan (light tier on open; M opens the bubble map).
170
+ // task-1359: the cells are shared with the live NEW PAIRS list (launchIntelCells, one source of the thresholds).
171
+ const cells = launchIntelCells(state.live?.intel, v.mint, 'M bubble map');
172
+ // task-1367: the dev-mint count comes from Jupiter's audit, not Helius: once the launch window was read a missing
173
+ // count is Jupiter not reporting it (it used to say "needs Helius key" with a key configured)
174
+ const needsHistory = na(cells.measured ? 'not reported (jup)' : cells.snipers.text);
175
+ // task-1367: holder classes + top-10 raw / ex-pool from the same scan; the colour reads the ex-pool figure
176
+ const tags = holderTags(state.live?.intel, v.mint);
177
+ const t10lvl = (p) => (p < 25 ? 'ok' : p < 35 ? 'warn' : 'bad');
178
+ const top10Rows = tags.top10
179
+ ? [['Top 10 ex-pool', `${tags.top10.exPool.toFixed(1)}%`, t10lvl(tags.top10.exPool)], ['Top 10 raw', `${tags.top10.raw.toFixed(1)}%`, 'na']]
180
+ : [['Top 10 holders', ...(v.top10 === null ? na(v.panels.holders.state === 'needs-key' ? 'needs key' : dash) : [`${v.top10.toFixed(1)}%${v.panels.holders.state === 'ok' ? '' : ' (jup)'}`, t10lvl(v.top10)])]];
181
+ const sniperRow = [cells.snipers.text, cells.snipers.level];
182
+ const bundleRow = [cells.bundle.text, cells.bundle.level];
183
+ const rows = [
184
+ ...top10Rows,
185
+ ['Dev wallet', ...(v.dev === null ? na(dash) : [`${v.dev.toFixed(2)}%`, v.dev < 3 ? 'ok' : 'warn'])],
186
+ ['Bundled supply', ...bundleRow],
187
+ ['Snipers (first 3 slots)', ...sniperRow],
188
+ ['LP locked / burned', ...(v.lpLocked === null ? na(dash) : [`${Math.round(v.lpLocked)}%`, v.lpLocked >= 90 ? 'ok' : 'bad'])],
189
+ ['Mint authority', ...(v.mintAuth === null ? na(dash) : [v.mintAuth ? 'ENABLED' : 'revoked', v.mintAuth ? 'bad' : 'ok'])],
190
+ ['Freeze authority', ...(v.freezeAuth === null ? na(dash) : [v.freezeAuth ? 'ENABLED' : 'revoked', v.freezeAuth ? 'bad' : 'ok'])],
191
+ ['Dev history', ...(v.devMints === null ? needsHistory : [`${v.devMints} mints by dev (jup)`, v.devMints > 3 ? 'warn' : 'ok'])],
192
+ ['RugCheck score', ...(v.rug === null || v.rug.score === null ? na(v.panels.rugcheck.state === 'error' ? `unavailable: ${v.panels.rugcheck.reason ?? ''}`.trim() : dash) : [`${Math.round(v.rug.score)}/100`, v.rug.score < 30 ? 'ok' : v.rug.score < 60 ? 'warn' : 'bad'])],
193
+ ['GoPlus', ...goplusRow(v)],
194
+ ];
195
+ for (const [label, val0, lvl] of rows) {
196
+ if (sy >= sb.y + sb.h)
197
+ break;
198
+ const color = lvl === 'ok' ? t.up : lvl === 'warn' ? t.warn : lvl === 'bad' ? t.down : t.dim;
199
+ c.text(sb.x + 1, sy, lvl === 'ok' ? '●' : lvl === 'warn' ? '◐' : lvl === 'bad' ? '○' : '·', { fg: color, bg: sb.bg });
200
+ // label and value share the row: the value keeps at most half, the label takes what is left
201
+ const inner = sb.w - 4;
202
+ const val = clip(val0, Math.max(8, inner - 8));
203
+ c.text(sb.x + 3, sy, label, { fg: t.muted, bg: sb.bg }, Math.max(4, inner - val.length - 1));
204
+ c.textR(sb.x + 1, sy, sb.w - 2, val, { fg: lvl === 'ok' ? t.text : lvl === 'na' ? t.dim : color, bg: sb.bg, b: lvl === 'bad', i: lvl === 'na' });
205
+ sy += 1;
206
+ }
207
+ if (v.rug && v.rug.risks.length && sy < sb.y + sb.h) {
208
+ sy += sy + 1 < sb.y + sb.h ? 1 : 0;
209
+ for (const r of v.rug.risks.slice(0, 4)) {
210
+ if (sy >= sb.y + sb.h)
211
+ break;
212
+ const color = r.level === 'danger' ? t.down : r.level === 'warn' ? t.warn : t.muted;
213
+ c.text(sb.x + 1, sy, `${r.level === 'danger' ? '○' : r.level === 'warn' ? '◐' : '·'} ${r.name}`, { fg: color, bg: sb.bg }, sb.w - 2);
214
+ sy += 1;
215
+ }
216
+ }
217
+ if (bp !== 'compact') {
218
+ const hx = statsW;
219
+ const hw = mainW - statsW;
220
+ const hp = v.panels.holders;
221
+ // task-1305 (M10): a keyless user on the public RPC gets throttled holders with a raw "rate limited
222
+ // (429)". Point them at the fix instead — a free Helius key, which `dexterm doctor` walks through.
223
+ const keyless = state.live?.rpc.provider === 'public';
224
+ const keyHint = 'needs a free Helius key — run `dexterm doctor`';
225
+ const holdersNeedsKey = hp.state === 'needs-key' || (keyless && hp.state === 'error');
226
+ const hcap = hp.state === 'ok' || hp.fetchedAt !== null ? `${v.holdersSample ?? 'holders'} · via ${hp.via ?? '?'}${hp.stale || hp.state === 'error' ? ' · stale' : ''}` : holdersNeedsKey ? `holders: ${keyHint}` : hp.state === 'loading' ? 'holders: loading…' : `holders unavailable: ${hp.reason ?? 'rpc failed'}`;
227
+ const hb = c.box(hx, y, hw, lower, { title: 'HOLDERS', caption: hcap, captionFg: hp.state === 'error' && hp.fetchedAt === null ? t.warn : undefined });
228
+ let hy = hb.y;
229
+ c.text(hb.x + 1, hy, 'WALLET', { fg: t.dim, bg: hb.bg });
230
+ c.textR(hb.x + 1, hy, hb.w - 2, '% AMOUNT', { fg: t.dim, bg: hb.bg });
231
+ hy += 1;
232
+ // task-1367: the main-risk line (an unlabelled / creator / linked owner > 30 %) or why the biggest holder is not one
233
+ if (tags.note && hy < hb.y + hb.h) {
234
+ c.text(hb.x + 1, hy, `${tags.note.level === 'bad' ? '⚠ ' : '· '}${tags.note.text}`, { fg: tags.note.level === 'bad' ? t.down : t.muted, bg: hb.bg, b: tags.note.level === 'bad' }, hb.w - 2);
235
+ hy += 1;
236
+ }
237
+ // task-1323 item 6: the panel under the holders is BUY/SELL FLOW by default; `F` swaps in the raw
238
+ // tape (state.tokenTape). Flow reads only data already fetched: the tape (1m) and the DexScreener
239
+ // pair Store.enrich cached on the market refresh (5m..24h) — no request of its own.
240
+ const tapeOn = state.tokenTape === true;
241
+ let flowRows = [];
242
+ let flow = null;
243
+ if (!tapeOn) {
244
+ flow = state.tokenFlow ??= new FlowTracker();
245
+ const dl = state.live?.token?.dl ?? state.live?.dl;
246
+ if (dl)
247
+ flow.syncFromCache(dl.cache, v.mint);
248
+ flowRows = flow.rows(v.mint, { trades: v.trades, panel: v.panels.trades }, now);
249
+ }
250
+ const reserve = tapeOn ? (v.trades.length ? Math.min(8, v.trades.length) + 3 : 0) : flowRows.length + 3;
251
+ const shown = v.holdersTop.slice(0, Math.max(0, Math.min(v.holdersTop.length, hb.h - (hy - hb.y) - reserve)));
252
+ if (!shown.length) {
253
+ c.textC(hb.x, hy + 1, hb.w, hp.state === 'loading' ? 'loading holders…' : holdersNeedsKey ? keyHint : `unavailable: ${hp.reason ?? 'no holder source answered'}`, { fg: holdersNeedsKey ? t.warn : t.muted, bg: hb.bg, i: true });
254
+ hy += 3;
255
+ }
256
+ const maxPct = Math.max(1e-9, ...shown.map((h) => h.pct));
257
+ // task-1367: a class badge (up to " UNLABELLED ") needs two more columns than the old static labels
258
+ const tagged = tags.byOwner.size > 0;
259
+ const barW = Math.max(6, hb.w - (tagged ? 40 : 38));
260
+ for (const h of shown) {
261
+ if (hy >= hb.y + hb.h)
262
+ break;
263
+ const cx = c.text(hb.x + 1, hy, shortAddr(h.address, 4), { fg: h.tokenAccountOnly ? t.muted : t.text, bg: hb.bg });
264
+ // task-1367: the scan's class wins (POOL / CURVE / … / UNLABELLED); before it lands the static label, if any
265
+ const cls = tags.byOwner.get(h.address) ?? null;
266
+ const tag = cls ? cls.tag : h.label ? h.label.toUpperCase() : null;
267
+ const notPerson = cls ? EX_POOL_TAG_SET.has(cls.tag) : h.label === 'LP';
268
+ if (tag)
269
+ badge(c, cx + 1, hy, tag, tagColor(t, tag), { on: hb.bg });
270
+ const bx = hb.x + (tagged ? 24 : 22);
271
+ bar(c, bx, hy, barW, (h.pct / maxPct) * 100, { bg: hb.bg, color: notPerson ? t.accent : h.pct > 5 ? t.warn : mix(hb.bg, t.accent, 0.6) });
272
+ c.textR(hb.x + 1, hy, hb.w - 2, `${h.pct.toFixed(2).padStart(5)} ${fmtAmt(h.amount).padStart(7)}`, { fg: h.pct > 5 ? t.warn : t.muted, bg: hb.bg });
273
+ hy += 1;
274
+ }
275
+ // tape (toggled) or buy/sell flow (default) under holders if room
276
+ if (hy + 3 < hb.y + hb.h) {
277
+ hy += 1;
278
+ c.hline(hb.x, hy - 1, hb.w, { fg: t.border, bg: hb.bg });
279
+ const tp = v.panels.trades;
280
+ if (tapeOn) {
281
+ c.text(hb.x + 1, hy, 'TAPE', { fg: t.dim, bg: hb.bg });
282
+ c.textR(hb.x + 1, hy, hb.w - 2, tp.state === 'ok' || tp.fetchedAt !== null ? `geckoterminal · 20s poll${tp.stale || tp.state === 'error' ? ' · stale' : ''}` : panelCaption(tp, now, 'tape unavailable'), { fg: tp.state === 'error' && tp.fetchedAt === null ? t.warn : t.dim, bg: hb.bg });
283
+ hy += 1;
284
+ hy += tradeTape(c, hb.x + 1, hy, hb.w - 2, hb.y + hb.h - hy, v.trades, now, { bg: hb.bg });
285
+ }
286
+ else if (flow) {
287
+ c.text(hb.x + 1, hy, 'BUY/SELL FLOW', { fg: t.dim, bg: hb.bg });
288
+ c.textR(hb.x + 15, hy, hb.w - 16, flow.caption(v.mint, now), { fg: t.dim, bg: hb.bg });
289
+ hy += 1;
290
+ hy += flowPanel(c, hb.x + 1, hy, hb.w - 2, hb.y + hb.h - hy, flowRows, { bg: hb.bg });
291
+ if (hy < hb.y + hb.h)
292
+ c.text(hb.x + 1, hy, 'trend = buy share vs the previous window', { fg: t.dim, bg: hb.bg, i: true }, hb.w - 2);
293
+ }
294
+ }
295
+ }
296
+ }
297
+ if (showAi)
298
+ analystPlaceholder(c, mainW, top, aiW, bottom - top, v, state);
299
+ const watched = state.live?.watch?.has(v.mint) ? 'unwatch' : 'watch';
300
+ // task-1311: `i` asks claude about this token (chat), `a` runs the paid dossier on demand, `y`
301
+ // copies the full contract address.
302
+ const aLabel = state.analyst ? (showAi ? 'hide' : 'show') : 'dossier';
303
+ // task-1315: surface the open-links keys only when the coin actually advertises a link.
304
+ const hasLinks = !!(v.socialLinks.web || v.socialLinks.x || v.socialLinks.tg);
305
+ const keys = state.tokenSocial
306
+ ? [['esc', 'back'], ['s', 'on-chain'], ['f', 'filter'], ['x', state.socialShowSpam ? 'hide spam' : 'show hidden'], ['K', 'KOL lists'], ['i', 'ask'], ['a', aLabel], ['B/S', 'paper trade'], ['y', 'copy CA']]
307
+ : [['esc', 'back'], ['i', 'ask'], ['a', aLabel], ['s', 'social'], ['M', 'bubble map'], ['V', 'vet product'], ['B/S', 'paper trade'], ['1-6', 'timeframe'], ['F', state.tokenTape ? 'flow' : 'tape'], ['y', 'copy CA']];
308
+ if (hasLinks)
309
+ keys.push(['W/X/T', 'open site/x/tg'], ['L', 'all links']);
310
+ keys.push(['w', watched], ['P', 'positions'], ['q', 'quit']);
311
+ footer(c, keys, state.theme.name);
312
+ }
313
+ /** Height of the POSITION panel (border, labels, values, border). */
314
+ export const POSITION_PANEL_H = 4;
315
+ /** `$0.0₄123` for USD, `0.0₄123 SOL` when only the SOL ledger price is known. */
316
+ function pxText(usd, solPx) {
317
+ if (usd !== null)
318
+ return fmtPrice(usd);
319
+ if (solPx !== null)
320
+ return `${fmtPrice(solPx).replace('$', '')} SOL`;
321
+ return dash;
322
+ }
323
+ /**
324
+ * One-row table of the held position under the chart (task-1323 item 5): side, size in tokens and USD,
325
+ * average entry, current price, unrealized $ and %, realized (only when there is some), and the distance
326
+ * to a stop/target (only when set). Falls back to SOL units when SOL/USD is unknown. Columns drop by
327
+ * priority when the panel is narrow (target/stop first, then realized).
328
+ */
329
+ function positionPanel(c, x, y, w, r, kind, mode = 'price', supply = null) {
330
+ const t = c.theme;
331
+ const usd = r.solUsd !== null;
332
+ const cap = r.basis === 'none' ? 'no price yet' : usd ? `${kind} ledger · SOL $${r.solUsd.toFixed(2)}` : `${kind} ledger · SOL units (no SOL/USD yet)`;
333
+ const box = c.box(x, y, w, POSITION_PANEL_H, { title: kind === 'paper' ? '◈ POSITION · PAPER' : '◈ POSITION', titleFg: kind === 'paper' ? t.warn : t.text, caption: cap });
334
+ const bg = box.bg;
335
+ const pnlColor = (n) => (n === null ? t.muted : n > 0 ? t.up : n < 0 ? t.down : t.muted);
336
+ const cols = [];
337
+ cols.push({ label: 'SIDE', value: r.side.toUpperCase(), st: { fg: r.side === 'long' ? t.up : t.down, bg, b: true }, prio: 0 });
338
+ cols.push({ label: 'SIZE', value: `${fmtAmt(r.tokens)} ${r.symbol}`, st: { fg: t.text, bg }, prio: 0 });
339
+ cols.push({ label: 'VALUE', value: r.valueUsd !== null ? fmtUsdFine(r.valueUsd) : r.valueSol !== null ? `${fmtSol(r.valueSol)} SOL` : dash, st: { fg: t.text, bg, b: true }, prio: 0 });
340
+ // task-1366: in mcap mode (supply known) AVG / PRICE read market cap; PnL stays USD
341
+ const pc = positionPriceCells(mode, supply, r.avgEntryUsd, r.priceUsd, pxText(r.avgEntryUsd, r.avgEntrySol), pxText(r.priceUsd, r.priceSol));
342
+ cols.push({ label: pc.avg.label, value: pc.avg.value, st: { fg: t.muted, bg }, prio: 1 });
343
+ cols.push({ label: pc.price.label, value: pc.price.value, st: { fg: t.text, bg }, prio: 1 });
344
+ const uAmt = usd ? (r.unrealizedUsd !== null ? signedUsd(r.unrealizedUsd) : null) : (r.unrealizedSol !== null ? `${signedSol(r.unrealizedSol)} SOL` : null);
345
+ cols.push({ label: 'UNREALIZED', value: uAmt === null ? dash : `${uAmt}${r.unrealizedPct !== null ? ` ${signedPctOf(r.unrealizedPct)}` : ''}`, st: { fg: pnlColor(r.unrealizedSol), bg, b: true }, prio: 0 });
346
+ if (r.realizedSol !== 0)
347
+ cols.push({ label: 'REALIZED', value: usd && r.realizedUsd !== null ? signedUsd(r.realizedUsd) : `${signedSol(r.realizedSol)} SOL`, st: { fg: pnlColor(r.realizedSol), bg }, prio: 2 });
348
+ if (r.stopUsd !== null)
349
+ cols.push({ label: 'STOP', value: `${fmtPrice(r.stopUsd)}${r.stopDistPct !== null ? ` ${signedPctOf(r.stopDistPct)}` : ''}`, st: { fg: t.down, bg }, prio: 3 });
350
+ if (r.targetUsd !== null)
351
+ cols.push({ label: 'TARGET', value: `${fmtPrice(r.targetUsd)}${r.targetDistPct !== null ? ` ${signedPctOf(r.targetDistPct)}` : ''}`, st: { fg: t.up, bg }, prio: 3 });
352
+ const gap = 3;
353
+ const widthOf = (list) => list.reduce((s, k) => s + Math.max(k.label.length, k.value.length) + gap, -gap);
354
+ const inner = box.w - 2;
355
+ let keep = cols.slice();
356
+ while (keep.length > 1 && widthOf(keep) > inner) {
357
+ const worst = Math.max(...keep.map((k) => k.prio));
358
+ const i = keep.map((k) => k.prio).lastIndexOf(worst);
359
+ keep = keep.filter((_, j) => j !== i);
360
+ }
361
+ let cx = box.x + 1;
362
+ for (const k of keep) {
363
+ const cw = Math.max(k.label.length, k.value.length);
364
+ c.text(cx, box.y, k.label, { fg: t.dim, bg }, Math.max(0, box.x + box.w - 1 - cx));
365
+ c.text(cx, box.y + 1, k.value, k.st, Math.max(0, box.x + box.w - 1 - cx));
366
+ cx += cw + gap;
367
+ }
368
+ }
369
+ function clip(s, max) {
370
+ return s.length > max ? s.slice(0, Math.max(1, max - 1)) + '…' : s;
371
+ }
372
+ /** Token amounts up to trillions (fmtNum stops at B). */
373
+ function fmtAmt(n) {
374
+ if (n >= 1e12)
375
+ return `${(n / 1e12).toFixed(2)}T`;
376
+ return fmtNum(n);
377
+ }
378
+ function onChainCaption(v, now) {
379
+ const a = v.panels.authorities;
380
+ if (a.state === 'ok' || a.fetchedAt !== null)
381
+ return `via ${a.via ?? '?'}${v.tokenProgram ? ` · ${v.tokenProgram}` : ''}${a.stale || a.state === 'error' ? ' · stale' : ''}`;
382
+ if (a.state === 'loading')
383
+ return 'authorities: loading…';
384
+ if (a.state === 'needs-key')
385
+ return 'authorities: needs key';
386
+ return `authorities: ${v.mintAuth !== null ? 'jup/goplus' : 'unavailable'}${a.reason ? ` (${a.reason})` : ''}${panelCaption(a, now) ? '' : ''}`;
387
+ }
388
+ function goplusRow(v) {
389
+ const p = v.panels.goplus;
390
+ if (!v.goplus)
391
+ return [p.state === 'loading' ? 'loading…' : p.state === 'error' ? `unavailable: ${p.reason ?? ''}`.trim() : '—', 'na'];
392
+ const g = v.goplus;
393
+ if (g.flags.length)
394
+ return [g.flags.map((f) => f.replace(/^goplus:/, '')).join(', '), g.flags.some((f) => /mintable|freezable|malicious|transfer-hook|balance-mutable/.test(f)) ? 'bad' : 'warn'];
395
+ const extra = g.lpHolders.length ? ` · ${g.lpHolders.length} LP holders` : '';
396
+ return [`clean${extra}`, 'ok'];
397
+ }
398
+ // ---------------- labelled analyst placeholder (Claude wiring is a later card) ----------------
399
+ function analystPlaceholder(c, x, y, w, h, v, state) {
400
+ if (state.analyst) {
401
+ const focused = state.aiFocused === true;
402
+ renderAnalystSession(c, x, y, w, h, state.analyst, {
403
+ frame: state.vfx.frame, code: state.vfx.code, calm: isCalm(state.vfx),
404
+ input: state.aiInput ?? '', inputFocused: focused,
405
+ scroll: state.aiScroll, revealAll: state.aiRevealAll,
406
+ chips: [`$${v.symbol}`, v.panels.holders.state === 'ok' ? `holders:${v.holdersTop.length}` : 'holders:—', v.panels.trades.state === 'ok' ? `trades:${v.trades.length}` : 'trades:—', v.rug ? 'rugcheck' : 'rugcheck:—', 'candles'],
407
+ inputHint: focused ? `ask about $${v.symbol}… ⏎ send · esc blur` : `press i to ask about $${v.symbol} · read-only`,
408
+ });
409
+ return;
410
+ }
411
+ // task-1311 (QA M5): the pre-run state. Opening a token no longer spends a paid dossier; this panel
412
+ // waits until you ask for one with `a` (dossier) or type a question with `i`. No fake verdicts.
413
+ const t = c.theme;
414
+ const box = c.box(x, y, w, h, { title: '✦ ANALYST · claude', titleFg: t.ai, caption: 'read-only · local claude · on demand', captionFg: t.dim, border: mix(t.border, t.ai, 0.35) });
415
+ const bg = box.bg;
416
+ const inner = box.w - 2;
417
+ let cy = box.y;
418
+ let cx = box.x + 1;
419
+ const chips = [`$${v.symbol}`, v.panels.holders.state === 'ok' ? `holders:${v.holdersTop.length}` : 'holders:—', v.panels.trades.state === 'ok' ? `trades:${v.trades.length}` : 'trades:—', v.rug ? 'rugcheck' : 'rugcheck:—', v.goplus ? 'goplus' : 'goplus:—'];
420
+ for (const chip of chips) {
421
+ if (cx + chip.length + 3 > box.x + box.w - 1)
422
+ break;
423
+ cx = badge(c, cx, cy, chip, t.ai, { on: bg }) + 1;
424
+ }
425
+ cy += 2;
426
+ const lines = [
427
+ 'Press a to run a full dossier on this token (one paid claude run), or i to ask a free-text question.',
428
+ '',
429
+ 'The chips above are the live context claude gets: holders, tape, RugCheck, GoPlus and candles, each with its source and age.',
430
+ '',
431
+ 'Opening a token is free — the analyst only runs when you ask, and keeps a separate conversation per token.',
432
+ ];
433
+ const inputY = box.y + box.h - 2;
434
+ for (const ln of lines) {
435
+ if (cy >= inputY - 1)
436
+ break;
437
+ if (!ln) {
438
+ cy += 1;
439
+ continue;
440
+ }
441
+ for (const w2 of wrap(ln, inner - 2)) {
442
+ if (cy >= inputY - 1)
443
+ break;
444
+ c.text(box.x + 2, cy, w2, { fg: t.muted, bg });
445
+ cy += 1;
446
+ }
447
+ }
448
+ const bc = mix(t.border, t.ai, 0.35);
449
+ const focused = state.aiFocused === true;
450
+ const typed = state.aiInput ?? '';
451
+ c.hline(box.x, inputY - 1, box.w, { fg: bc, bg });
452
+ c.put(box.x, inputY - 1, '├', { fg: bc, bg });
453
+ c.put(box.x + box.w - 1, inputY - 1, '┤', { fg: bc, bg });
454
+ c.text(box.x + 1, inputY, '▌', { fg: focused ? t.ai : t.dim, bg, b: true });
455
+ if (typed)
456
+ c.text(box.x + 2, inputY, typed + (focused ? '█' : ''), { fg: t.text, bg }, inner - 2);
457
+ else
458
+ c.text(box.x + 2, inputY, focused ? `█ ask about $${v.symbol}… ⏎ send` : `press i to ask · a for a dossier`, { fg: t.dim, bg, i: true }, inner - 2);
459
+ }
@@ -0,0 +1,149 @@
1
+ // dexterm — live WATCHLIST screen (task-1298): the layout of screens/watchlist.ts over the persisted list.
2
+ // ALERTS shows the real event log; the right panel lists alert PROPOSALS (y arms, x dismisses) and the
3
+ // armed rules, instead of the fixture "analyst digest" (Claude wiring is a later card).
4
+ import { Canvas, table, sparkline, heatPct, launchpadBadge, badge, fmtUsd, fmtPrice, fmtAge, header, footer, breakpoint, } from '../../../../tui/src/index.js';
5
+ import { priceModeOf, promoteMcap, supplyRatio, mcapAt, fallbackCount, fallbackCaption, FALLBACK_MARK } from "../../display.js";
6
+ import { headerModel } from "../../state.js";
7
+ import { fmtValue } from '../../../../data/src/index.js';
8
+ import { drawCoin, imagesOn } from "../../screens/coin.js";
9
+ import { COIN_ICON_W } from '../../../../tui/src/index.js';
10
+ export function renderWatchlistLive(c, state, live) {
11
+ const t = c.theme;
12
+ const bp = breakpoint(c.w);
13
+ header(c, headerModel(state));
14
+ const now = state.now.getTime();
15
+ const top = 2, bottom = c.h - 1;
16
+ const rows = live.list();
17
+ const sel = Math.min(Math.max(0, state.selected || 0), Math.max(0, rows.length - 1));
18
+ const st = live.status();
19
+ const listH = Math.max(8, Math.min(rows.length + 4, Math.floor((bottom - top) * 0.5)));
20
+ const mode = priceModeOf(state); // task-1366
21
+ const fb = mode === 'mcap' ? fallbackCaption(fallbackCount(rows, (r) => ({ price: r.row?.price, mcap: r.row?.mcap }))) : null;
22
+ const box = c.box(0, top, c.w, listH, { title: 'WATCHLIST', caption: fb ? `${st.caption} · ${fb}` : st.caption });
23
+ const dash = (cv, x, y, w, bg) => cv.textR(x, y, w, '—', { fg: t.dim, bg });
24
+ const cols = [
25
+ // task-1326: the coin image from the latest market row (null until the first refresh → placeholder).
26
+ ...(imagesOn(state) && bp !== 'compact' ? [{ key: 'img', label: '', w: COIN_ICON_W, prio: 2, render: (cv, x, y, w, r, bg) => { drawCoin(cv, state, { x, y, w, h: 1, url: r.row?.image ?? null, symbol: r.entry.symbol, bg, prio: 1, mode: 'quad' }); } }] : []),
27
+ { key: 'token', label: 'TOKEN', w: 22, prio: 0, flex: true, render: (cv, x, y, w, r, bg) => {
28
+ const cx = cv.text(x, y, r.entry.symbol, { fg: t.text, bg, b: true }, 10);
29
+ cv.text(cx + 1, y, r.entry.name, { fg: t.muted, bg }, w - (cx - x) - 1);
30
+ } },
31
+ { key: 'lp', label: 'LP', w: 6, prio: 3, render: (cv, x, y, w, r, bg) => { if (r.entry.launchpad === 'unknown')
32
+ cv.text(x, y, '—', { fg: t.dim, bg });
33
+ else
34
+ launchpadBadge(cv, x, y, r.entry.launchpad, { on: bg }); void w; } },
35
+ { key: 'price', label: 'PRICE', w: 11, align: 'right', prio: 0, render: (cv, x, y, w, r, bg) => {
36
+ // Fast lane (task-1312): the on-chain push price when this mint is streaming LIVE, else the polled price.
37
+ const live = state.live?.badgeFor(r.entry.mint) === 'LIVE' ? state.live?.livePrice(r.entry.mint) ?? null : null;
38
+ const p = live ?? r.row?.price ?? null;
39
+ if (p === null)
40
+ dash(cv, x, y, w, bg);
41
+ else
42
+ cv.textR(x, y, w, fmtPrice(p), { fg: r.stale && live === null ? t.muted : t.text, bg });
43
+ } },
44
+ // task-1366: in mcap mode the entry reads as the market cap at entry (entry price × the CURRENT supply ratio)
45
+ { key: 'entry', label: mode === 'mcap' ? 'ENTRY MC' : 'ENTRY', w: 11, align: 'right', prio: 2, render: (cv, x, y, w, r, bg) => {
46
+ if (r.entry.entryPrice === null) {
47
+ dash(cv, x, y, w, bg);
48
+ return;
49
+ }
50
+ const ratio = mode === 'mcap' ? supplyRatio(r.row?.price, r.row?.mcap) : null;
51
+ cv.textR(x, y, w, ratio !== null ? fmtUsd(mcapAt(r.entry.entryPrice, ratio), { digits: 1 }) : fmtPrice(r.entry.entryPrice) + (mode === 'mcap' ? FALLBACK_MARK : ''), { fg: t.muted, bg });
52
+ } },
53
+ { key: 'pnl', label: 'VS ENTRY', w: 9, align: 'right', prio: 0, render: (cv, x, y, w, r, bg) => heatPct(cv, x, y, w, r.pnlPct, bg) },
54
+ { key: 'h1', label: '1H', w: 7, align: 'right', prio: 1, render: (cv, x, y, w, r, bg) => heatPct(cv, x, y, w, r.row?.chg.h1 ?? null, bg) },
55
+ { key: 'h24', label: '24H', w: 8, align: 'right', prio: 1, render: (cv, x, y, w, r, bg) => heatPct(cv, x, y, w, r.row?.chg.h24 ?? null, bg) },
56
+ { key: 'mcap', label: 'MCAP', w: mode === 'mcap' ? 9 : 8, align: 'right', prio: 1, render: (cv, x, y, w, r, bg) => {
57
+ // task-1366: in mcap mode a streaming (LIVE) price moves the market cap too (live price × the polled supply ratio);
58
+ // without a supply the cell is the price, marked ᵖ in amber.
59
+ const live = mode === 'mcap' && state.live?.badgeFor(r.entry.mint) === 'LIVE' ? state.live?.livePrice(r.entry.mint) ?? null : null;
60
+ const ratio = supplyRatio(r.row?.price, r.row?.mcap);
61
+ const v = live !== null && ratio !== null ? mcapAt(live, ratio) : r.row?.mcap ?? null;
62
+ const p = live ?? r.row?.price ?? null;
63
+ if (v === null && mode === 'mcap' && p !== null) {
64
+ cv.textR(x, y, w, fmtPrice(p) + FALLBACK_MARK, { fg: t.warn, bg });
65
+ return;
66
+ }
67
+ if (v === null)
68
+ dash(cv, x, y, w, bg);
69
+ else
70
+ cv.textR(x, y, w, fmtUsd(v, { digits: 1 }), { fg: t.text, bg });
71
+ } },
72
+ { key: 'liq', label: 'LIQ', w: 8, align: 'right', prio: 2, render: (cv, x, y, w, r, bg) => { const v = r.row?.liq ?? null; if (v === null)
73
+ dash(cv, x, y, w, bg);
74
+ else
75
+ cv.textR(x, y, w, fmtUsd(v, { digits: 1 }), { fg: t.text, bg }); } },
76
+ { key: 'spark', label: '24H TREND', w: 16, prio: 1, render: (cv, x, y, w, r, bg) => { if (r.row?.spark)
77
+ sparkline(cv, x, y, w, r.row.spark, { bg });
78
+ else
79
+ cv.text(x, y, '—', { fg: t.dim, bg }); } },
80
+ { key: 'note', label: 'NOTE', w: 30, prio: 2, render: (cv, x, y, w, r, bg) => { cv.text(x, y, r.entry.note, { fg: t.muted, bg, i: true }, w); } },
81
+ { key: 'added', label: 'ADDED', w: 6, align: 'right', prio: 4, render: (cv, x, y, w, r, bg) => { cv.textR(x, y, w, fmtAge(Math.max(0, (now - r.entry.addedAt) / 60000)), { fg: t.dim, bg }); } },
82
+ ];
83
+ // Fast lane (task-1312): an honest per-row LIVE/POLL badge, only in live mode (the fast lane exists).
84
+ if (state.live?.fastLane) {
85
+ cols.splice(2, 0, { key: 'feed', label: 'FEED', w: 5, prio: 2, render: (cv, x, y, w, r, bg) => {
86
+ const live = state.live?.badgeFor(r.entry.mint) === 'LIVE';
87
+ badge(cv, x, y, live ? 'LIVE' : 'POLL', live ? t.up : t.dim, { on: bg });
88
+ void w;
89
+ } });
90
+ }
91
+ if (rows.length)
92
+ table(c, box.x + 1, box.y, box.w - 2, box.h, { columns: promoteMcap(cols, mode), rows, selected: sel, bg: box.bg });
93
+ else
94
+ c.textC(box.x, box.y + Math.min(3, Math.floor(box.h / 2)), box.w, 'watchlist is empty · press w on any token (screener, new pairs, token screen) to add it', { fg: t.muted, bg: box.bg, i: true });
95
+ const y2 = top + listH;
96
+ const h2 = bottom - y2;
97
+ const alertsW = bp === 'compact' ? c.w : Math.floor(c.w * 0.58);
98
+ const notice = live.notice ?? state.live?.notice ?? null;
99
+ const ab = c.box(0, y2, alertsW, h2, { title: 'ALERTS', caption: notice ?? 'armed rules fire once into this log · nothing trades', captionFg: notice ? t.warn : undefined });
100
+ let ay = ab.y;
101
+ const events = live.events(h2);
102
+ if (!events.length)
103
+ c.text(ab.x + 1, ay, 'no alerts yet', { fg: t.dim, bg: ab.bg, i: true });
104
+ for (const a of events) {
105
+ if (ay >= ab.y + ab.h)
106
+ break;
107
+ const color = a.level === 'up' ? t.up : a.level === 'down' ? t.down : a.level === 'ai' ? t.ai : t.accent;
108
+ let cx = c.text(ab.x + 1, ay, fmtAge(Math.max(0, (now - a.t) / 60000)).padStart(4), { fg: t.dim, bg: ab.bg });
109
+ cx = badge(c, cx + 1, ay, a.kind.toUpperCase().padEnd(9), color, { on: ab.bg }) + 1;
110
+ c.text(cx, ay, a.text, { fg: t.text, bg: ab.bg }, ab.w - (cx - ab.x) - 1);
111
+ ay += 1;
112
+ }
113
+ if (bp !== 'compact') {
114
+ const rb = c.box(alertsW, y2, c.w - alertsW, h2, { title: 'ALERT PROPOSALS', caption: 'y arm · x dismiss · b propose' });
115
+ let ry = rb.y;
116
+ const proposals = live.proposals();
117
+ const armed = live.armed();
118
+ const sym = (m) => live.store.get(m)?.symbol ?? m.slice(0, 6);
119
+ if (!proposals.length && !armed.length) {
120
+ c.text(rb.x + 1, ry, 'no proposals · b on a row proposes a liquidity alert', { fg: t.dim, bg: rb.bg, i: true }, rb.w - 2);
121
+ ry += 1;
122
+ }
123
+ proposals.forEach((p, i) => {
124
+ if (ry >= rb.y + rb.h)
125
+ return;
126
+ const hot = i === 0;
127
+ c.text(rb.x + 1, ry, hot ? '▸' : ' ', { fg: t.accent, bg: rb.bg });
128
+ c.text(rb.x + 3, ry, `${sym(p.mint)} ${p.kind} ${p.op} ${fmtValue(p.kind, p.value)}`, { fg: t.text, bg: rb.bg, b: hot }, rb.w - 4);
129
+ c.textR(rb.x + 1, ry, rb.w - 2, p.note, { fg: t.dim, bg: rb.bg });
130
+ ry += 1;
131
+ });
132
+ if (armed.length && ry < rb.y + rb.h) {
133
+ ry += ry > rb.y ? 1 : 0;
134
+ c.text(rb.x + 1, ry, 'ARMED', { fg: t.dim, bg: rb.bg });
135
+ ry += 1;
136
+ for (const r of armed) {
137
+ if (ry >= rb.y + rb.h)
138
+ break;
139
+ c.text(rb.x + 1, ry, `● ${sym(r.mint)} ${r.kind} ${r.op} ${fmtValue(r.kind, r.value)}`, { fg: t.up, bg: rb.bg }, rb.w - 2);
140
+ ry += 1;
141
+ }
142
+ }
143
+ if (ry < rb.y + rb.h - 1) {
144
+ ry = rb.y + rb.h - 1;
145
+ c.text(rb.x + 1, ry, '✦ analyst digest: ◌ demo data · not wired yet', { fg: t.warn, bg: rb.bg, i: true }, rb.w - 2);
146
+ }
147
+ }
148
+ footer(c, [['j/k', 'move'], ['⏎', 'open'], ['d', 'remove'], ['b', 'propose alert'], ['y/x', 'arm/dismiss'], ['r', 'refresh'], ['q', 'quit']], state.theme.name);
149
+ }