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,419 @@
1
+ import { buyShare } from "../live/flow.js";
2
+ export const TRIGGER_IDS = ['vol', 'flow', 'fresh', 'breakout', 'liq', 'holders'];
3
+ /** Points a fully-strong trigger contributes; a just-fired one contributes half. */
4
+ export const WEIGHTS = { vol: 30, flow: 25, fresh: 20, breakout: 20, liq: 15, holders: 15 };
5
+ export const DEFAULT_SCAN_CONFIG = Object.freeze({
6
+ minScore: 45, minTriggers: 2, minLiqUsd: 10_000,
7
+ volX: 3, volFullX: 10, volMinUsd: 2_000,
8
+ flowShare: 0.62, flowPrevMax: 0.52, flowMinTxns: 20, flowFullShare: 0.85,
9
+ freshMaxAgeMin: 60, freshMinTxns: 40, freshShare: 0.58, freshMinVolUsd: 5_000, freshFullTxns: 150,
10
+ breakoutLookback: 20, breakoutPct: 2, breakoutFullPct: 10,
11
+ liqJumpPct: 20, liqFullPct: 60,
12
+ holdersJumpPct: 5, holdersJumpMin: 25, holdersFullPct: 25,
13
+ windowMs: 10 * 60_000,
14
+ vetoAuthorities: true,
15
+ warmupS: 30,
16
+ maxAiPerHour: 8, maxAiPerCoin: 4,
17
+ refreshS: 90, movePct: 3, moveFlowPts: 10, moveVolX: 1.5,
18
+ coinCooldownS: 15 * 60, postLockS: 60,
19
+ tickerSize: 5,
20
+ });
21
+ /** Merge overrides onto the defaults, dropping non-finite numbers (a bad flag never poisons the engine). */
22
+ export function scanConfig(over = {}) {
23
+ const out = { ...DEFAULT_SCAN_CONFIG };
24
+ for (const [k, v] of Object.entries(over)) {
25
+ if (!(k in DEFAULT_SCAN_CONFIG))
26
+ continue;
27
+ if (typeof DEFAULT_SCAN_CONFIG[k] === 'boolean') {
28
+ if (typeof v === 'boolean')
29
+ out[k] = v;
30
+ continue;
31
+ }
32
+ if (typeof v === 'number' && Number.isFinite(v))
33
+ out[k] = v;
34
+ }
35
+ return out;
36
+ }
37
+ const clamp01 = (x) => (Number.isFinite(x) ? Math.max(0, Math.min(1, x)) : 0);
38
+ const pos = (n) => typeof n === 'number' && Number.isFinite(n) && n > 0;
39
+ const pct0 = (x) => `${Math.round(x * 100)}%`;
40
+ /** `$12.4K` style for reasons (no locale, deterministic). */
41
+ export function usd(n) {
42
+ const a = Math.abs(n);
43
+ if (a >= 1e9)
44
+ return `$${(n / 1e9).toFixed(1)}B`;
45
+ if (a >= 1e6)
46
+ return `$${(n / 1e6).toFixed(1)}M`;
47
+ if (a >= 1e3)
48
+ return `$${(n / 1e3).toFixed(1)}K`;
49
+ return `$${Math.round(n)}`;
50
+ }
51
+ const hit = (id, strength, reason) => {
52
+ const s = clamp01(strength);
53
+ return { id, strength: s, points: WEIGHTS[id] * (0.5 + 0.5 * s), reason };
54
+ };
55
+ /** The coin's own average 5-minute volume pace: vol6h/72, else vol1h/12, else vol24/288. */
56
+ export function volBaseline(o) {
57
+ if (pos(o.vol6h))
58
+ return { per5m: o.vol6h / 72, basis: '6h' };
59
+ if (pos(o.vol1h))
60
+ return { per5m: o.vol1h / 12, basis: '1h' };
61
+ if (pos(o.vol24))
62
+ return { per5m: o.vol24 / 288, basis: '24h' };
63
+ return null;
64
+ }
65
+ export function volTrigger(o, cfg) {
66
+ if (!pos(o.vol5m) || o.vol5m < cfg.volMinUsd)
67
+ return null;
68
+ const b = volBaseline(o);
69
+ if (!b || b.per5m <= 0)
70
+ return null;
71
+ const ratio = o.vol5m / b.per5m;
72
+ if (ratio < cfg.volX)
73
+ return null;
74
+ return hit('vol', (ratio - cfg.volX) / Math.max(1e-9, cfg.volFullX - cfg.volX), `vol 5m ${usd(o.vol5m)} = ${ratio.toFixed(1)}× its ${b.basis} pace`);
75
+ }
76
+ export function flowTrigger(o, cfg) {
77
+ if (!o.m5 || !o.h1)
78
+ return null;
79
+ const n5 = o.m5.buys + o.m5.sells;
80
+ const s5 = buyShare(o.m5);
81
+ const s1 = buyShare(o.h1);
82
+ if (s5 === null || s1 === null || n5 < cfg.flowMinTxns)
83
+ return null;
84
+ if (s5 < cfg.flowShare || s1 > cfg.flowPrevMax)
85
+ return null;
86
+ return hit('flow', (s5 - cfg.flowShare) / Math.max(1e-9, cfg.flowFullShare - cfg.flowShare), `flow flip: 5m buys ${pct0(s5)} (${o.m5.buys}/${n5}) vs 1h ${pct0(s1)}`);
87
+ }
88
+ export function freshTrigger(o, cfg) {
89
+ if (o.ageMin === null || o.ageMin < 0 || o.ageMin > cfg.freshMaxAgeMin || !o.m5)
90
+ return null;
91
+ const n5 = o.m5.buys + o.m5.sells;
92
+ const s5 = buyShare(o.m5);
93
+ if (s5 === null || n5 < cfg.freshMinTxns || s5 < cfg.freshShare)
94
+ return null;
95
+ if (!pos(o.vol5m) || o.vol5m < cfg.freshMinVolUsd)
96
+ return null;
97
+ return hit('fresh', (n5 - cfg.freshMinTxns) / Math.max(1e-9, cfg.freshFullTxns - cfg.freshMinTxns), `fresh launch ${Math.round(o.ageMin)}m old: 5m ${n5} txns, ${pct0(s5)} buys, ${usd(o.vol5m)}`);
98
+ }
99
+ export function breakoutTrigger(o, cfg) {
100
+ const cs = o.candles;
101
+ const n = cfg.breakoutLookback;
102
+ if (!cs || n < 1 || cs.length < n + 1)
103
+ return null;
104
+ const last = cs[cs.length - 1];
105
+ let hi = -Infinity;
106
+ for (let i = cs.length - 1 - n; i < cs.length - 1; i++)
107
+ hi = Math.max(hi, cs[i].h);
108
+ if (!pos(hi) || !pos(last.c))
109
+ return null;
110
+ const p = (last.c / hi - 1) * 100;
111
+ if (p < cfg.breakoutPct)
112
+ return null;
113
+ return hit('breakout', (p - cfg.breakoutPct) / Math.max(1e-9, cfg.breakoutFullPct - cfg.breakoutPct), `breakout: close +${p.toFixed(1)}% above the ${n}-bar high`);
114
+ }
115
+ /** Ring samples strictly before `o.t` inside the look-back window. */
116
+ function window(ring, t, ms) {
117
+ return (ring ?? []).filter((s) => s.t < t && t - s.t <= ms);
118
+ }
119
+ export function liqTrigger(o, ring, cfg) {
120
+ if (!pos(o.liq))
121
+ return null;
122
+ const prior = window(ring, o.t, cfg.windowMs).filter((s) => pos(s.liq));
123
+ if (!prior.length)
124
+ return null;
125
+ let lo = prior[0];
126
+ for (const s of prior)
127
+ if (s.liq < lo.liq)
128
+ lo = s;
129
+ const p = (o.liq / lo.liq - 1) * 100;
130
+ if (p < cfg.liqJumpPct)
131
+ return null;
132
+ return hit('liq', (p - cfg.liqJumpPct) / Math.max(1e-9, cfg.liqFullPct - cfg.liqJumpPct), `liquidity +${Math.round(p)}% in ${Math.max(1, Math.round((o.t - lo.t) / 60_000))}m (${usd(lo.liq)}→${usd(o.liq)})`);
133
+ }
134
+ export function holdersTrigger(o, ring, cfg) {
135
+ if (!pos(o.holders))
136
+ return null;
137
+ const prior = window(ring, o.t, cfg.windowMs).filter((s) => pos(s.holders));
138
+ if (!prior.length)
139
+ return null;
140
+ const first = prior[0]; // oldest in the window
141
+ const h0 = first.holders;
142
+ const d = o.holders - h0;
143
+ const p = (d / h0) * 100;
144
+ if (p < cfg.holdersJumpPct || d < cfg.holdersJumpMin)
145
+ return null;
146
+ return hit('holders', (p - cfg.holdersJumpPct) / Math.max(1e-9, cfg.holdersFullPct - cfg.holdersJumpPct), `holders +${d} (+${p.toFixed(1)}%) in ${Math.max(1, Math.round((o.t - first.t) / 60_000))}m`);
147
+ }
148
+ /** Every trigger for one coin, its score, the veto and whether it is EXCITING. Pure. */
149
+ export function scoreCoin(o, ring, cfg) {
150
+ const base = { mint: o.mint, symbol: o.symbol, t: o.t };
151
+ if (!pos(o.liq) || o.liq < cfg.minLiqUsd)
152
+ return { ...base, score: 0, hits: [], veto: null, exciting: false };
153
+ const hits = [volTrigger(o, cfg), flowTrigger(o, cfg), freshTrigger(o, cfg), breakoutTrigger(o, cfg), liqTrigger(o, ring, cfg), holdersTrigger(o, ring, cfg)]
154
+ .filter((h) => h !== null)
155
+ .sort((a, b) => b.points - a.points);
156
+ let veto = null;
157
+ if (o.larp === 'LARP')
158
+ veto = 'vetoed: LARP verdict (larp-check)';
159
+ else if (cfg.vetoAuthorities && o.mintAuth === true)
160
+ veto = 'vetoed: mint authority enabled';
161
+ else if (cfg.vetoAuthorities && o.freezeAuth === true)
162
+ veto = 'vetoed: freeze authority enabled';
163
+ const raw = Math.min(100, Math.round(hits.reduce((s, h) => s + h.points, 0)));
164
+ const score = veto ? 0 : raw;
165
+ return { ...base, score, hits, veto, exciting: !veto && score >= cfg.minScore && hits.length >= cfg.minTriggers };
166
+ }
167
+ /** What changed enough since the last read to justify a refresh turn (empty = nothing material). */
168
+ export function materialMove(prev, cur, prevHits, curHits, cfg) {
169
+ if (!prev)
170
+ return ['first read'];
171
+ const out = [];
172
+ if (pos(prev.price) && pos(cur.price)) {
173
+ const p = (cur.price / prev.price - 1) * 100;
174
+ if (Math.abs(p) >= cfg.movePct)
175
+ out.push(`price ${p >= 0 ? '+' : ''}${p.toFixed(1)}%`);
176
+ }
177
+ const a = buyShare(prev.m5), b = buyShare(cur.m5);
178
+ if (a !== null && b !== null && Math.abs(b - a) * 100 >= cfg.moveFlowPts)
179
+ out.push(`5m buy share ${pct0(a)}→${pct0(b)}`);
180
+ if (pos(prev.vol5m) && pos(cur.vol5m)) {
181
+ const f = cur.vol5m / prev.vol5m;
182
+ if (f >= cfg.moveVolX || f <= 1 / cfg.moveVolX)
183
+ out.push(`vol 5m ×${f.toFixed(2)}`);
184
+ }
185
+ const fresh = curHits.filter((id) => !prevHits.includes(id));
186
+ if (fresh.length)
187
+ out.push(`new trigger: ${fresh.join(', ')}`);
188
+ return out;
189
+ }
190
+ /** The first analyst turn of a lock: the reasons (engine-built text: numbers + fixed words, no token text). */
191
+ export function lockQuestion(score, hits) {
192
+ return `SCAN TRIGGER on the SELECTED coin (scan score ${score}/100): ${hits.map((h) => h.reason).join('; ')}. `
193
+ + 'Give your short read now: call · why · levels · risk. Observe only: do not open tickets or navigate.';
194
+ }
195
+ /** A refresh turn on the warm session while LOCKED. */
196
+ export function refreshQuestion(moved) {
197
+ return `SCAN REFRESH: the data moved since your last read (${moved.join('; ')}). `
198
+ + 'Give an UPDATED short read and say in one line what changed. Observe only.';
199
+ }
200
+ const HOUR = 3_600_000;
201
+ const RING_MAX = 40;
202
+ const MAX_MINTS = 800;
203
+ const FORGET_MS = 30 * 60_000;
204
+ /**
205
+ * The scan state machine. `observe()` takes the coins the app already has, `tick()` scores them and
206
+ * returns at most one decision (lock a coin / refresh the locked read). Invariants:
207
+ * - off/paused never decide anything; locked only ever decides a refresh OF THE LOCKED COIN (no hop);
208
+ * - every decision is one analyst invocation and is only returned when the hourly and per-coin caps
209
+ * allow it; a refresh additionally needs refreshS since the last read, an idle analyst and a
210
+ * material data move; a lock needs the warm-up, the post-lock cooldown and the coin's cooldown over.
211
+ */
212
+ export class ScanEngine {
213
+ cfg;
214
+ mode = 'off';
215
+ startedAt = null;
216
+ lock = null;
217
+ /** Top coins by score (excluding vetoed / ineligible), for the SCANNING ticker. */
218
+ ticker = [];
219
+ /** Coins scored on the last tick (eligible by liquidity). */
220
+ scanned = 0;
221
+ /** Why nothing locked on the last tick (warming up, cooldown, ai cap …), for the ticker. */
222
+ note = null;
223
+ rings = new Map();
224
+ latest = new Map();
225
+ seenAt = new Map();
226
+ invocations = [];
227
+ barredUntil = new Map();
228
+ noLockUntil = 0;
229
+ constructor(cfg = {}) {
230
+ this.cfg = scanConfig(cfg);
231
+ }
232
+ get on() { return this.mode !== 'off'; }
233
+ /** off → scanning. Returns false when already on. */
234
+ start(now) {
235
+ if (this.mode !== 'off')
236
+ return false;
237
+ this.mode = 'scanning';
238
+ this.startedAt = now;
239
+ this.lock = null;
240
+ this.note = null;
241
+ return true;
242
+ }
243
+ /** Any state → off. The locked coin (if any) gets its cooldown. Returns the mint that was locked. */
244
+ stop(now) {
245
+ const was = this.lock?.mint ?? null;
246
+ if (was)
247
+ this.barredUntil.set(was, now + this.cfg.coinCooldownS * 1000);
248
+ this.mode = 'off';
249
+ this.lock = null;
250
+ this.ticker = [];
251
+ this.note = null;
252
+ this.startedAt = null;
253
+ return was;
254
+ }
255
+ /** locked → paused (the user cancelled the analysis). The coin stays pulled up; nothing else locks. */
256
+ cancelAnalysis(_now) {
257
+ if (this.mode !== 'locked')
258
+ return false;
259
+ this.mode = 'paused';
260
+ return true;
261
+ }
262
+ /** locked/paused → scanning (the "resume scan" key). The released coin is barred; a short global cooldown. */
263
+ resume(now) {
264
+ if (this.mode === 'off')
265
+ return this.start(now);
266
+ if (this.mode === 'scanning')
267
+ return false;
268
+ if (this.lock)
269
+ this.barredUntil.set(this.lock.mint, now + this.cfg.coinCooldownS * 1000);
270
+ this.lock = null;
271
+ this.noLockUntil = now + this.cfg.postLockS * 1000;
272
+ this.mode = 'scanning';
273
+ return true;
274
+ }
275
+ /** Feed the latest observations (the feed rows + the open token). Cheap; keeps a short ring per coin. */
276
+ observe(obs, now) {
277
+ for (const o of obs) {
278
+ if (!o.mint)
279
+ continue;
280
+ this.latest.set(o.mint, o);
281
+ this.seenAt.set(o.mint, now);
282
+ const ring = this.rings.get(o.mint) ?? [];
283
+ const last = ring[ring.length - 1];
284
+ if (!last || o.t > last.t) {
285
+ ring.push({ t: o.t, liq: o.liq, holders: o.holders });
286
+ while (ring.length > RING_MAX || (ring.length > 1 && o.t - ring[0].t > this.cfg.windowMs * 1.5))
287
+ ring.shift();
288
+ this.rings.set(o.mint, ring);
289
+ }
290
+ }
291
+ // forget coins nobody has shown us for a while (bounded memory)
292
+ if (this.latest.size > MAX_MINTS || this.seenAt.size > MAX_MINTS) {
293
+ for (const [m, t] of this.seenAt)
294
+ if (now - t > FORGET_MS || this.latest.size > MAX_MINTS) {
295
+ this.seenAt.delete(m);
296
+ this.latest.delete(m);
297
+ this.rings.delete(m);
298
+ }
299
+ }
300
+ }
301
+ /** Invocations in the last hour (overall and for one coin), and when the next slot frees. */
302
+ usage(now, mint) {
303
+ this.invocations = this.invocations.filter((x) => now - x.t < HOUR);
304
+ const coin = mint ? this.invocations.filter((x) => x.mint === mint).length : 0;
305
+ const full = this.invocations.length >= this.cfg.maxAiPerHour;
306
+ return {
307
+ hour: this.invocations.length, hourCap: this.cfg.maxAiPerHour, coin, coinCap: this.cfg.maxAiPerCoin,
308
+ nextFreeInMs: full && this.invocations.length ? Math.max(0, HOUR - (now - this.invocations[0].t)) : null,
309
+ };
310
+ }
311
+ canInvoke(now, mint) {
312
+ const u = this.usage(now, mint);
313
+ return u.hour < u.hourCap && u.coin < u.coinCap;
314
+ }
315
+ /** Score one observed coin with its ring (exposed for the ticker/tests). */
316
+ score(mint) {
317
+ const o = this.latest.get(mint);
318
+ return o ? scoreCoin(o, this.rings.get(mint), this.cfg) : null;
319
+ }
320
+ /** Score everything, update the ticker, and return at most one decision. */
321
+ tick(now, ctx = { analystBusy: false }) {
322
+ if (this.mode === 'off')
323
+ return null;
324
+ const scored = [];
325
+ for (const [m, o] of this.latest) {
326
+ if (!pos(o.liq) || o.liq < this.cfg.minLiqUsd)
327
+ continue;
328
+ scored.push(scoreCoin(o, this.rings.get(m), this.cfg));
329
+ }
330
+ this.scanned = scored.length;
331
+ scored.sort((a, b) => b.score - a.score || b.hits.length - a.hits.length || (a.mint < b.mint ? -1 : 1));
332
+ this.ticker = scored.filter((s) => !s.veto && s.score > 0).slice(0, this.cfg.tickerSize);
333
+ if (this.mode === 'paused') {
334
+ this.note = 'analysis cancelled · R resume scan · Z stop';
335
+ return null;
336
+ }
337
+ if (this.mode === 'locked' && this.lock) {
338
+ // NO-HOP: only the locked coin is ever considered here, whatever else scores higher.
339
+ const lock = this.lock;
340
+ const o = this.latest.get(lock.mint);
341
+ if (!o)
342
+ return null;
343
+ const cur = scoreCoin(o, this.rings.get(lock.mint), this.cfg);
344
+ lock.liveScore = cur.score;
345
+ if (ctx.analystBusy) {
346
+ this.note = 'reading…';
347
+ return null;
348
+ }
349
+ const wait = lock.lastReadAt + this.cfg.refreshS * 1000 - now;
350
+ if (wait > 0) {
351
+ this.note = `next read ≥ ${Math.ceil(wait / 1000)}s`;
352
+ return null;
353
+ }
354
+ const curIds = cur.hits.map((h) => h.id);
355
+ const moved = materialMove(lock.lastRead, o, lock.lastHits, curIds, this.cfg);
356
+ if (!moved.length) {
357
+ this.note = 'holding · data flat since last read';
358
+ return null;
359
+ }
360
+ if (!this.canInvoke(now, lock.mint)) {
361
+ this.note = this.capNote(now, lock.mint);
362
+ return null;
363
+ }
364
+ this.invocations.push({ t: now, mint: lock.mint });
365
+ lock.lastReadAt = now;
366
+ lock.lastRead = o;
367
+ lock.lastHits = curIds;
368
+ lock.reads += 1;
369
+ lock.moved = moved;
370
+ this.note = null;
371
+ return { kind: 'refresh', mint: lock.mint, symbol: lock.symbol, moved, question: refreshQuestion(moved) };
372
+ }
373
+ // SCANNING
374
+ if (this.startedAt !== null && now - this.startedAt < this.cfg.warmupS * 1000) {
375
+ this.note = `warming up ${Math.ceil((this.cfg.warmupS * 1000 - (now - this.startedAt)) / 1000)}s`;
376
+ return null;
377
+ }
378
+ if (now < this.noLockUntil) {
379
+ this.note = `cooldown ${Math.ceil((this.noLockUntil - now) / 1000)}s`;
380
+ return null;
381
+ }
382
+ // The UI is busy (a ticket, an overlay, an editor, a half-typed question): never pull a coin up now.
383
+ if (ctx.holdLock) {
384
+ this.note = 'holding: you are busy';
385
+ return null;
386
+ }
387
+ let capped = null;
388
+ const pick = scored.find((s) => {
389
+ if (!s.exciting)
390
+ return false;
391
+ if ((this.barredUntil.get(s.mint) ?? 0) > now)
392
+ return false;
393
+ if (!this.canInvoke(now, s.mint)) {
394
+ capped ??= s.mint;
395
+ return false;
396
+ }
397
+ return true;
398
+ });
399
+ if (!pick) {
400
+ this.note = capped ? this.capNote(now, capped) : null;
401
+ return null;
402
+ }
403
+ const o = this.latest.get(pick.mint);
404
+ this.invocations.push({ t: now, mint: pick.mint });
405
+ this.mode = 'locked';
406
+ this.lock = {
407
+ mint: pick.mint, symbol: pick.symbol, at: now, score: pick.score, hits: pick.hits, liveScore: pick.score,
408
+ lastReadAt: now, lastRead: o, lastHits: pick.hits.map((h) => h.id), reads: 1, moved: [],
409
+ };
410
+ this.note = null;
411
+ return { kind: 'lock', mint: pick.mint, symbol: pick.symbol, score: pick.score, hits: pick.hits, question: lockQuestion(pick.score, pick.hits) };
412
+ }
413
+ capNote(now, mint) {
414
+ const u = this.usage(now, mint);
415
+ if (u.hour >= u.hourCap)
416
+ return `ai cap ${u.hour}/${u.hourCap} per hour${u.nextFreeInMs !== null ? ` · next in ${Math.ceil(u.nextFreeInMs / 60_000)}m` : ''}`;
417
+ return `coin ai cap ${u.coin}/${u.coinCap} per hour`;
418
+ }
419
+ }
@@ -0,0 +1,64 @@
1
+ import { DS_TOKEN_CACHE_NS } from "../live/flow.js";
2
+ /** Default cap on observed rows per pass: excitement needs volume, so the top 5m-volume rows. */
3
+ export const OBSERVE_MAX = 120;
4
+ function txns(get, mint) {
5
+ if (!get)
6
+ return { m5: null, h1: null };
7
+ let hit;
8
+ try {
9
+ hit = get(DS_TOKEN_CACHE_NS, mint);
10
+ }
11
+ catch {
12
+ return { m5: null, h1: null };
13
+ }
14
+ const pair = hit?.value?.pair;
15
+ const ok = (b) => (b && Number.isFinite(b.buys) && Number.isFinite(b.sells) ? { buys: b.buys, sells: b.sells } : null);
16
+ return { m5: ok(pair?.txns?.m5), h1: ok(pair?.txns?.h1) };
17
+ }
18
+ /**
19
+ * A cheap signature of the scan inputs: when it is unchanged the loop skips rebuilding observations
20
+ * (calm-mode CPU). Row count + newest fetch time + the open token's last load + verdict count.
21
+ */
22
+ export function observeSig(src) {
23
+ let maxT = 0;
24
+ for (const r of src.rows)
25
+ if (r.fetchedAt !== null && r.fetchedAt > maxT)
26
+ maxT = r.fetchedAt;
27
+ return `${src.rows.length}:${maxT}:${src.token?.mint ?? ''}:${src.token?.updatedAt ?? 0}:${src.token?.candles.length ?? 0}:${src.larp?.size ?? 0}`;
28
+ }
29
+ /** One observation per eligible feed row, plus the open token (merged when it is also a row). */
30
+ export function buildObservations(src) {
31
+ const max = src.max ?? OBSERVE_MAX;
32
+ const eligible = src.rows.filter((r) => !r.skeleton && !r.fixture && !!r.mint && r.liq !== null && r.liq >= src.minLiqUsd);
33
+ eligible.sort((a, b) => (b.vol5m ?? 0) - (a.vol5m ?? 0));
34
+ const out = new Map();
35
+ for (const r of eligible.slice(0, max)) {
36
+ const tx = txns(src.cacheGet, r.mint);
37
+ out.set(r.mint, {
38
+ mint: r.mint, symbol: r.symbol, t: r.fetchedAt ?? src.now,
39
+ price: r.price, vol5m: r.vol5m, vol1h: r.vol1h, vol6h: r.vol6h, vol24: r.vol24,
40
+ liq: r.liq, holders: r.holders, ageMin: r.ageMin, m5: tx.m5, h1: tx.h1,
41
+ candles: null, larp: src.larp?.get(r.mint)?.verdict ?? null, mintAuth: r.mintAuth, freezeAuth: r.freezeAuth,
42
+ });
43
+ }
44
+ const v = src.token;
45
+ if (v && v.mint) {
46
+ const row = out.get(v.mint);
47
+ const tx = row ? { m5: row.m5, h1: row.h1 } : txns(src.cacheGet, v.mint);
48
+ const px = src.livePrice?.(v.mint) ?? v.price;
49
+ if (row || (v.liq !== null && v.liq >= src.minLiqUsd)) {
50
+ out.set(v.mint, {
51
+ mint: v.mint, symbol: v.symbol, t: Math.max(row?.t ?? 0, v.updatedAt ?? 0) || src.now,
52
+ price: px ?? row?.price ?? null,
53
+ vol5m: row?.vol5m ?? null, vol1h: row?.vol1h ?? null, vol6h: row?.vol6h ?? null, vol24: row?.vol24 ?? v.vol24,
54
+ liq: v.liq ?? row?.liq ?? null, holders: v.holders ?? row?.holders ?? null, ageMin: v.ageMin ?? row?.ageMin ?? null,
55
+ m5: tx.m5, h1: tx.h1,
56
+ // the open token's candles are already on screen — the only candles the scan ever reads
57
+ candles: v.candles.length ? v.candles : null,
58
+ larp: src.larp?.get(v.mint)?.verdict ?? null,
59
+ mintAuth: v.mintAuth ?? row?.mintAuth ?? null, freezeAuth: v.freezeAuth ?? row?.freezeAuth ?? null,
60
+ });
61
+ }
62
+ }
63
+ return [...out.values()];
64
+ }
@@ -0,0 +1,98 @@
1
+ // dexterm AI SCAN MODE (task-1324) — per-run scan UI state and the two one-line strips it draws on
2
+ // row 1 (the blank band under the header, so no screen layout moves):
3
+ // - list screens: the SCANNING ticker — state, coins scanned, the top scores with their triggers;
4
+ // - the locked coin's token screen: WHY IT TRIGGERED — reasons, score, when, reads, next read, keys.
5
+ // Rendering only; the decisions live in engine.ts and the loop in main.ts.
6
+ import { clock, mix } from '../../../tui/src/index.js';
7
+ import { cleanText } from "../screener/rows.js";
8
+ import { ScanEngine } from "./engine.js";
9
+ /** How often the loop runs a scan tick, and the forced recompute when inputs did not change. */
10
+ export const SCAN_TICK_MS = 2_000;
11
+ export const SCAN_RECOMPUTE_MS = 10_000;
12
+ /** The analyst UI actions a scan session may request: NONE (observe-only). */
13
+ export const SCAN_ACTION_TYPES = Object.freeze([]);
14
+ export function makeScanUi(cfg = {}) {
15
+ return { engine: new ScanEngine(cfg), sig: '', lastTickAt: 0, lastComputeAt: 0, session: null, blocked: [], returnTo: null };
16
+ }
17
+ export const MODE_LABEL = { off: 'OFF', scanning: 'SCANNING', locked: 'LOCKED', paused: 'PAUSED' };
18
+ const SHORT = { vol: 'vol', flow: 'flow', fresh: 'fresh', breakout: 'brk', liq: 'liq', holders: 'hold' };
19
+ /** `◉ SCAN LOCKED $WIF` — the header's running indicator (state.ts headerModel). */
20
+ export function scanIndicator(scan) {
21
+ const e = scan.engine;
22
+ const sym = e.lock ? ` $${cleanText(e.lock.symbol, 12)}` : '';
23
+ return `◉ AI SCAN ${MODE_LABEL[e.mode]}${sym}`;
24
+ }
25
+ function band(c) {
26
+ const t = c.theme;
27
+ const bg = mix(t.bg, t.ai, 0.14);
28
+ c.fill(0, 1, c.w, 1, ' ', { bg });
29
+ c.noRain(0, 1, c.w, 1);
30
+ return bg;
31
+ }
32
+ /** The SCANNING ticker on row 1 of the list screens. `ai` = the meter text (`ai 1/8 per h`). */
33
+ export function renderScanTicker(c, scan, ai) {
34
+ const e = scan.engine;
35
+ if (!e.on)
36
+ return;
37
+ const t = c.theme;
38
+ const bg = band(c);
39
+ const end = c.w - 1;
40
+ let x = c.text(1, 1, '◉ AI SCAN ', { fg: t.ai, bg, b: true });
41
+ x = c.text(x, 1, MODE_LABEL[e.mode], { fg: e.mode === 'scanning' ? t.up : t.warn, bg, b: true }, end - x);
42
+ if (e.lock)
43
+ x = c.text(x + 1, 1, `$${cleanText(e.lock.symbol, 12)} · read ${e.lock.reads}`, { fg: t.text, bg, b: true }, end - x - 1);
44
+ x = c.text(x + 1, 1, `· ${e.scanned} coins`, { fg: t.muted, bg }, end - x - 1);
45
+ const right = ` ${e.note ? e.note + ' · ' : ''}${ai} · ${e.mode === 'scanning' ? 'Z stop' : 'R resume · Z stop'} `;
46
+ const rightX = Math.max(x + 2, end - right.length);
47
+ if (!e.ticker.length)
48
+ c.text(x + 2, 1, e.mode === 'scanning' ? 'watching… nothing scores yet' : '', { fg: t.dim, bg, i: true }, rightX - x - 3);
49
+ for (const s of e.ticker) {
50
+ if (x + 12 > rightX)
51
+ break;
52
+ x = c.text(x, 1, ' │ ', { fg: t.dim, bg }, rightX - x);
53
+ x = c.text(x, 1, `$${cleanText(s.symbol, 10)} `, { fg: t.text, bg }, rightX - x);
54
+ x = c.text(x, 1, String(s.score), { fg: s.exciting ? t.warn : t.muted, bg, b: s.exciting }, rightX - x);
55
+ if (s.hits.length)
56
+ x = c.text(x + 1, 1, s.hits.map((h) => SHORT[h.id]).join('·'), { fg: t.dim, bg }, rightX - x - 1);
57
+ }
58
+ c.text(rightX, 1, right, { fg: e.note && /cap/.test(e.note) ? t.warn : t.dim, bg }, end - rightX + 1);
59
+ }
60
+ /** WHY IT TRIGGERED on row 1 of the locked coin's token screen. */
61
+ export function renderScanWhy(c, scan, _ai, now, timeZone) {
62
+ const e = scan.engine;
63
+ const lock = e.lock;
64
+ if (!lock)
65
+ return;
66
+ const t = c.theme;
67
+ const bg = band(c);
68
+ const end = c.w - 1;
69
+ let x = c.text(1, 1, '◉ AI SCAN ', { fg: t.ai, bg, b: true });
70
+ x = c.text(x, 1, MODE_LABEL[e.mode], { fg: e.mode === 'locked' ? t.up : t.warn, bg, b: true });
71
+ if (e.mode === 'paused')
72
+ x = c.text(x + 1, 1, '· analysis cancelled', { fg: t.warn, bg });
73
+ x = c.text(x + 1, 1, '· WHY IT TRIGGERED', { fg: t.ai, bg, b: true });
74
+ x = c.text(x + 1, 1, clock(new Date(lock.at), timeZone), { fg: t.muted, bg });
75
+ x = c.text(x + 1, 1, `· score ${lock.score}${lock.liveScore !== lock.score ? ` (now ${lock.liveScore})` : ''} ·`, { fg: t.warn, bg, b: true });
76
+ const keys = e.mode === 'paused' ? 'R resume · Z stop' : 'esc cancel · R resume · Z stop';
77
+ const sinceRead = Math.max(0, Math.round((now - lock.lastReadAt) / 1000));
78
+ // The analyst meter is already in the header; the strip keeps the read status + keys short so the
79
+ // reasons — the point of the strip — get the room.
80
+ const right = `read ${lock.reads} (${sinceRead}s ago)${e.note ? ' · ' + e.note : ''} · ${keys}`;
81
+ const room = Math.max(30, end - x - 3 - right.length);
82
+ // Full reasons while they fit; the rest by trigger id so the user still sees every trigger that fired.
83
+ let why = '';
84
+ const rest = [];
85
+ for (const h of lock.hits) {
86
+ const next = why ? `${why} · ${h.reason}` : h.reason;
87
+ if (!rest.length && next.length <= room)
88
+ why = next;
89
+ else
90
+ rest.push(h.id);
91
+ }
92
+ if (rest.length)
93
+ why = `${why}${why ? ' ' : ''}+ ${rest.join(', ')}`;
94
+ const wx = c.text(x + 1, 1, why, { fg: t.text, bg }, room);
95
+ const rx = Math.max(wx + 2, end - right.length);
96
+ if (rx < end)
97
+ c.text(rx, 1, right, { fg: t.dim, bg }, end - rx + 1);
98
+ }