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,312 @@
1
+ // dexterm — Token detail screen: glowing candles + on-chain stats + holders/tape rain + decrypting analyst.
2
+ // Cyberspace styling (task-1297): pump/dump band on the price line, rain zones in the title strip and
3
+ // the blank panel areas, ▲/▼ everywhere a direction is shown, the analyst's verdict and tool lines in
4
+ // coral with the Claude-Code spinner and decrypt stream (VISUAL-V2 §3).
5
+ import { Canvas, candleChart, statGrid, bar, badge, launchpadBadge, pctStyle, tapeRain, decryptText, spinnerGlyph, fmtUsd, fmtPrice, fmtMcap, fmtAge, fmtAgeSec, fmtNum, fmtPctDir, fmtDeltaDir, shortAddr, wrap, header, footer, breakpoint, mix, isCalm, } from '../../../tui/src/index.js';
6
+ import { headerModel } from "../state.js";
7
+ import { renderTokenLive } from "../live/screens/token-live.js";
8
+ import { aiTranscript } from "../fixtures.js";
9
+ import { renderAnalystSession } from "./analyst-session.js";
10
+ import { renderSocialPanel } from "./social-panel.js";
11
+ import { layoutFor } from "../custom.js";
12
+ import { chartMode, priceModeOf } from "../display.js";
13
+ export function renderToken(c, state) {
14
+ if (state.live?.token && state.screener.source !== 'fixtures')
15
+ return renderTokenLive(c, state, state.live.token.view); // task-1298: live data path
16
+ const t = c.theme;
17
+ const bp = breakpoint(c.w);
18
+ header(c, headerModel(state));
19
+ const tk = state.token || state.fx.tokens[state.selected || 0];
20
+ const top = 2, bottom = c.h - 1;
21
+ const lay = layoutFor(state.custom, c.w, c.h); // task-1358: analyst width + chart share from the workspace layout
22
+ const showAi = state.aiOpen !== false && bp !== 'compact' && lay.analystW > 0;
23
+ const aiW = showAi ? lay.analystW : 0;
24
+ const mainW = c.w - aiW;
25
+ const v = state.vfx;
26
+ const pumping = v.mood === 'pump';
27
+ const dumping = v.mood === 'dump';
28
+ // ---- title strip
29
+ let y = top;
30
+ c.fill(0, y, mainW, 2, ' ', { bg: t.bg });
31
+ let x = c.text(1, y, '‹ ', { fg: t.muted, bg: t.bg });
32
+ x = c.text(x, y, `$${tk.symbol}`, { fg: t.borderFocus, bg: t.bg, b: true });
33
+ c.heading(3, y, tk.symbol.length + 1);
34
+ x = c.text(x + 1, y, tk.name, { fg: t.muted, bg: t.bg });
35
+ x = launchpadBadge(c, x + 2, y, tk.launchpad, { on: t.bg });
36
+ if (tk.migrated && tk.launchpad === 'pump.fun')
37
+ x = badge(c, x + 1, y, 'PUMPSWAP', t.pump, { on: t.bg });
38
+ x = c.text(x + 2, y, shortAddr(tk.mint, 6), { fg: t.dim, bg: t.bg });
39
+ x = c.text(x + 2, y, `age ${fmtAge(tk.ageMin)}`, { fg: t.dim, bg: t.bg });
40
+ const socials = [tk.socials.x && 'X', tk.socials.tg && 'TG', tk.socials.web && 'WEB'].filter((s) => !!s);
41
+ let sx = mainW - 2 - socials.reduce((s, k) => s + k.length + 3, 0);
42
+ if (sx > x + 2) {
43
+ c.rainZone(x + 1, y, sx - x - 2, 1, 1);
44
+ for (const s of socials)
45
+ sx = badge(c, sx, y, s, t.muted, { on: t.bg }) + 1;
46
+ }
47
+ y += 1;
48
+ // price line: pump glow = gold bg band, dump = red band (the rain ramp of the moment)
49
+ const priceBg = pumping ? mix(t.bg, t.rainPump[2], 0.3) : dumping ? mix(t.bg, t.rainDump[2], 0.3) : t.bg;
50
+ // task-1366: mcap mode leads with the market cap and keeps the price beside it (fixture tokens always carry a supply)
51
+ const cm = chartMode(priceModeOf(state), tk.price, tk.mcap);
52
+ if (cm.kind === 'mcap') {
53
+ x = c.text(1, y, fmtMcap(tk.mcap), { fg: pumping ? t.rainPump[3] : t.text, bg: priceBg, b: true });
54
+ x = c.text(x, y, ` (${fmtPrice(tk.price)})`, { fg: t.dim, bg: priceBg });
55
+ }
56
+ else
57
+ x = c.text(1, y, fmtPrice(tk.price), { fg: cm.label ? t.warn : pumping ? t.rainPump[3] : t.text, bg: priceBg, b: true });
58
+ const deltas = [['5m', tk.chg.m5], ['1h', tk.chg.h1], ['6h', tk.chg.h6], ['24h', tk.chg.h24]];
59
+ x = c.text(x, y, ' ', { bg: t.bg });
60
+ for (const [k, vv] of deltas) {
61
+ x = c.text(x, y, k, { fg: t.dim, bg: t.bg });
62
+ x = c.text(x + 1, y, fmtPctDir(vv), { ...pctStyle(c, vv, t.bg), b: k === '24h' });
63
+ x += 2;
64
+ }
65
+ if (pumping)
66
+ x = badge(c, x + 1, y, '▲ PUMPING', t.rainPump[3], { bg: mix(t.bg, t.rainPump[2], 0.35), bold: true }) + 1;
67
+ if (dumping)
68
+ x = badge(c, x + 1, y, '▼ DUMPING', t.rainDump[3], { bg: mix(t.bg, t.rainDump[2], 0.35), bold: true }) + 1;
69
+ c.rainZone(x + 1, y, Math.max(0, mainW - x - 2), 1, 1);
70
+ y += 1;
71
+ // ---- chart
72
+ const chartH = Math.max(10, Math.floor((bottom - y) * lay.chartFrac));
73
+ const cb = c.box(0, y, mainW, chartH, { title: cm.label ? (cm.kind === 'mcap' ? `${tk.symbol} ${cm.label} · 1h` : `${tk.symbol}/SOL · ${cm.label} · 1h`) : `${tk.symbol}/SOL · 1h`, caption: 'ohlcv: geckoterminal' });
74
+ const tfx = cb.x + cb.w - 2;
75
+ const tfs = ['1m', '5m', '15m', '1h', '4h', '1d'];
76
+ let tx = tfx - tfs.reduce((s, f) => s + f.length + 1, 0) - 24;
77
+ for (const f of tfs)
78
+ tx = c.text(tx, cb.y - 1, f, f === '1h' ? { fg: t.accent, bg: cb.bg, b: true } : { fg: t.dim, bg: cb.bg }) + 1;
79
+ candleChart(c, cb.x + 1, cb.y, cb.w - 2, cb.h, tk.candles, { bg: cb.bg, interval: '1h', scale: cm.scale ?? undefined, axisFmt: cm.axisFmt });
80
+ y += chartH;
81
+ // ---- task-1330: the SOCIAL panel (`s`) replaces the ON-CHAIN / HOLDERS band; the chart stays.
82
+ if (state.tokenSocial) {
83
+ renderSocialPanel(c, 0, y, mainW, bottom - y, state, { mint: tk.mint, symbol: tk.symbol, now: state.now.getTime() });
84
+ }
85
+ else {
86
+ // ---- stats + holders/trades split
87
+ const lower = bottom - y;
88
+ const statsW = bp === 'compact' ? mainW : Math.min(44, Math.floor(mainW * 0.4));
89
+ const sb = c.box(0, y, statsW, lower, { title: 'ON-CHAIN' });
90
+ let sy = sb.y;
91
+ sy += statGrid(c, sb.x + 1, sy, sb.w - 2, [
92
+ { label: 'mcap', value: fmtUsd(tk.mcap, { digits: 1 }) },
93
+ { label: 'fdv', value: fmtUsd(tk.fdv, { digits: 1 }) },
94
+ { label: 'liquidity', value: fmtUsd(tk.liq, { digits: 1 }), sub: `${(tk.liq / tk.mcap * 100).toFixed(1)}% of mcap` },
95
+ { label: 'vol 24h', value: fmtUsd(tk.vol24, { digits: 1 }) },
96
+ { label: 'holders', value: fmtNum(tk.holders), sub: fmtDeltaDir(tk.holders24) + ' 24h' },
97
+ { label: 'organic', value: `${tk.org}/100`, style: { fg: tk.org >= 70 ? t.up : tk.org >= 40 ? t.warn : t.down, b: true } },
98
+ ], { bg: sb.bg, cols: 2 });
99
+ sy += 1;
100
+ const rows = [
101
+ ['Top 10 holders', `${tk.top10.toFixed(1)}%`, tk.top10 < 25 ? 'ok' : tk.top10 < 35 ? 'warn' : 'bad'],
102
+ ['Dev wallet', `${tk.dev.toFixed(2)}%`, tk.dev < 3 ? 'ok' : 'warn'],
103
+ ['Bundled supply', `${tk.bundled.toFixed(1)}%`, tk.bundled < 10 ? 'ok' : 'warn'],
104
+ ['Snipers (block 0-2)', String(tk.snipers), tk.snipers < 5 ? 'ok' : 'warn'],
105
+ ['LP locked / burned', `${tk.lpLocked}%`, tk.lpLocked >= 90 ? 'ok' : 'bad'],
106
+ ['Mint authority', tk.mintAuth ? 'ENABLED' : 'revoked', tk.mintAuth ? 'bad' : 'ok'],
107
+ ['Freeze authority', tk.freezeAuth ? 'ENABLED' : 'revoked', tk.freezeAuth ? 'bad' : 'ok'],
108
+ ['Dev history', `${tk.devHistory.launches} launches · ${tk.devHistory.rugged} rugs`, tk.devHistory.rugged ? 'warn' : 'ok'],
109
+ ['RugCheck score', `${tk.rug.score}/100`, tk.rug.score < 30 ? 'ok' : tk.rug.score < 60 ? 'warn' : 'bad'],
110
+ ];
111
+ for (const [label, val, lvl] of rows) {
112
+ if (sy >= sb.y + sb.h)
113
+ break;
114
+ const color = lvl === 'ok' ? t.up : lvl === 'warn' ? t.warn : t.down;
115
+ c.text(sb.x + 1, sy, lvl === 'ok' ? '●' : lvl === 'warn' ? '◐' : '○', { fg: color, bg: sb.bg });
116
+ c.text(sb.x + 3, sy, label, { fg: t.muted, bg: sb.bg }, Math.max(0, sb.w - 4 - val.length - 1));
117
+ c.textR(sb.x + 1, sy, sb.w - 2, val, { fg: lvl === 'ok' ? t.text : color, bg: sb.bg, b: lvl === 'bad' });
118
+ sy += 1;
119
+ }
120
+ if (sy + 1 < sb.y + sb.h)
121
+ c.rainZone(sb.x + 1, sy + 1, sb.w - 2, sb.y + sb.h - sy - 1, 2);
122
+ if (bp !== 'compact') {
123
+ const hx = statsW;
124
+ const hw = mainW - statsW;
125
+ const hb = c.box(hx, y, hw, lower, { title: 'HOLDERS', caption: 'holders · trades · traders (h/t/r)' });
126
+ let hy = hb.y;
127
+ c.text(hb.x + 1, hy, 'WALLET', { fg: t.dim, bg: hb.bg });
128
+ c.textR(hb.x + 1, hy, hb.w - 2, '% SOL', { fg: t.dim, bg: hb.bg });
129
+ hy += 1;
130
+ const maxPct = Math.max(...tk.holdersTop.map((h) => h.pct));
131
+ const barW = Math.max(6, hb.w - 36);
132
+ for (const h of tk.holdersTop) {
133
+ if (hy >= hb.y + hb.h)
134
+ break;
135
+ const cx = c.text(hb.x + 1, hy, shortAddr(h.addr, 4), { fg: t.text, bg: hb.bg });
136
+ if (h.kind)
137
+ badge(c, cx + 1, hy, h.kind.toUpperCase(), h.kind === 'dev' ? t.warn : h.kind === 'sniper' ? t.down : h.kind === 'LP' ? t.accent : t.muted, { on: hb.bg });
138
+ const bx = hb.x + 22;
139
+ bar(c, bx, hy, barW, (h.pct / maxPct) * 100, { bg: hb.bg, color: h.kind === 'LP' ? t.accent : h.pct > 5 ? t.warn : t.up });
140
+ c.textR(hb.x + 1, hy, hb.w - 2, `${h.pct.toFixed(2).padStart(5)} ${fmtNum(h.sol).padStart(6)}`, { fg: h.pct > 5 ? t.warn : t.muted, bg: hb.bg });
141
+ hy += 1;
142
+ }
143
+ // trade tape: text rows on the left, falling rain lines on the right
144
+ if (hy + 3 < hb.y + hb.h) {
145
+ hy += 1;
146
+ c.hline(hb.x, hy - 1, hb.w, { fg: t.border, bg: hb.bg, soft: true });
147
+ c.text(hb.x + 1, hy, 'TAPE', { fg: t.dim, bg: hb.bg });
148
+ c.heading(hb.x + 1, hy, 4);
149
+ c.textR(hb.x + 1, hy, hb.w - 2, 'live · ws', { fg: t.dim, bg: hb.bg });
150
+ hy += 1;
151
+ const tapeTop = hy;
152
+ const textW = 46;
153
+ const rainX = hb.x + textW + 2;
154
+ const rainW = hb.x + hb.w - 1 - rainX;
155
+ for (const tr of tk.trades) {
156
+ if (hy >= hb.y + hb.h)
157
+ break;
158
+ const color = tr.side === 'buy' ? t.up : t.down;
159
+ let cx = c.text(hb.x + 1, hy, fmtAgeSec(tr.ago).padStart(4), { fg: t.dim, bg: hb.bg });
160
+ cx = c.text(cx + 1, hy, tr.side === 'buy' ? '▲BUY ' : '▼SELL', { fg: color, bg: hb.bg, b: true });
161
+ cx = c.textR(cx + 1, hy, 8, fmtUsd(tr.usd), { fg: t.text, bg: hb.bg });
162
+ cx = c.text(cx + 2, hy, fmtPrice(tr.price), { fg: t.muted, bg: hb.bg });
163
+ cx = c.text(cx + 2, hy, shortAddr(tr.addr, 3), { fg: t.dim, bg: hb.bg });
164
+ if (tr.tag)
165
+ badge(c, cx + 1, hy, tr.tag, tr.tag === 'smart' ? t.accent : t.warn, { on: hb.bg });
166
+ hy += 1;
167
+ }
168
+ const tapeH = hb.y + hb.h - tapeTop;
169
+ if (rainW >= 6 && tapeH >= 3)
170
+ tapeRain(c, rainX, tapeTop, rainW, tapeH, tk.trades, v.frame, { bg: hb.bg });
171
+ }
172
+ }
173
+ }
174
+ if (showAi)
175
+ analystPanel(c, mainW, top, aiW, bottom - top, tk, state);
176
+ footer(c, state.tokenSocial
177
+ ? [['esc', 'back'], ['s', 'on-chain'], ['f', 'filter'], ['x', state.socialShowSpam ? 'hide spam' : 'show hidden'], ['K', 'KOL lists'], ['a', showAi ? 'hide analyst' : 'analyst'], ['w', 'watch'], ['q', 'quit']]
178
+ : [['esc', 'back'], ['g', 'go to'], ['a', showAi ? 'hide analyst' : 'analyst'], ['h/t/r', 'holders/trades/traders'], ['w', 'watch'], ['b', 'alert'], ['1-6', 'timeframe'], ['q', 'quit']], state.theme.name, isCalm(v));
179
+ }
180
+ // ---------------- AI analyst panel (coral for the verdict and tool-call lines; answers decrypt in) ----------------
181
+ export function analystPanel(c, x, y, w, h, tk, state) {
182
+ if (state.analyst) {
183
+ renderAnalystSession(c, x, y, w, h, state.analyst, {
184
+ frame: state.vfx.frame, code: state.vfx.code, calm: isCalm(state.vfx),
185
+ chips: [`$${tk.symbol}`, 'holders:20', 'trades:1h', 'rugcheck', 'candles'],
186
+ input: state.aiInput ?? '', inputFocused: state.aiFocused === true,
187
+ scroll: state.aiScroll, revealAll: state.aiRevealAll,
188
+ inputHint: `press i to ask about $${tk.symbol}… read-only · never trades`,
189
+ });
190
+ return;
191
+ }
192
+ const t = c.theme;
193
+ const bc = mix(t.border, t.ai, 0.55);
194
+ const box = c.box(x, y, w, h, { title: '✦ ANALYST · claude', titleFg: t.ai, caption: 'claude -p · 4 tools · local', captionFg: t.dim, border: bc });
195
+ const bg = box.bg;
196
+ const inner = box.w - 2;
197
+ let cy = box.y;
198
+ // context chips
199
+ let cx = box.x + 1;
200
+ for (const chip of [`$${tk.symbol}`, 'holders:20', 'trades:1h', 'rugcheck', 'screener']) {
201
+ if (cx + chip.length + 3 > box.x + box.w - 1)
202
+ break;
203
+ cx = badge(c, cx, cy, chip, t.ai, { on: bg }) + 1;
204
+ }
205
+ cy += 2;
206
+ const lines = state.transcript || aiTranscript(tk);
207
+ const inputY = box.y + box.h - 2;
208
+ const ai = state.ai ?? { phase: 'done', cut: lines.length, revealed: 0 };
209
+ const frame = state.vfx.frame;
210
+ const code = state.vfx.code;
211
+ const cut = ai.phase === 'done' ? lines.length : ai.cut;
212
+ let revealedLeft = ai.revealed;
213
+ // text writer that is plain for settled lines and decrypting for the streaming line
214
+ const write = (lx, ly, str, st, maxW, streaming) => {
215
+ if (!streaming) {
216
+ c.text(lx, ly, str, st, maxW);
217
+ return true;
218
+ }
219
+ const n = Math.min(revealedLeft, str.length);
220
+ decryptText(c, lx, ly, str, n, frame, st, maxW, code);
221
+ revealedLeft -= str.length + 1;
222
+ return revealedLeft > -6; // keep drawing the scramble window
223
+ };
224
+ for (let li = 0; li < lines.length; li++) {
225
+ const ln = lines[li];
226
+ if (cy >= inputY - 1)
227
+ break;
228
+ if (li > cut && revealedLeft <= 0)
229
+ break; // streaming budget spent: later messages are not here yet
230
+ const streaming = li >= cut;
231
+ if (streaming && ai.phase === 'thinking') {
232
+ // Claude-Code-style spinner line
233
+ c.text(box.x + 2, cy, spinnerGlyph(frame), { fg: t.ai, bg, b: true });
234
+ const status = ['Thinking', 'Reading holders', 'Checking rugcheck', 'Decrypting'][Math.floor(frame / 24) % 4];
235
+ c.text(box.x + 4, cy, `${status}…`, { fg: t.ai, bg, b: true });
236
+ c.text(box.x + 5 + status.length, cy, `(esc to interrupt · ${(frame / 30).toFixed(1)}s · ↓ ${Math.floor(frame * 11)} tokens)`, { fg: t.dim, bg }, inner - status.length - 6);
237
+ cy += 1;
238
+ break;
239
+ }
240
+ if (ln.role === 'user') {
241
+ c.text(box.x + 1, cy, '›', { fg: t.accent, bg, b: true });
242
+ for (const w2 of wrap(ln.text, inner - 3)) {
243
+ if (cy >= inputY - 1)
244
+ break;
245
+ c.text(box.x + 3, cy, w2, { fg: t.text, bg, b: true });
246
+ cy += 1;
247
+ }
248
+ cy += 1;
249
+ }
250
+ else if (ln.role === 'tool') {
251
+ c.text(box.x + 3, cy, '↳ ' + ln.text, { fg: t.ai, bg, i: true }, inner - 3);
252
+ cy += 1;
253
+ }
254
+ else if ('verdict' in ln && ln.verdict) {
255
+ const vc = ln.verdict === 'watch' ? t.up : ln.verdict === 'avoid' ? t.down : t.warn;
256
+ c.fill(box.x + 1, cy, inner, 1, ' ', { bg: mix(bg, vc, 0.18) });
257
+ c.text(box.x + 2, cy, `● ${ln.verdict.toUpperCase()}`, { fg: vc, bg: mix(bg, vc, 0.18), b: true });
258
+ c.textR(box.x + 1, cy, inner - 1, `conf ${ln.verdict === 'watch' ? '0.62' : ln.verdict === 'avoid' ? '0.81' : '0.44'} `, { fg: t.muted, bg: mix(bg, vc, 0.18) });
259
+ cy += 1;
260
+ for (const w2 of wrap(ln.text, inner - 2)) {
261
+ if (cy >= inputY - 1)
262
+ break;
263
+ if (!write(box.x + 2, cy, w2, { fg: t.ai, bg }, inner - 2, streaming))
264
+ break;
265
+ cy += 1;
266
+ }
267
+ cy += 1;
268
+ }
269
+ else if ('bullets' in ln) {
270
+ let stop = false;
271
+ for (const b of ln.bullets) {
272
+ if (stop)
273
+ break;
274
+ const ws = wrap(b, inner - 4);
275
+ for (let i = 0; i < ws.length; i++) {
276
+ if (cy >= inputY - 1) {
277
+ stop = true;
278
+ break;
279
+ }
280
+ c.text(box.x + 2, cy, i === 0 ? '•' : ' ', { fg: t.ai, bg });
281
+ if (!write(box.x + 4, cy, ws[i], { fg: t.text, bg }, inner - 4, streaming)) {
282
+ stop = true;
283
+ cy += 1;
284
+ break;
285
+ }
286
+ cy += 1;
287
+ }
288
+ }
289
+ cy += 1;
290
+ }
291
+ else {
292
+ for (const w2 of wrap(ln.text, inner - 2)) {
293
+ if (cy >= inputY - 1)
294
+ break;
295
+ if (!write(box.x + 2, cy, w2, { fg: t.text, bg }, inner - 2, streaming)) {
296
+ cy += 1;
297
+ break;
298
+ }
299
+ cy += 1;
300
+ }
301
+ cy += 1;
302
+ }
303
+ }
304
+ if (inputY - 2 > cy)
305
+ c.rainZone(box.x + 1, cy, inner, inputY - 2 - cy, 2);
306
+ // guard rail line
307
+ c.hline(box.x, inputY - 1, box.w, { fg: bc, bg, soft: true });
308
+ c.put(box.x, inputY - 1, '├', { fg: bc, bg, soft: true });
309
+ c.put(box.x + box.w - 1, inputY - 1, '┤', { fg: bc, bg, soft: true });
310
+ c.text(box.x + 1, inputY, '▌', { fg: t.ai, bg, b: true, soft: true });
311
+ c.text(box.x + 2, inputY, state.aiInput || `ask about $${tk.symbol}… read-only · never trades`, { fg: state.aiInput ? t.text : t.dim, bg, i: !state.aiInput }, inner - 2);
312
+ }
@@ -0,0 +1,181 @@
1
+ // dexterm — TRENDING IN THE SPACE (screen T, task-1330; INTEGRATIONS-DESIGN §5.3, mockups trending-feed-*.txt).
2
+ // Rank = velocity × credibility, computed locally from the keyless venues + whatever posts the feed holds.
3
+ // Sub-panels: NARRATIVES (clusters by shared KOL threads + co-mention; label from the analyst/user, never a
4
+ // single post) and NEWS (official RSS, "context, not signal"). `s` swaps NARRATIVES for the SOURCES panel
5
+ // (health + ages + errors). Without X the header band says `x-stream ○ logged out` and the board still works.
6
+ import { Canvas, table, badge, header, footer, breakpoint, mix, fmtUsd, fmtAgeSec } from '../../../tui/src/index.js';
7
+ import { headerModel } from "../state.js";
8
+ import { socialOf, kolOf, trendingRows, fmtVelocity, sourcesLine, newsTagLabel } from "../social/view.js";
9
+ const dash = '—';
10
+ export function renderTrending(c, state) {
11
+ const t = c.theme;
12
+ const bp = breakpoint(c.w);
13
+ header(c, headerModel(state));
14
+ const now = state.now.getTime();
15
+ const feed = socialOf(state);
16
+ const top = 2, bottom = c.h - 2; // last row above the footer carries the model-output line
17
+ // row 1 band: the ranking rule + source ages
18
+ c.fill(0, 1, c.w, 1, ' ', { bg: t.bg });
19
+ c.noRain(0, 1, c.w, 1);
20
+ let bx = c.text(1, 1, 'TRENDING IN THE SPACE', { fg: t.borderFocus, bg: t.bg, b: true });
21
+ bx = c.text(bx, 1, ' · rank = velocity × credibility · window 1h · ', { fg: t.dim, bg: t.bg }, Math.max(0, c.w - bx - 1));
22
+ if (feed)
23
+ c.text(bx, 1, sourcesLine(feed), { fg: t.muted, bg: t.bg }, Math.max(0, c.w - bx - 1));
24
+ if (!feed) {
25
+ const box = c.box(0, top, c.w, bottom - top, { title: 'TRENDING', caption: 'no social feed in this mode' });
26
+ c.textC(box.x, box.y + 1, box.w, 'the trending board needs live, replay or fixture mode', { fg: t.muted, bg: box.bg, i: true });
27
+ footer(c, [['esc', 'back'], ['q', 'quit']], t.name);
28
+ return;
29
+ }
30
+ const all = feed.trendRows();
31
+ const rows = trendingRows(state);
32
+ const st = feed.status();
33
+ const nar = state.socialNarrative ?? 0;
34
+ const narratives = feed.narratives();
35
+ const narName = nar > 0 ? (narratives[nar - 1]?.label ?? narratives[nar - 1]?.symbols.slice(0, 3).join(' ') ?? '') : '';
36
+ const filter = state.socialFilter ?? 'all';
37
+ const hiddenCap = `hidden: ${st.hiddenSpam} spam cluster post${st.hiddenSpam === 1 ? '' : 's'}, ${st.hiddenSquats} unverified cashtag squat${st.hiddenSquats === 1 ? '' : 's'}${st.hiddenInjection ? `, ${st.hiddenInjection} withheld (firewall)` : ''}`;
38
+ const title = `TRENDING · ${rows.length} of ${all.length}${filter !== 'all' ? ` · filter ${filter}` : ''}${state.socialAuthor ? ` · @${state.socialAuthor} only` : ''}${narName ? ` · narrative ${narName}` : ''} · ${hiddenCap}`;
39
+ const subH = bp === 'compact' ? Math.min(12, Math.max(6, Math.floor((bottom - top) * 0.4))) : Math.max(6, Math.floor((bottom - top) * 0.36));
40
+ const boardH = bottom - top - subH;
41
+ const box = c.box(0, top, c.w, boardH, { title, caption: bp === 'compact' ? undefined : `${state.socialShowSpam ? 'x=hide' : 'x=show hidden'} · f=filter · n=narrative · s=sources · a=summary`, focus: true });
42
+ const bg = box.bg;
43
+ const sel = Math.max(0, Math.min(rows.length - 1, state.selected || 0));
44
+ const scroll = Math.max(0, sel - (box.h - 3));
45
+ const credStyle = (n) => ({ fg: n >= 70 ? t.up : n >= 40 ? t.text : n >= 20 ? t.muted : t.down, bg });
46
+ const cols = [
47
+ { key: 'rank', label: '#', w: 2, align: 'right', prio: 0, render: (cv, x, y, w, _r, rb, m) => { cv.textR(x, y, w, String(m.index + 1), { fg: m.selected ? t.borderFocus : t.dim, bg: rb }); } },
48
+ { key: 'token', label: 'token', w: 8, prio: 0, render: (cv, x, y, w, r, rb, m) => { cv.text(x, y, r.symbol, { fg: m.selected ? t.borderFocus : t.text, bg: rb, b: true }, w); } },
49
+ { key: 'mint', label: 'mint', w: 11, prio: 3, render: (cv, x, y, w, r, rb) => { cv.text(x, y, `${r.mint.slice(0, 4)}…${r.mint.slice(-4)}`, { fg: t.dim, bg: rb }, w); } },
50
+ { key: 'narrative', label: bp === 'compact' ? 'narr' : 'narrative', w: bp === 'compact' ? 9 : 12, prio: 2, render: (cv, x, y, w, r, rb) => { cv.text(x, y, r.narrative ?? '', { fg: t.muted, bg: rb, i: true }, w); } },
51
+ { key: 'vel', label: 'vel', w: 5, align: 'right', prio: 0, render: (cv, x, y, w, r, rb) => { cv.textR(x, y, w, fmtVelocity(r.velocity, r.velocityBasis), { fg: r.velocity === null ? t.dim : r.velocity >= 5 ? t.up : r.velocity >= 2 ? t.text : t.muted, bg: rb, b: (r.velocity ?? 0) >= 5 }); } },
52
+ { key: 'pph', label: bp === 'compact' ? 'p/h' : 'posts/h', w: bp === 'compact' ? 5 : 7, align: 'right', prio: 1, render: (cv, x, y, w, r, rb) => { cv.textR(x, y, w, r.postsPerHour ? String(r.postsPerHour) : dash, { fg: t.text, bg: rb }); } },
53
+ { key: 'kol', label: 'kol', w: 3, align: 'right', prio: 1, render: (cv, x, y, w, r, rb) => { cv.textR(x, y, w, String(r.kolCount), { fg: r.kolCount ? t.accent : t.dim, bg: rb }); } },
54
+ { key: 'cred', label: bp === 'compact' ? 'cr' : 'cred', w: bp === 'compact' ? 3 : 4, align: 'right', prio: 0, render: (cv, x, y, w, r, rb) => { cv.textR(x, y, w, String(r.credibility), { ...credStyle(r.credibility), bg: rb }); } },
55
+ { key: 'px1h', label: 'px 1h', w: 6, align: 'right', prio: 1, render: (cv, x, y, w, r, rb) => { const v = r.priceChange1h; cv.textR(x, y, w, v === null ? dash : `${v >= 0 ? '+' : ''}${Math.abs(v) >= 100 ? Math.round(v) : v.toFixed(0)}%`, { fg: v === null ? t.dim : v >= 0 ? t.up : t.down, bg: rb }); } },
56
+ { key: 'liq', label: 'liq', w: 6, align: 'right', prio: 2, render: (cv, x, y, w, r, rb) => { cv.textR(x, y, w, r.liq === null ? dash : fmtUsd(r.liq, { digits: 1 }), { fg: t.muted, bg: rb }); } },
57
+ { key: 'sources', label: bp === 'compact' ? 'src' : 'sources', w: bp === 'compact' ? 12 : 15, prio: 1, render: (cv, x, y, w, r, rb) => { const s = r.sources.join('·'); cv.text(x, y, s || dash, { fg: r.venue?.boosted ? t.warn : t.muted, bg: rb }, w); } },
58
+ { key: 'age', label: 'age', w: 4, align: 'right', prio: 2, render: (cv, x, y, w, r, rb) => { cv.textR(x, y, w, fmtAgeSec(Math.max(0, Math.round((now - r.updatedAt) / 1000))), { fg: t.dim, bg: rb }); } },
59
+ { key: 'latest', label: 'latest credible post (firewalled)', w: 22, prio: 4, flex: true, render: (cv, x, y, w, r, rb) => {
60
+ const p = r.latestCrediblePost;
61
+ if (!p) {
62
+ cv.text(x, y, r.venue ? `— (no credible post; ${r.venue.sources.join('+')} only)` : dash, { fg: t.dim, bg: rb, i: true }, w);
63
+ return;
64
+ }
65
+ const who = p.platform === 'news' ? p.author.handle : p.author.handle;
66
+ const cx = cv.text(x, y, `${who}: `, { fg: p.author.tier.startsWith('kol') ? t.accent : t.muted, bg: rb }, w);
67
+ cv.text(cx, y, p.text, { fg: p.platform === 'chain' ? t.up : t.text, bg: rb }, Math.max(0, w - (cx - x)));
68
+ } },
69
+ ];
70
+ if (rows.length === 0)
71
+ c.textC(box.x, box.y + 2, box.w, all.length ? 'no rows match the filter — f cycles filters, n clears the narrative' : 'nothing trending yet — the keyless venues refresh every 60 s', { fg: t.muted, bg, i: true });
72
+ else
73
+ table(c, box.x + 1, box.y, box.w - 2, box.h, { columns: cols, rows, selected: sel, scroll, bg });
74
+ // sub-panels
75
+ const sy = top + boardH;
76
+ const news = feed.newsItems(12);
77
+ if (bp === 'compact') {
78
+ const half = Math.floor(subH / 2);
79
+ if (state.socialSources)
80
+ renderSources(c, 0, sy, c.w, subH, state, feed);
81
+ else {
82
+ renderNarratives(c, 0, sy, c.w, half, narratives, nar, bg);
83
+ renderNews(c, 0, sy + half, c.w, subH - half, news, now);
84
+ }
85
+ }
86
+ else {
87
+ const leftW = Math.floor(c.w * 0.5);
88
+ if (state.socialSources)
89
+ renderSources(c, 0, sy, leftW, subH, state, feed);
90
+ else
91
+ renderNarratives(c, 0, sy, leftW, subH, narratives, nar, bg);
92
+ renderNews(c, leftW, sy, c.w - leftW, subH, news, now);
93
+ }
94
+ // the model-output line above the footer
95
+ const kol = kolOf(state);
96
+ c.fill(0, bottom, c.w, 1, ' ', { bg: t.bg });
97
+ c.noRain(0, bottom, c.w, 1);
98
+ c.text(1, bottom, `✦ model output · KOL lists: ${kol?.summary() ?? 'none'} · credibility = early-entry score × follower quality × on-chain confirmation; never from follower counts alone`, { fg: t.dim, bg: t.bg, i: true }, c.w - 2);
99
+ footer(c, [['⏎', 'open token'], ['n', 'narrative'], ['K', 'KOL lists'], ['f', 'filter'], ['w', 'watch'], ['s', state.socialSources ? 'narratives' : 'sources'], ['x', state.socialShowSpam ? 'hide spam' : 'show hidden'], ['a', 'summary'], ['r', 'refresh'], ['esc', 'back'], ['q', 'quit']], t.name);
100
+ }
101
+ function renderNarratives(c, x, y, w, h, narratives, active, _bg) {
102
+ const t = c.theme;
103
+ const box = c.box(x, y, w, h, { title: 'NARRATIVES · 1h', caption: active ? 'n=next · filtering the board' : 'clusters by shared KOL threads + co-mention · label from the analyst, never one post' });
104
+ let cy = box.y;
105
+ if (narratives.length === 0) {
106
+ c.textC(box.x, cy, box.w, 'no cross-token clusters yet (needs ≥ 2 tokens sharing a KOL thread or a post)', { fg: t.dim, bg: box.bg, i: true });
107
+ return;
108
+ }
109
+ const maxVel = Math.max(1, ...narratives.map((n) => n.velocity ?? 0));
110
+ narratives.forEach((n, i) => {
111
+ if (cy >= box.y + box.h)
112
+ return;
113
+ const sel = active === i + 1;
114
+ const label = (n.label ?? 'unlabelled').padEnd(12).slice(0, 12);
115
+ let cx = c.text(box.x + 1, cy, label, { fg: sel ? t.borderFocus : n.label ? t.text : t.dim, bg: box.bg, b: sel, i: !n.label });
116
+ const barW = Math.max(2, Math.min(10, Math.round(((n.velocity ?? 0) / maxVel) * 10)));
117
+ cx = c.text(cx + 1, cy, '▇'.repeat(barW).padEnd(10), { fg: mix(box.bg, t.accent, 0.8), bg: box.bg, soft: true });
118
+ const d = n.delta24h;
119
+ cx = c.textR(cx + 1, cy, 6, d === null ? dash : `${d >= 0 ? '+' : ''}${d.toFixed(0)}%`, { fg: d === null ? t.dim : d >= 0 ? t.up : t.down, bg: box.bg });
120
+ const tail = `${n.symbols.slice(0, 4).join(' ')} · ${n.kolThreads} KOL thread${n.kolThreads === 1 ? '' : 's'}${n.newsCount ? ` · news: ${n.newsCount}` : ''}${n.velocity !== null ? ` · ${fmtVelocity(n.velocity)}` : ''}`;
121
+ c.text(cx + 1, cy, tail, { fg: t.muted, bg: box.bg }, Math.max(0, box.x + box.w - cx - 2));
122
+ cy += 1;
123
+ });
124
+ if (cy + 1 < box.y + box.h)
125
+ c.rainZone(box.x + 1, cy + 1, box.w - 2, box.y + box.h - cy - 1, 2);
126
+ }
127
+ function renderNews(c, x, y, w, h, news, now) {
128
+ const t = c.theme;
129
+ const box = c.box(x, y, w, h, { title: 'NEWS · official RSS', caption: 'context, not signal' });
130
+ let cy = box.y;
131
+ if (news.length === 0) {
132
+ c.textC(box.x, cy, box.w, 'no headlines yet (RSS polls every 10 min)', { fg: t.dim, bg: box.bg, i: true });
133
+ return;
134
+ }
135
+ for (const n of news) {
136
+ if (cy >= box.y + box.h)
137
+ break;
138
+ const tag = newsTagLabel(n);
139
+ let cx = c.text(box.x + 1, cy, fmtAgeSec(Math.max(0, Math.round((now - n.post.t) / 1000))).padStart(3), { fg: t.dim, bg: box.bg });
140
+ cx = c.text(cx + 1, cy, n.domain.replace(/\..*$/, '').slice(0, 10).padEnd(10), { fg: t.muted, bg: box.bg });
141
+ const room = box.x + box.w - 1 - cx - 1 - tag.length - 1;
142
+ const headline = n.post.text.length > room ? n.post.text.slice(0, Math.max(1, room - 2)) + ' …' : n.post.text;
143
+ cx = c.text(cx + 1, cy, headline, { fg: n.tag === 'cross-ref' ? t.text : t.muted, bg: box.bg }, Math.max(0, room));
144
+ c.textR(box.x + 1, cy, box.w - 2, tag, { fg: n.tag === 'security' ? t.down : n.tag === 'cross-ref' ? t.accent : t.dim, bg: box.bg });
145
+ cy += 1;
146
+ }
147
+ if (cy + 1 < box.y + box.h)
148
+ c.rainZone(box.x + 1, cy + 1, box.w - 2, box.y + box.h - cy - 1, 2);
149
+ }
150
+ function renderSources(c, x, y, w, h, state, feed) {
151
+ const t = c.theme;
152
+ const st = feed.status();
153
+ const box = c.box(x, y, w, h, { title: 'SOURCES · keyless', caption: 'health · age · limiter' });
154
+ let cy = box.y;
155
+ const fmt = (ms) => (ms === null ? dash : ms < 60_000 ? `${Math.round(ms / 1000)}s` : ms < 3_600_000 ? `${Math.round(ms / 60_000)}m` : `${Math.round(ms / 3_600_000)}h`);
156
+ const buckets = state.live ? state.live.dl.limiter.stats() : [];
157
+ const bucketOf = { jup: 'jupiter', gecko: 'geckoterminal', boost: 'dexscreener-slow', cg: 'coingecko', news: 'news-rss' };
158
+ for (const s of st.sources) {
159
+ if (cy >= box.y + box.h)
160
+ break;
161
+ const glyph = s.state === 'ok' ? '●' : s.state === 'error' ? '◐' : '○';
162
+ const fg = s.state === 'ok' ? t.up : s.state === 'error' ? t.warn : t.dim;
163
+ let cx = c.text(box.x + 1, cy, glyph, { fg, bg: box.bg });
164
+ cx = c.text(cx + 1, cy, s.label.padEnd(10), { fg: t.text, bg: box.bg });
165
+ const xs = s.id === 'x' ? feed.xStatus() : null;
166
+ const detail = xs ? (xs.state === 'ok' ? `${xs.detail}${s.ageMs !== null ? ` · age ${fmt(s.ageMs)}` : ' · nothing read yet'} · ${xs.requests} req this run` : xs.detail)
167
+ : s.state === 'off' ? 'off in this mode' : s.state === 'error' ? `error: ${s.error ?? '?'} · last ok ${fmt(s.ageMs)}` : s.state === 'never' ? 'not fetched yet' : `age ${fmt(s.ageMs)}`;
168
+ cx = c.text(cx + 1, cy, detail, { fg: s.state === 'error' ? t.warn : t.muted, bg: box.bg }, Math.max(0, box.x + box.w - cx - 2));
169
+ const b = buckets.find((k) => k.bucket === (s.id === 'x' ? 'x-api' : bucketOf[s.id]));
170
+ if (b)
171
+ c.textR(box.x + 1, cy, box.w - 2, `${b.rpmUsed}/${b.budget} rpm${b.pausedUntil ? ' · paused' : ''}`, { fg: t.dim, bg: box.bg });
172
+ cy += 1;
173
+ }
174
+ if (cy < box.y + box.h) {
175
+ c.text(box.x + 1, cy, `posts stored ${st.posts} (24 h) · hidden spam ${st.hiddenSpam} · squats ${st.hiddenSquats} · withheld ${st.hiddenInjection}`, { fg: t.dim, bg: box.bg, i: true }, box.w - 2);
176
+ cy += 1;
177
+ }
178
+ if (cy + 1 < box.y + box.h)
179
+ c.rainZone(box.x + 1, cy + 1, box.w - 2, box.y + box.h - cy - 1, 2);
180
+ }
181
+ export { badge };
@@ -0,0 +1,104 @@
1
+ // dexterm — Watchlist + alerts log + analyst digest.
2
+ // Cyberspace styling (task-1297): alert glyphs ▲/▼/✦/◆ beside the badges, claude notes and the digest
3
+ // headline in coral, rain zones under the alerts and the digest, ▲ on holder deltas.
4
+ import { Canvas, table, sparkline, heatPct, launchpadBadge, badge, fmtUsd, fmtPrice, fmtAge, wrap, header, footer, breakpoint, mix, isCalm, } from '../../../tui/src/index.js';
5
+ import { priceModeOf, promoteMcap, supplyRatio, mcapAt, priceCell, fallbackCount, fallbackCaption, FALLBACK_MARK } from "../display.js";
6
+ import { headerModel } from "../state.js";
7
+ import { renderWatchlistLive } from "../live/screens/watchlist-live.js";
8
+ export function renderWatchlist(c, state) {
9
+ if (state.live && state.screener.source !== 'fixtures')
10
+ return renderWatchlistLive(c, state, state.live.watch); // task-1298: live data path
11
+ const t = c.theme;
12
+ const bp = breakpoint(c.w);
13
+ header(c, headerModel(state));
14
+ const top = 2, bottom = c.h - 1;
15
+ const sel = state.selected || 0;
16
+ const rows = state.fx.watch;
17
+ const listH = Math.max(8, Math.min(rows.length + 4, Math.floor((bottom - top) * 0.5)));
18
+ const mode = priceModeOf(state); // task-1366
19
+ const fb = mode === 'mcap' ? fallbackCaption(fallbackCount(rows, (r) => ({ price: r.token.price, mcap: r.token.mcap }))) : null;
20
+ const box = c.box(0, top, c.w, listH, { title: 'WATCHLIST', caption: `${rows.length} tokens · PnL vs entry${fb ? ` · ${fb}` : ''}` });
21
+ const cols = [
22
+ { key: 'token', label: 'TOKEN', w: 22, prio: 0, flex: true, render: (cv, x, y, w, r, bg, m) => {
23
+ const cx = cv.text(x, y, r.token.symbol, { fg: m.selected ? t.borderFocus : t.text, bg, b: true }, 10);
24
+ cv.text(cx + 1, y, r.token.name, { fg: t.muted, bg }, w - (cx - x) - 1);
25
+ } },
26
+ { key: 'lp', label: 'LP', w: 6, prio: 3, render: (cv, x, y, _w, r, bg) => { launchpadBadge(cv, x, y, r.token.launchpad, { on: bg }); } },
27
+ { key: 'price', label: 'PRICE', w: 11, align: 'right', prio: 0, render: (cv, x, y, w, r, bg) => { cv.textR(x, y, w, fmtPrice(r.token.price), { fg: t.text, bg }); } },
28
+ // task-1366: in mcap mode the entry reads as the market cap at entry (entry price × the CURRENT supply ratio)
29
+ { key: 'entry', label: mode === 'mcap' ? 'ENTRY MC' : 'ENTRY', w: 11, align: 'right', prio: 2, render: (cv, x, y, w, r, bg) => {
30
+ const ratio = mode === 'mcap' ? supplyRatio(r.token.price, r.token.mcap) : null;
31
+ cv.textR(x, y, w, ratio !== null ? fmtUsd(mcapAt(r.entry, ratio), { digits: 1 }) : fmtPrice(r.entry) + (mode === 'mcap' ? FALLBACK_MARK : ''), { fg: t.muted, bg });
32
+ } },
33
+ { key: 'pnl', label: 'VS ENTRY', w: 9, align: 'right', prio: 0, render: (cv, x, y, w, r, bg) => heatPct(cv, x, y, w, (r.token.price / r.entry - 1) * 100, bg) },
34
+ { key: 'h1', label: '1H', w: 7, align: 'right', prio: 1, render: (cv, x, y, w, r, bg) => heatPct(cv, x, y, w, r.token.chg.h1, bg) },
35
+ { key: 'h24', label: '24H', w: 8, align: 'right', prio: 1, render: (cv, x, y, w, r, bg) => heatPct(cv, x, y, w, r.token.chg.h24, bg) },
36
+ { key: 'mcap', label: 'MCAP', w: mode === 'mcap' ? 9 : 8, align: 'right', prio: 1, render: (cv, x, y, w, r, bg) => {
37
+ const pc = priceCell(mode, r.token.price, r.token.mcap);
38
+ if (pc.fallback)
39
+ cv.textR(x, y, w, pc.text + FALLBACK_MARK, { fg: t.warn, bg });
40
+ else
41
+ cv.textR(x, y, w, fmtUsd(r.token.mcap, { digits: 1 }), { fg: t.text, bg });
42
+ } },
43
+ { key: 'liq', label: 'LIQ', w: 8, align: 'right', prio: 2, render: (cv, x, y, w, r, bg) => { cv.textR(x, y, w, fmtUsd(r.token.liq, { digits: 1 }), { fg: t.text, bg }); } },
44
+ { key: 'spark', label: '24H TREND', w: 16, prio: 1, render: (cv, x, y, w, r, bg) => sparkline(cv, x, y, w, r.token.spark, { bg }) },
45
+ { key: 'note', label: 'NOTE', w: 30, prio: 2, render: (cv, x, y, w, r, bg) => { cv.text(x, y, r.note, { fg: r.note.startsWith('claude') ? t.ai : t.muted, bg, i: true }, w); } },
46
+ { key: 'added', label: 'ADDED', w: 6, align: 'right', prio: 4, render: (cv, x, y, w, r, bg) => { cv.textR(x, y, w, fmtAge(r.added), { fg: t.dim, bg }); } },
47
+ ];
48
+ table(c, box.x + 1, box.y, box.w - 2, box.h, { columns: promoteMcap(cols, mode), rows, selected: sel, bg: box.bg });
49
+ const y2 = top + listH;
50
+ const h2 = bottom - y2;
51
+ const alertsW = bp === 'compact' ? c.w : Math.floor(c.w * 0.58);
52
+ const ab = c.box(0, y2, alertsW, h2, { title: 'ALERTS', caption: 'price · holders · risk · smart money · claude' });
53
+ let ay = ab.y;
54
+ for (const a of state.fx.alerts) {
55
+ if (ay >= ab.y + ab.h)
56
+ break;
57
+ const color = a.level === 'up' ? t.up : a.level === 'down' ? t.down : a.level === 'ai' ? t.ai : t.accent;
58
+ const glyph = a.level === 'up' ? '▲' : a.level === 'down' ? '▼' : a.level === 'ai' ? '✦' : '◆';
59
+ let cx = c.text(ab.x + 1, ay, fmtAge(a.ago).padStart(4), { fg: t.dim, bg: ab.bg });
60
+ cx = c.text(cx + 1, ay, glyph, { fg: color, bg: ab.bg, b: true });
61
+ cx = badge(c, cx + 1, ay, a.kind.toUpperCase().padEnd(9), color, { on: ab.bg }) + 1;
62
+ c.text(cx, ay, a.text, { fg: a.level === 'ai' ? t.ai : t.text, bg: ab.bg }, ab.w - (cx - ab.x) - 1);
63
+ ay += 1;
64
+ }
65
+ if (ay + 1 < ab.y + ab.h)
66
+ c.rainZone(ab.x + 1, ay + 1, ab.w - 2, ab.y + ab.h - ay - 1, 2);
67
+ if (bp !== 'compact') {
68
+ const rb = c.box(alertsW, y2, c.w - alertsW, h2, { title: '✦ ANALYST DIGEST', titleFg: t.ai, caption: 'every 15m · claude -p', border: mix(t.border, t.ai, 0.55) });
69
+ const lines = [
70
+ ['head', 'Watchlist health: 4 of 6 intact'],
71
+ ['', ''],
72
+ ['ok', `${rows[0].token.symbol}: momentum intact, holders ▲${Math.abs(rows[0].token.holders24)} 24h`],
73
+ ['ok', `${rows[1].token.symbol}: consolidating above entry, no dev sells`],
74
+ ['warn', `${rows[2].token.symbol}: volume fading, thesis needs a catalyst`],
75
+ ['bad', `${rows[4].token.symbol}: wash-trade signature (organic ${rows[4].token.org}), consider dropping`],
76
+ ['ok', `${rows[3].token.symbol}: smart wallets accumulating (3 tagged buys)`],
77
+ ['', ''],
78
+ ['dim', 'Suggested: alert on CATWIF liq < $80K; review SNAIL at 1.2M'],
79
+ ['dim', 'I do not trade. Confirm any action in the Trade panel (v2).'],
80
+ ];
81
+ let ry = rb.y;
82
+ for (const [kind, text] of lines) {
83
+ if (ry >= rb.y + rb.h)
84
+ break;
85
+ const fg = kind === 'head' ? t.ai : kind === 'ok' ? t.up : kind === 'warn' ? t.warn : kind === 'bad' ? t.down : t.dim;
86
+ const mark = kind === 'ok' ? '●' : kind === 'warn' ? '◐' : kind === 'bad' ? '○' : ' ';
87
+ const indent = kind && kind !== 'head' && kind !== 'dim' ? 3 : 1;
88
+ const parts = text ? wrap(text, rb.w - 1 - indent) : [''];
89
+ parts.forEach((part, i) => {
90
+ if (ry >= rb.y + rb.h)
91
+ return;
92
+ if (indent === 3 && i === 0)
93
+ c.text(rb.x + 1, ry, mark, { fg, bg: rb.bg });
94
+ c.text(rb.x + indent, ry, part, { fg: indent === 3 ? t.text : fg, bg: rb.bg, b: kind === 'head' }, rb.w - 1 - indent);
95
+ ry += 1;
96
+ });
97
+ }
98
+ if (ry + 1 < rb.y + rb.h)
99
+ c.rainZone(rb.x + 1, ry + 1, rb.w - 2, rb.y + rb.h - ry - 1, 2);
100
+ }
101
+ // task-1388: `g` opens the go-to prompt here too (keymap + the ? overlay list it); the footer is kept
102
+ // byte-identical to prototype-v2 (snapshot parity), so the hint lives in `?` and on the screener footer.
103
+ footer(c, [['j/k', 'move'], ['⏎', 'open'], ['d', 'remove'], ['n', 'note'], ['b', 'add alert'], ['a', 'ask claude'], ['q', 'quit']], state.theme.name, isCalm(state.vfx));
104
+ }