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,240 @@
1
+ // `dexterm kb …` / `dexterm skill …` (task-1377): the user's overlay of the knowledge base and the analyst
2
+ // skills, under ~/.dexterm/kb/entries/<id>.md and ~/.dexterm/skills/<name>.md (DEXTERM_HOME overrides).
3
+ // A user file with a bundled id overrides it, a new id is added, bundled files are never edited in place.
4
+ // list bundled + user entries with their origin; skipped files with the reason
5
+ // show <id> print the file in effect (origin and path on stderr)
6
+ // new <id> write a starter file (refuses a bundled id: use edit), open $EDITOR, check it
7
+ // edit <id> copy the bundled file into the overlay if needed, open $EDITOR, check it
8
+ // check [id|file] validate (frontmatter, size, KB live-token rule, skill read-tools only); exit 1 on an error
9
+ // reset <id> move the user file to <dir>/.reset/ (the bundled entry is back in effect)
10
+ // path [id] the overlay directory, or the file in effect for <id>
11
+ // The MCP server, the analyst and Jarvis re-read the merged set on the next call: no restart. No network, no model.
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { spawnSync } from 'node:child_process';
16
+ import { OverlayStore, checkKb, checkSkill, dextermHome, userDir, writeOverlayFile, resetOverlayFile, templateFor, journalContent, READ_ONLY_TOOLS, BUNDLED_KB_DIR, BUNDLED_SKILLS_DIR, KB_ID, SKILL_NAME, KB_RULE, } from '../../mcp/src/index.js';
17
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
18
+ /** The analyst plugin's skills (the copies `/dexterm:<skill>` runs) come first; the MCP server's own copies fill any gap. */
19
+ export const SKILL_SOURCES = [
20
+ { dir: path.resolve(HERE, '..', 'assets', 'claude', 'plugin', 'skills'), layout: 'nested' },
21
+ { dir: BUNDLED_SKILLS_DIR, layout: 'flat' },
22
+ ];
23
+ export const KB_SOURCES = [{ dir: BUNDLED_KB_DIR, layout: 'flat' }];
24
+ const stores = new Map();
25
+ function storeFor(kind, env) {
26
+ const home = dextermHome(env);
27
+ const key = `${kind}\n${home}`;
28
+ let s = stores.get(key);
29
+ if (!s) {
30
+ s = new OverlayStore({ kind, bundled: kind === 'kb' ? KB_SOURCES : SKILL_SOURCES, readTools: READ_ONLY_TOOLS, home: () => home });
31
+ stores.set(key, s);
32
+ }
33
+ return s;
34
+ }
35
+ /** The hot-reloading merged KB for a DEXTERM_HOME (doctor, the CLI). */
36
+ export const kbStore = (env = process.env) => storeFor('kb', env);
37
+ /** The hot-reloading merged skills for a DEXTERM_HOME (the analyst's per-turn plugin dir, doctor, the CLI). */
38
+ export const skillStore = (env = process.env) => storeFor('skill', env);
39
+ const NOUN = { kb: 'KB entry', skill: 'skill' };
40
+ export function usage(kind) {
41
+ return kind === 'kb'
42
+ ? `usage: dexterm kb list | show <id> | new <id> | edit <id> | check [<id>|<file>] | reset <id> | path [<id>]
43
+ files: ~/.dexterm/kb/entries/<id>.md (DEXTERM_HOME overrides): same id as a bundled entry overrides it, a new id adds one.
44
+ The MCP server, the analyst and Jarvis read the merged set on their next call (no restart). ${KB_RULE}`
45
+ : `usage: dexterm skill list | show <name> | new <name> | edit <name> | check [<name>|<file>] | reset <name> | path [<name>]
46
+ files: ~/.dexterm/skills/<name>.md (DEXTERM_HOME overrides): same name as a bundled skill overrides it, a new name adds one.
47
+ A skill is read-only guidance: \`tools:\` may name only read tools and nothing may name a trade/sign/arm/ticket tool.`;
48
+ }
49
+ function check(kind, raw, stem, file) {
50
+ return kind === 'kb' ? checkKb(raw, stem, 'user', file) : checkSkill(raw, stem, READ_ONLY_TOOLS, 'user', file);
51
+ }
52
+ /** Print one check result; returns true when the file passes. The KB rule line is printed once by the caller. */
53
+ function report(io, file, res) {
54
+ const ok = res.errors.length === 0;
55
+ io.stdout.write(`${ok ? '✓' : '✗'} ${file}\n`);
56
+ for (const e of res.errors)
57
+ io.stdout.write(` error: ${e}\n`);
58
+ for (const w of res.warnings)
59
+ if (!w.startsWith('rule: '))
60
+ io.stdout.write(` warn: ${w}\n`);
61
+ return ok;
62
+ }
63
+ function openEditor(io, file) {
64
+ if (io.editor)
65
+ return io.editor(file) === 0 ? 'edited' : 'failed';
66
+ const ed = (io.env.VISUAL || io.env.EDITOR || '').trim();
67
+ if (!ed || io.isTTY === false)
68
+ return 'no-editor';
69
+ // `sh -c '$EDITOR "$1"'` so an EDITOR with arguments ("code -w") works; the path is passed as $1, never interpolated.
70
+ const r = spawnSync('/bin/sh', ['-c', `${ed} "$1"`, 'sh', file], { stdio: 'inherit', env: io.env });
71
+ return r.status === 0 ? 'edited' : 'failed';
72
+ }
73
+ function readOr(file) { try {
74
+ return fs.readFileSync(file, 'utf8');
75
+ }
76
+ catch {
77
+ return '';
78
+ } }
79
+ /** After new/edit: open the editor, journal the change, check the file, say what the loader will do. */
80
+ function editAndCheck(kind, io, home, id, file, before, action) {
81
+ const ed = openEditor(io, file);
82
+ if (ed === 'no-editor')
83
+ io.stdout.write(`no $EDITOR (or not a terminal): edit ${file} with any editor; \`dexterm ${kind} check ${id}\` validates it\n`);
84
+ if (ed === 'failed')
85
+ io.stderr.write(`dexterm ${kind}: the editor exited with an error; the file is kept as it is\n`);
86
+ const after = readOr(file);
87
+ if (after !== before)
88
+ journalContent(home, { kind, action, id, before, after });
89
+ const res = check(kind, after, id, file);
90
+ const ok = report(io, file, res);
91
+ if (kind === 'kb')
92
+ io.stdout.write(` rule: ${KB_RULE}\n`);
93
+ if (!ok) {
94
+ io.stdout.write(` the loader skips this file until it passes (the bundled ${NOUN[kind]} stays in effect); fix it, or \`dexterm ${kind} reset ${id}\`\n`);
95
+ return 1;
96
+ }
97
+ io.stdout.write(` in effect on the next MCP/analyst/Jarvis call (no restart)\n`);
98
+ return 0;
99
+ }
100
+ function listMerged(kind, io, m) {
101
+ io.stdout.write(`${m.dir} (${m.bundled} bundled · ${m.user} user: ${m.overridden} override${m.overridden === 1 ? '' : 's'}, ${m.added} new · ${m.skipped.length} skipped)\n`);
102
+ for (const e of m.entries) {
103
+ const tag = e.origin === 'bundled' ? 'bundled' : e.overrides ? 'user (overrides bundled)' : 'user (new)';
104
+ const what = String((kind === 'kb' ? e.meta.title : e.meta.description) ?? '').slice(0, 90);
105
+ io.stdout.write(` ${e.id.padEnd(32)} ${tag.padEnd(25)} ${what}\n`);
106
+ }
107
+ for (const s of m.skipped)
108
+ io.stdout.write(`! ${s.id.padEnd(32)} skipped: ${s.reason}\n`);
109
+ }
110
+ export async function kbSkillCommand(kind, argv, io) {
111
+ const home = dextermHome(io.env);
112
+ const store = storeFor(kind, io.env);
113
+ const dir = userDir(kind, home);
114
+ const [cmd, arg] = argv;
115
+ const bad = (s) => { io.stderr.write(`dexterm ${kind}: ${s}\n${usage(kind)}\n`); return 2; };
116
+ const idOk = (id) => !!id && (kind === 'kb' ? KB_ID : SKILL_NAME).test(id);
117
+ const userFile = (id) => path.join(dir, `${id}.md`);
118
+ const bundledOf = (id) => store.bundled().find((e) => e.id === id) ?? null;
119
+ switch (cmd) {
120
+ case undefined:
121
+ case 'list': {
122
+ listMerged(kind, io, store.get());
123
+ return 0;
124
+ }
125
+ case 'show': {
126
+ if (!arg)
127
+ return bad('show needs an id');
128
+ const e = store.get().entries.find((x) => x.id === arg);
129
+ if (!e) {
130
+ io.stderr.write(`dexterm ${kind}: no ${NOUN[kind]} "${arg}" (see: dexterm ${kind} list)\n`);
131
+ return 1;
132
+ }
133
+ io.stderr.write(`# ${e.origin}${e.overrides ? ' (overrides bundled)' : ''} · ${e.file}\n`);
134
+ io.stdout.write(readOr(e.file));
135
+ return 0;
136
+ }
137
+ case 'path': {
138
+ if (!arg) {
139
+ io.stdout.write(`${dir}\n`);
140
+ return 0;
141
+ }
142
+ const e = store.get().entries.find((x) => x.id === arg);
143
+ if (!e) {
144
+ io.stderr.write(`dexterm ${kind}: no ${NOUN[kind]} "${arg}"\n`);
145
+ return 1;
146
+ }
147
+ io.stdout.write(`${e.file}\n`);
148
+ return 0;
149
+ }
150
+ case 'new': {
151
+ if (!idOk(arg))
152
+ return bad(`new needs a ${kind === 'kb' ? 'KB id' : 'skill name'} (lowercase letters, digits, dashes)`);
153
+ if (bundledOf(arg)) {
154
+ io.stderr.write(`dexterm ${kind}: "${arg}" is bundled; \`dexterm ${kind} edit ${arg}\` makes an editable copy that overrides it\n`);
155
+ return 1;
156
+ }
157
+ if (fs.existsSync(userFile(arg))) {
158
+ io.stderr.write(`dexterm ${kind}: ${userFile(arg)} already exists; \`dexterm ${kind} edit ${arg}\`\n`);
159
+ return 1;
160
+ }
161
+ const file = writeOverlayFile(kind, home, arg, templateFor(kind, arg));
162
+ io.stdout.write(`wrote ${file}\n`);
163
+ return editAndCheck(kind, io, home, arg, file, '', 'new');
164
+ }
165
+ case 'edit': {
166
+ if (!idOk(arg))
167
+ return bad('edit needs an id');
168
+ let file = userFile(arg);
169
+ let before = readOr(file);
170
+ if (!fs.existsSync(file)) {
171
+ const b = bundledOf(arg);
172
+ if (!b) {
173
+ io.stderr.write(`dexterm ${kind}: no ${NOUN[kind]} "${arg}" to edit; \`dexterm ${kind} new ${arg}\` starts one\n`);
174
+ return 1;
175
+ }
176
+ // A copy of the bundled file, byte for byte; the bundled asset itself is never touched.
177
+ file = writeOverlayFile(kind, home, arg, readOr(b.file));
178
+ before = '';
179
+ io.stdout.write(`copied the bundled ${NOUN[kind]} to ${file} (it now overrides the bundled one; \`dexterm ${kind} reset ${arg}\` undoes it)\n`);
180
+ }
181
+ return editAndCheck(kind, io, home, arg, file, before, 'edit');
182
+ }
183
+ case 'check': {
184
+ let targets;
185
+ if (arg && (arg.includes('/') || arg.endsWith('.md')))
186
+ targets = [{ file: path.resolve(arg), stem: path.basename(arg) === 'SKILL.md' ? path.basename(path.dirname(path.resolve(arg))) : path.basename(arg, '.md') }];
187
+ else if (arg) {
188
+ const f = fs.existsSync(userFile(arg)) ? userFile(arg) : bundledOf(arg)?.file;
189
+ if (!f) {
190
+ io.stderr.write(`dexterm ${kind}: no ${NOUN[kind]} "${arg}"\n`);
191
+ return 1;
192
+ }
193
+ targets = [{ file: f, stem: arg }];
194
+ }
195
+ else {
196
+ let names = [];
197
+ try {
198
+ names = fs.readdirSync(dir).filter((f) => f.endsWith('.md') && !f.startsWith('.')).sort();
199
+ }
200
+ catch { /* no overlay yet */ }
201
+ targets = names.map((n) => ({ file: path.join(dir, n), stem: n.slice(0, -3) }));
202
+ if (!targets.length) {
203
+ io.stdout.write(`no user ${kind === 'kb' ? 'KB entries' : 'skills'} in ${dir} (the ${store.bundled().length} bundled are in use)\n`);
204
+ return 0;
205
+ }
206
+ }
207
+ let failed = 0;
208
+ for (const t of targets) {
209
+ let st = null;
210
+ try {
211
+ st = fs.lstatSync(t.file);
212
+ }
213
+ catch { /* missing */ }
214
+ if (!st || !st.isFile()) {
215
+ io.stdout.write(`✗ ${t.file}\n error: ${st?.isSymbolicLink() ? 'a symlink: overlay files must be regular files' : 'not a regular file'}\n`);
216
+ failed++;
217
+ continue;
218
+ }
219
+ if (!report(io, t.file, check(kind, readOr(t.file), t.stem, t.file)))
220
+ failed++;
221
+ }
222
+ if (kind === 'kb')
223
+ io.stdout.write(`rule: ${KB_RULE}\n`);
224
+ io.stdout.write(failed ? `${failed} of ${targets.length} refused (the loader skips them; bundled entries stay in effect)\n` : `${targets.length} ok\n`);
225
+ return failed ? 1 : 0;
226
+ }
227
+ case 'reset': {
228
+ if (!idOk(arg))
229
+ return bad('reset needs an id');
230
+ const bak = resetOverlayFile(kind, home, arg);
231
+ if (!bak) {
232
+ io.stdout.write(`no user ${NOUN[kind]} "${arg}" in ${dir}; nothing to reset\n`);
233
+ return 0;
234
+ }
235
+ io.stdout.write(`moved your ${NOUN[kind]} to ${bak}\n ${bundledOf(arg) ? `the bundled "${arg}" is back in effect` : `"${arg}" is no longer offered`} on the next call\n`);
236
+ return 0;
237
+ }
238
+ default: return bad(`unknown subcommand "${cmd}"`);
239
+ }
240
+ }
@@ -0,0 +1,21 @@
1
+ // task-1359: persist a settled LARP-detector verdict into the recorder's `verdicts` table (pack `larp-check`) so
2
+ // `dexterm backtest --subject larp` can later score it against the outcome labels. Before this, verdicts lived
3
+ // only in memory (state.larpByMint) and no accuracy could ever be measured. Best-effort: no recorder, no table
4
+ // or a failed write is a silent no-op — the analyst turn never depends on it.
5
+ import { createHash } from 'node:crypto';
6
+ import { recordVerdict } from '../../packs/src/index.js';
7
+ import { LARP_PACK, LARP_VERDICTS } from '../../recorder/src/index.js';
8
+ /** Returns true when a row was written. Unknown verdict strings are refused (the eval only scores the four classes). */
9
+ export function recordLarpVerdict(db, v) {
10
+ if (!db || !LARP_VERDICTS.includes(v.verdict))
11
+ return false;
12
+ try {
13
+ // input_hash is a row key (the turn's tool inputs are not retained), not a cache key over the inputs
14
+ const hash = createHash('sha256').update(`${LARP_PACK}|${v.mint}|${v.t}|${v.verdict}`).digest('hex');
15
+ recordVerdict(db, { mint: v.mint, t: v.t, pack: LARP_PACK, pack_version: `task-1315/${v.skill}`, model: v.model, verdict: v.verdict, rug_p: null, confidence: v.confidence, score: null, cost_usd: null, wall_ms: null, input_hash: hash, run_file: null });
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
@@ -0,0 +1,80 @@
1
+ // dexterm — live binding for COPY TRADING Tier 1 (task-1360): the follow store + wallet tracker for this run.
2
+ // Live mode polls Helius history for every followed wallet (10 credits a call, through the data layer's limiter
3
+ // and credit meter) and queues alerts the loop turns into toasts; fixtures / replay / keyless get the same store
4
+ // with an honestly UNAVAILABLE tracker. Read-only: nothing here trades, signs or proposes.
5
+ import { HeliusHistory, systemClock } from '../../../data/src/index.js';
6
+ import { FollowStore, WalletTracker, defaultFollowsPath } from '../../../data/src/trade/copy/index.js';
7
+ export class CopyLive {
8
+ store;
9
+ tracker;
10
+ pending = [];
11
+ /** Tier 3: the auto-mirror service, attached by the app once the plan service exists (null = no auto path at all). */
12
+ auto = null;
13
+ dl;
14
+ clock;
15
+ constructor(dl, opts = {}) {
16
+ const clock = opts.clock ?? dl?.clock ?? systemClock;
17
+ this.dl = dl;
18
+ this.clock = clock;
19
+ this.store = new FollowStore({ path: opts.followsPath === undefined ? defaultFollowsPath(opts.env) : opts.followsPath, clock });
20
+ const url = dl?.rpcPool.heliusUrl ?? null;
21
+ const history = opts.history !== undefined ? opts.history : (dl && url ? new HeliusHistory(dl.http, url) : null);
22
+ this.tracker = new WalletTracker({
23
+ follows: this.store, history, clock, solUsd: opts.solUsd, pollMs: opts.pollMs,
24
+ onAlert: (a) => { this.pending.push(a); if (this.pending.length > 20)
25
+ this.pending.splice(0, this.pending.length - 20); if (this.auto)
26
+ void this.auto.onAlert(a); },
27
+ onChange: () => { this.auto?.tick(); opts.onChange?.('copy'); },
28
+ available: () => (!dl || dl.health.isAvailable('helius') ? { ok: true } : { ok: false, why: `Helius is ${dl.health.state('helius')}; wallet polling paused` }),
29
+ });
30
+ }
31
+ follows() { return this.store.list(); }
32
+ status() { return this.tracker.status(); }
33
+ stats(addr) { return this.tracker.stats(addr); }
34
+ trades(addr) { return this.tracker.trades(addr); }
35
+ alerts(limit = 50) { return this.tracker.alerts(limit); }
36
+ /** Alerts raised since the last call (the loop toasts them). */
37
+ takeAlerts() { const a = this.pending; this.pending = []; return a; }
38
+ now() { return this.clock.now(); }
39
+ /**
40
+ * Tier 2: what the mirror checks need for a mint. The screen's numbers come first (the hint); the data layer
41
+ * fills the gaps: decimals from RPC mint info, price / liquidity / age from DexScreener when nothing on screen
42
+ * has them. Never throws; what cannot be learned stays null and the mirror shows it as unknown.
43
+ */
44
+ async mirrorMarket(mint, hint = {}) {
45
+ const m = { symbol: hint.symbol ?? null, decimals: hint.decimals ?? null, priceUsd: hint.priceUsd ?? null, priceAt: hint.priceAt ?? null, liqUsd: hint.liqUsd ?? null, mcapUsd: hint.mcapUsd ?? null, ageMs: hint.ageMs ?? null };
46
+ if (!this.dl)
47
+ return m;
48
+ if (m.decimals === null) {
49
+ try {
50
+ m.decimals = (await this.dl.rpc.mintInfo(mint)).decimals;
51
+ }
52
+ catch { /* stays unknown */ }
53
+ }
54
+ if (m.priceUsd === null || m.liqUsd === null || m.ageMs === null || m.decimals === null) {
55
+ try {
56
+ const v = (await this.dl.dexscreener.tokens([mint])).get(mint);
57
+ if (v) {
58
+ if (m.priceUsd === null && v.pair.priceUsd > 0) {
59
+ m.priceUsd = v.pair.priceUsd;
60
+ m.priceAt = v.pair.fetchedAt;
61
+ }
62
+ if (m.liqUsd === null)
63
+ m.liqUsd = v.pair.liquidityUsd;
64
+ if (m.mcapUsd === null)
65
+ m.mcapUsd = v.pair.mcap ?? v.pair.fdv;
66
+ if (m.ageMs === null && v.pair.createdAt)
67
+ m.ageMs = Math.max(0, this.clock.now() - v.pair.createdAt);
68
+ if (m.symbol === null)
69
+ m.symbol = v.token.symbol;
70
+ if (m.decimals === null && v.token.decimals !== null)
71
+ m.decimals = v.token.decimals;
72
+ }
73
+ }
74
+ catch { /* stays unknown */ }
75
+ }
76
+ return m;
77
+ }
78
+ start() { this.tracker.start(); }
79
+ stop() { this.tracker.stop(); }
80
+ }
@@ -0,0 +1,233 @@
1
+ /** Cache namespace `Store.enrich` writes DexScreener token views under (store.ts `NS.ds`). A test pins it. */
2
+ export const DS_TOKEN_CACHE_NS = 'ds:token';
3
+ /** TokenLive keeps the newest 60 tape trades (live/token.ts `slice(0, 60)`); a full tape may not reach back a whole minute. */
4
+ export const TAPE_CAP = 60;
5
+ /** A buy-share move smaller than this (percentage points) is "flat". */
6
+ export const FLAT_PTS = 2;
7
+ /** A previous-window sample may be off by this fraction of the window length. */
8
+ export const PREV_TOLERANCE = 0.25;
9
+ /** Minimum spacing between ring samples; older samples thin out to ~2 % of their age. */
10
+ export const MIN_SAMPLE_GAP_MS = 5_000;
11
+ const THIN_FRACTION = 0.02;
12
+ const MAX_AGE_MS = 25 * 3_600_000;
13
+ const MAX_SAMPLES = 1_000;
14
+ /** The panel's timeframes, shortest first. `ds: null` = derived from the trade tape. */
15
+ export const FLOW_WINDOWS = [
16
+ { label: '1m', ms: 60_000, ds: null },
17
+ { label: '5m', ms: 300_000, ds: 'm5' },
18
+ { label: '1h', ms: 3_600_000, ds: 'h1' },
19
+ { label: '6h', ms: 21_600_000, ds: 'h6' },
20
+ { label: '24h', ms: 86_400_000, ds: 'h24' },
21
+ ];
22
+ /** Buy share of a window (0..1), null when nothing traded. */
23
+ export function buyShare(c) {
24
+ if (!c)
25
+ return null;
26
+ const n = c.buys + c.sells;
27
+ return n > 0 ? c.buys / n : null;
28
+ }
29
+ /** Trend of the current window vs the previous one: buy-share change in percentage points. */
30
+ export function trendCell(cur, prev, flatPts = FLAT_PTS) {
31
+ const a = buyShare(cur);
32
+ const b = buyShare(prev);
33
+ if (a === null || b === null)
34
+ return { na: 'no trades' };
35
+ const deltaPts = Math.round((a - b) * 100);
36
+ if (Math.abs((a - b) * 100) < flatPts)
37
+ return { dir: 0, deltaPts };
38
+ return { dir: a > b ? 1 : -1, deltaPts };
39
+ }
40
+ /** `45s` · `3m` · `2h` · `23h` (rounded up: never says 0 while still waiting). */
41
+ export function fmtWait(ms) {
42
+ const s = Math.max(1, Math.ceil(ms / 1000));
43
+ if (s < 60)
44
+ return `${s}s`;
45
+ const m = Math.ceil(s / 60);
46
+ if (m < 60)
47
+ return `${m}m`;
48
+ return `${Math.ceil(m / 60)}h`;
49
+ }
50
+ /**
51
+ * Count trades with `from < t <= to` by side and sum their USD. The tape holds only the newest `cap`
52
+ * trades, so a full tape whose oldest trade is younger than `from` under-counts: `complete` is false.
53
+ */
54
+ export function tapeWindow(trades, from, to, cap = TAPE_CAP) {
55
+ let buys = 0, sells = 0, buyUsd = 0, sellUsd = 0;
56
+ let oldest = Infinity;
57
+ for (const tr of trades) {
58
+ if (tr.t < oldest)
59
+ oldest = tr.t;
60
+ if (tr.t <= from || tr.t > to)
61
+ continue;
62
+ const usd = Number.isFinite(tr.usd) ? tr.usd : 0;
63
+ if (tr.side === 'buy') {
64
+ buys += 1;
65
+ buyUsd += usd;
66
+ }
67
+ else {
68
+ sells += 1;
69
+ sellUsd += usd;
70
+ }
71
+ }
72
+ const complete = trades.length < cap || oldest <= from;
73
+ return { buys, sells, buyUsd, sellUsd, complete };
74
+ }
75
+ /**
76
+ * The ring sample to compare against for a window of `ms`: the one closest to `latestT - ms` within
77
+ * ±PREV_TOLERANCE·ms. When none qualifies, `waitMs` is how long until the next sample becomes old enough.
78
+ */
79
+ export function prevSample(samples, latestT, ms) {
80
+ const target = latestT - ms;
81
+ const tol = ms * PREV_TOLERANCE;
82
+ let best = null;
83
+ let bestD = Infinity;
84
+ let youngestTooNew = -Infinity; // the oldest sample still younger than (1 - tol) * ms
85
+ for (const s of samples) {
86
+ const d = Math.abs(s.t - target);
87
+ if (d <= tol && d < bestD) {
88
+ best = s;
89
+ bestD = d;
90
+ }
91
+ const age = latestT - s.t;
92
+ if (age < ms - tol && age > youngestTooNew)
93
+ youngestTooNew = age;
94
+ }
95
+ if (best)
96
+ return { sample: best };
97
+ const age = youngestTooNew === -Infinity ? 0 : youngestTooNew;
98
+ return { waitMs: Math.max(1, ms - tol - age) };
99
+ }
100
+ /** Thin a ring (oldest first): keep the newest; older samples need spacing ≥ max(5 s, 2 % of their age). */
101
+ export function thinSamples(samples) {
102
+ if (samples.length <= 1)
103
+ return samples.slice();
104
+ const latest = samples[samples.length - 1].t;
105
+ const out = [samples[samples.length - 1]];
106
+ let lastKept = latest;
107
+ for (let i = samples.length - 2; i >= 0; i--) {
108
+ const s = samples[i];
109
+ const age = latest - s.t;
110
+ if (age > MAX_AGE_MS)
111
+ break;
112
+ if (lastKept - s.t >= Math.max(MIN_SAMPLE_GAP_MS, age * THIN_FRACTION)) {
113
+ out.push(s);
114
+ lastKept = s.t;
115
+ }
116
+ }
117
+ out.reverse();
118
+ return out.length > MAX_SAMPLES ? out.slice(out.length - MAX_SAMPLES) : out;
119
+ }
120
+ /** Per-mint ring of DexScreener flow samples plus the row builder for the panel. In-memory only. */
121
+ export class FlowTracker {
122
+ rings = new Map();
123
+ lastRead = new Map();
124
+ clock;
125
+ minReadMs;
126
+ maxMints;
127
+ constructor(opts = {}) {
128
+ this.clock = opts.clock ?? (() => Date.now());
129
+ this.minReadMs = opts.minReadMs ?? 1_000;
130
+ this.maxMints = opts.maxMints ?? 24;
131
+ }
132
+ /** Add a DexScreener observation. Same-or-older `t` than the newest sample is ignored (cache re-reads). */
133
+ observe(mint, s) {
134
+ const ring = this.rings.get(mint) ?? [];
135
+ const last = ring[ring.length - 1];
136
+ if (last && s.t <= last.t)
137
+ return;
138
+ ring.push(s);
139
+ this.rings.delete(mint); // bump recency
140
+ this.rings.set(mint, thinSamples(ring));
141
+ while (this.rings.size > this.maxMints) {
142
+ const oldest = this.rings.keys().next().value;
143
+ this.rings.delete(oldest);
144
+ this.lastRead.delete(oldest);
145
+ }
146
+ }
147
+ samples(mint) {
148
+ return this.rings.get(mint) ?? [];
149
+ }
150
+ /**
151
+ * Pull the DexScreener pair `Store.enrich` cached for this mint (no network) into the ring. Throttled
152
+ * per mint; a cache miss or a pair without txns is a no-op.
153
+ */
154
+ syncFromCache(cache, mint) {
155
+ const now = this.clock();
156
+ const last = this.lastRead.get(mint);
157
+ if (last !== undefined && now - last < this.minReadMs)
158
+ return;
159
+ this.lastRead.set(mint, now);
160
+ let hit;
161
+ try {
162
+ hit = cache.get(DS_TOKEN_CACHE_NS, mint);
163
+ }
164
+ catch {
165
+ return;
166
+ }
167
+ const pair = hit?.value?.pair;
168
+ if (!hit || !pair || !pair.txns || !pair.volume)
169
+ return;
170
+ this.observe(mint, { t: pair.fetchedAt || hit.fetchedAt, txns: pair.txns, volume: pair.volume });
171
+ }
172
+ /**
173
+ * The panel rows: 1m from the tape, then the DexScreener windows. `tape` is the token view's trades
174
+ * and its panel status (the 1m window is anchored at the tape's fetch time, not the wall clock).
175
+ */
176
+ rows(mint, tape, now) {
177
+ const out = [];
178
+ const ring = this.samples(mint);
179
+ const latest = ring[ring.length - 1] ?? null;
180
+ for (const w of FLOW_WINDOWS) {
181
+ if (w.ds === null) {
182
+ out.push(tapeRow(w, tape, now));
183
+ continue;
184
+ }
185
+ if (!latest) {
186
+ out.push({ label: w.label, buys: null, sells: null, buyUsd: null, sellUsd: null, volUsd: null, trend: { na: '—' }, note: 'no dexscreener pair yet' });
187
+ continue;
188
+ }
189
+ const cur = latest.txns[w.ds] ?? null;
190
+ const prev = prevSample(ring, latest.t, w.ms);
191
+ out.push({
192
+ label: w.label,
193
+ buys: cur ? cur.buys : null,
194
+ sells: cur ? cur.sells : null,
195
+ buyUsd: null,
196
+ sellUsd: null,
197
+ volUsd: latest.volume[w.ds] ?? null,
198
+ trend: 'sample' in prev ? trendCell(cur, prev.sample.txns[w.ds] ?? null) : { wait: fmtWait(prev.waitMs) },
199
+ });
200
+ }
201
+ return out;
202
+ }
203
+ /** `dexscreener 8s · 1m from tape` — the panel caption (age of the newest DexScreener sample). */
204
+ caption(mint, now) {
205
+ const ring = this.samples(mint);
206
+ const latest = ring[ring.length - 1];
207
+ const ds = latest ? `dexscreener ${Math.max(0, Math.round((now - latest.t) / 1000))}s` : 'dexscreener —';
208
+ return `${ds} · 1m from tape`;
209
+ }
210
+ }
211
+ function tapeRow(w, tape, now) {
212
+ const p = tape.panel;
213
+ const empty = { label: w.label, buys: null, sells: null, buyUsd: null, sellUsd: null, volUsd: null, trend: { na: '—' } };
214
+ if (p.fetchedAt === null && (p.state === 'loading' || p.state === 'idle'))
215
+ return { ...empty, note: 'loading tape…' };
216
+ if (p.fetchedAt === null)
217
+ return { ...empty, note: `tape unavailable${p.reason ? `: ${p.reason}` : ''}` };
218
+ const anchor = p.fetchedAt ?? now;
219
+ const cur = tapeWindow(tape.trades, anchor - w.ms, Infinity);
220
+ const prev = tapeWindow(tape.trades, anchor - 2 * w.ms, anchor - w.ms);
221
+ return {
222
+ label: w.label,
223
+ buys: cur.buys,
224
+ sells: cur.sells,
225
+ buyUsd: cur.complete ? cur.buyUsd : null,
226
+ sellUsd: cur.complete ? cur.sellUsd : null,
227
+ volUsd: cur.buyUsd + cur.sellUsd,
228
+ partial: !cur.complete,
229
+ // A full tape that does not reach back far enough cannot give the window (counts get a `+`) or the
230
+ // previous minute honestly: say which, never compare against a truncated count.
231
+ trend: !cur.complete ? { na: 'partial' } : !prev.complete ? { na: 'prev n/a' } : trendCell(cur, prev),
232
+ };
233
+ }