dexterm-cli 0.1.6 → 0.1.7

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 (331) hide show
  1. package/dist/packages/agents/src/schema.js +22 -1
  2. package/dist/packages/agents/src/story.js +11 -2
  3. package/dist/packages/agents/src/tiers.js +27 -2
  4. package/dist/packages/agents/src/tools.js +30 -2
  5. package/dist/packages/autotrade/assets/exit-profile.schema.json +492 -0
  6. package/dist/packages/autotrade/assets/profiles/moonbag.json +46 -0
  7. package/dist/packages/autotrade/assets/profiles/runner.json +88 -0
  8. package/dist/packages/autotrade/assets/profiles/scalp.json +88 -0
  9. package/dist/packages/autotrade/assets/profiles/swing.json +88 -0
  10. package/dist/packages/autotrade/assets/strategies/moonbag.json +101 -0
  11. package/dist/packages/autotrade/assets/strategies/runner.json +101 -0
  12. package/dist/packages/autotrade/assets/strategies/sample-dip-ladder.json +188 -0
  13. package/dist/packages/autotrade/assets/strategies/scalp.json +98 -0
  14. package/dist/packages/autotrade/assets/strategies/swing.json +97 -0
  15. package/dist/packages/autotrade/assets/strategies/yolo.json +157 -0
  16. package/dist/packages/autotrade/assets/tapes/yolo-rehearsal.json +1 -0
  17. package/dist/packages/autotrade/src/account.js +74 -0
  18. package/dist/packages/autotrade/src/backtest.js +357 -0
  19. package/dist/packages/autotrade/src/basis-text.js +32 -0
  20. package/dist/packages/autotrade/src/chat/approvals.js +484 -0
  21. package/dist/packages/autotrade/src/chat/audit.js +140 -0
  22. package/dist/packages/autotrade/src/chat/config.js +93 -0
  23. package/dist/packages/autotrade/src/chat/connector.js +24 -0
  24. package/dist/packages/autotrade/src/chat/control.js +103 -0
  25. package/dist/packages/autotrade/src/chat/fake.js +100 -0
  26. package/dist/packages/autotrade/src/chat/format.js +71 -0
  27. package/dist/packages/autotrade/src/chat/hub.js +187 -0
  28. package/dist/packages/autotrade/src/chat/index.js +18 -0
  29. package/dist/packages/autotrade/src/chat/notify.js +292 -0
  30. package/dist/packages/autotrade/src/chat/pairing.js +46 -0
  31. package/dist/packages/autotrade/src/chat/redact.js +48 -0
  32. package/dist/packages/autotrade/src/chat/secrets.js +99 -0
  33. package/dist/packages/autotrade/src/chat/service.js +248 -0
  34. package/dist/packages/autotrade/src/chat/telegram.js +290 -0
  35. package/dist/packages/autotrade/src/desk-executor.js +188 -0
  36. package/dist/packages/autotrade/src/engine.js +387 -0
  37. package/dist/packages/autotrade/src/execution-port.js +97 -0
  38. package/dist/packages/autotrade/src/fleet-config.js +169 -0
  39. package/dist/packages/autotrade/src/fleet-control.js +276 -0
  40. package/dist/packages/autotrade/src/fleet.js +639 -0
  41. package/dist/packages/autotrade/src/index.js +53 -0
  42. package/dist/packages/autotrade/src/ledger.js +108 -0
  43. package/dist/packages/autotrade/src/live-source.js +182 -0
  44. package/dist/packages/autotrade/src/monitor-daemon.js +715 -0
  45. package/dist/packages/autotrade/src/monitor.js +190 -0
  46. package/dist/packages/autotrade/src/ollama-sessions.js +137 -0
  47. package/dist/packages/autotrade/src/paper-accounts.js +72 -0
  48. package/dist/packages/autotrade/src/replay.js +198 -0
  49. package/dist/packages/autotrade/src/runner-config.js +99 -0
  50. package/dist/packages/autotrade/src/runner.js +872 -0
  51. package/dist/packages/autotrade/src/safety.js +243 -0
  52. package/dist/packages/autotrade/src/session.js +269 -0
  53. package/dist/packages/autotrade/src/sessions.js +503 -0
  54. package/dist/packages/autotrade/src/store.js +250 -0
  55. package/dist/packages/autotrade/src/strategy.js +256 -0
  56. package/dist/packages/autotrade/src/trade-ledger.js +241 -0
  57. package/dist/packages/autotrade/src/yolo.js +140 -0
  58. package/dist/packages/cli/assets/claude/analyst-system.md +4 -4
  59. package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +1 -1
  60. package/dist/packages/cli/src/ai-cmd.js +412 -0
  61. package/dist/packages/cli/src/analyst/backend.js +82 -15
  62. package/dist/packages/cli/src/analyst/codex.js +14 -9
  63. package/dist/packages/cli/src/analyst/ollama.js +263 -0
  64. package/dist/packages/cli/src/analyst/session.js +5 -2
  65. package/dist/packages/cli/src/app-help-gen.js +44 -8
  66. package/dist/packages/cli/src/autotrade-cmd/audit.js +57 -0
  67. package/dist/packages/cli/src/autotrade-cmd/backtest.js +199 -0
  68. package/dist/packages/cli/src/autotrade-cmd/flatten.js +64 -0
  69. package/dist/packages/cli/src/autotrade-cmd/fleet.js +397 -0
  70. package/dist/packages/cli/src/autotrade-cmd/requests.js +79 -0
  71. package/dist/packages/cli/src/autotrade-cmd/run.js +413 -0
  72. package/dist/packages/cli/src/autotrade-cmd/stop.js +76 -0
  73. package/dist/packages/cli/src/autotrade-cmd/strategies.js +198 -0
  74. package/dist/packages/cli/src/autotrade-cmd/strategy-control.js +174 -0
  75. package/dist/packages/cli/src/autotrade-cmd/telegram-host.js +101 -0
  76. package/dist/packages/cli/src/autotrade-cmd.js +168 -0
  77. package/dist/packages/cli/src/display.js +156 -17
  78. package/dist/packages/cli/src/doctor.js +62 -3
  79. package/dist/packages/cli/src/export.js +10 -2
  80. package/dist/packages/cli/src/index.js +1 -1
  81. package/dist/packages/cli/src/ledger-cmd.js +288 -0
  82. package/dist/packages/cli/src/live/chart-fills.js +46 -0
  83. package/dist/packages/cli/src/live/index.js +23 -89
  84. package/dist/packages/cli/src/live/screens/token-live.js +9 -6
  85. package/dist/packages/cli/src/live/trade.js +12 -457
  86. package/dist/packages/cli/src/live/watchlist.js +1 -1
  87. package/dist/packages/cli/src/main.js +147 -190
  88. package/dist/packages/cli/src/mcp-cmd.js +7 -1
  89. package/dist/packages/cli/src/plan-cmd.js +1 -1
  90. package/dist/packages/cli/src/pnl-app.js +31 -21
  91. package/dist/packages/cli/src/pnl-card.js +158 -21
  92. package/dist/packages/cli/src/popup.js +22 -6
  93. package/dist/packages/cli/src/record-cmd.js +141 -20
  94. package/dist/packages/cli/src/screens/agent-hud.js +34 -2
  95. package/dist/packages/cli/src/screens/analyst-session.js +7 -4
  96. package/dist/packages/cli/src/screens/analyst.js +3 -2
  97. package/dist/packages/cli/src/screens/autotrade-keys.js +169 -0
  98. package/dist/packages/cli/src/screens/autotrade-proposals.js +123 -0
  99. package/dist/packages/cli/src/screens/autotrade-screen.js +725 -0
  100. package/dist/packages/cli/src/screens/autotrade-ui.js +640 -0
  101. package/dist/packages/cli/src/screens/autotrade-worker.js +89 -0
  102. package/dist/packages/cli/src/screens/brain.js +37 -11
  103. package/dist/packages/cli/src/screens/overlay.js +5 -2
  104. package/dist/packages/cli/src/screens/plans.js +5 -17
  105. package/dist/packages/cli/src/screens/pnl-chip.js +9 -8
  106. package/dist/packages/cli/src/screens/positions.js +6 -28
  107. package/dist/packages/cli/src/screens/ticket.js +38 -205
  108. package/dist/packages/cli/src/screens/token.js +3 -3
  109. package/dist/packages/cli/src/state.js +25 -19
  110. package/dist/packages/cli/src/telegram-cmd.js +451 -0
  111. package/dist/packages/cli/src/trade.js +3 -128
  112. package/dist/packages/cli/src/version.js +1 -1
  113. package/dist/packages/cli/src/wallet-cmd.js +39 -340
  114. package/dist/packages/cli/src/wallet-command.js +8 -11
  115. package/dist/packages/cli/src/widgets.js +0 -12
  116. package/dist/packages/custom/src/config.js +20 -2
  117. package/dist/packages/custom/src/schema.js +35 -10
  118. package/dist/packages/custom/src/workspace.js +3 -3
  119. package/dist/packages/data/src/basis.js +191 -0
  120. package/dist/packages/data/src/detail.js +53 -2
  121. package/dist/packages/data/src/index.js +0 -14
  122. package/dist/packages/data/src/store.js +14 -5
  123. package/dist/packages/data/src/trade/agent/paper-desk.js +228 -16
  124. package/dist/packages/data/src/trade/agent/rendezvous.js +3 -2
  125. package/dist/packages/data/src/trade/bridge.js +4 -102
  126. package/dist/packages/data/src/trade/ledger/export.js +101 -0
  127. package/dist/packages/data/src/trade/ledger/file.js +135 -0
  128. package/dist/packages/data/src/trade/ledger/fold.js +191 -0
  129. package/dist/packages/data/src/trade/ledger/index.js +9 -0
  130. package/dist/packages/data/src/trade/ledger/schema.js +83 -0
  131. package/dist/packages/data/src/trade/ledger/sources.js +307 -0
  132. package/dist/packages/data/src/trade/ledger/stats.js +123 -0
  133. package/dist/packages/data/src/trade/ledger/tags.js +116 -0
  134. package/dist/packages/data/src/trade/ledger/view.js +51 -0
  135. package/dist/packages/data/src/trade/plans/index.js +0 -1
  136. package/dist/packages/data/src/trade/plans/service.js +3 -27
  137. package/dist/packages/mcp/assets/kb/app-help/autotrade-chat.md +48 -0
  138. package/dist/packages/mcp/assets/kb/app-help/autotrade-parallel.md +42 -0
  139. package/dist/packages/mcp/assets/kb/app-help/autotrade-runner.md +35 -0
  140. package/dist/packages/mcp/assets/kb/app-help/autotrade-stop.md +37 -0
  141. package/dist/packages/mcp/assets/kb/app-help/autotrade-strategies.md +35 -0
  142. package/dist/packages/mcp/assets/kb/app-help/autotrade-telegram.md +40 -0
  143. package/dist/packages/mcp/assets/kb/app-help/chart-trade-markers.md +42 -0
  144. package/dist/packages/mcp/assets/kb/app-help/link-a-wallet.md +4 -10
  145. package/dist/packages/mcp/assets/kb/app-help/live-trading.md +38 -0
  146. package/dist/packages/mcp/assets/kb/app-help/live-yolo.md +34 -0
  147. package/dist/packages/mcp/assets/kb/app-help/order-plans.md +1 -1
  148. package/dist/packages/mcp/assets/kb/app-help/price-or-marketcap.md +5 -2
  149. package/dist/packages/mcp/assets/kb/app-help/screen-analyst.md +3 -0
  150. package/dist/packages/mcp/assets/kb/app-help/screen-autotrade.md +217 -0
  151. package/dist/packages/mcp/assets/kb/app-help/screen-kol.md +3 -0
  152. package/dist/packages/mcp/assets/kb/app-help/screen-newpairs.md +3 -0
  153. package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +3 -0
  154. package/dist/packages/mcp/assets/kb/app-help/screen-screener.md +3 -0
  155. package/dist/packages/mcp/assets/kb/app-help/screen-token.md +3 -0
  156. package/dist/packages/mcp/assets/kb/app-help/screen-trending.md +3 -0
  157. package/dist/packages/mcp/assets/kb/app-help/screen-watchlist.md +3 -0
  158. package/dist/packages/mcp/assets/kb/app-help/standing-mandates.md +1 -1
  159. package/dist/packages/mcp/assets/kb/app-help/trade-ledger.md +54 -0
  160. package/dist/packages/mcp/assets/kb/app-help/use-local-model.md +38 -0
  161. package/dist/packages/mcp/assets/kb/docs/docs-agents-agent-paper-trading.md +4 -1
  162. package/dist/packages/mcp/assets/kb/docs/docs-agents-reading-your-trade-ledger.md +31 -0
  163. package/dist/packages/mcp/assets/kb/docs/docs-agents-untrusted-by-default.md +1 -1
  164. package/dist/packages/mcp/assets/kb/docs/docs-analyst-if-claude-is-missing.md +1 -1
  165. package/dist/packages/mcp/assets/kb/docs/docs-analyst-use-a-local-model-instead.md +27 -0
  166. package/dist/packages/mcp/assets/kb/docs/docs-analyst-what-it-sees.md +2 -0
  167. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-backtests-and-their-limits.md +56 -0
  168. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-change-a-strategy-by-chat.md +49 -0
  169. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-checking-the-record-of-a-run.md +28 -0
  170. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-comparing-them.md +41 -0
  171. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-exits-and-wakes.md +76 -0
  172. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-how-a-stop-reaches-the-runner.md +32 -0
  173. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-on-the-q-screen.md +37 -0
  174. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-one-strategy-or-all-of-them.md +35 -0
  175. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-paper-accounts.md +50 -0
  176. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-presets.md +37 -0
  177. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-running-it-all-day.md +31 -0
  178. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-sessions-and-their-limits.md +31 -0
  179. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-several-strategies-at-once.md +33 -0
  180. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-stopping-it.md +35 -0
  181. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-strategies-and-presets.md +34 -0
  182. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-autotrade-json-file.md +48 -0
  183. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-heartbeat.md +27 -0
  184. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-run-file-and-the-heartbeat.md +34 -0
  185. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-runner-and-candidate-sessions.md +45 -0
  186. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-each-session-shows-first.md +31 -0
  187. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-costs-claude-usage-per-session.md +58 -0
  188. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-is-paper-self-directed.md +43 -0
  189. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-will-not-do.md +31 -0
  190. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-which-account-day-caps-and-the-usage-guard.md +44 -0
  191. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-your-strategies.md +37 -0
  192. package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +22 -0
  193. package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
  194. package/dist/packages/mcp/assets/kb/docs/docs-beginners.md +1 -1
  195. package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-markers.md +30 -0
  196. package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-y-axis.md +1 -1
  197. package/dist/packages/mcp/assets/kb/docs/docs-configuration-environment-variables.md +4 -1
  198. package/dist/packages/mcp/assets/kb/docs/docs-configuration-files-on-disk.md +1 -0
  199. package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -1
  200. package/dist/packages/mcp/assets/kb/docs/docs-configuration-price-display-mode.md +8 -6
  201. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-2-the-one-key-mirror-ticket-a-proposal.md +1 -1
  202. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-3-capped-auto-mirror-default-off-less.md +1 -1
  203. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-what-is-proven-what-is-not.md +1 -1
  204. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
  205. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +36 -0
  206. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-sources.md +1 -0
  207. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-first-run.md +1 -1
  208. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
  209. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +1 -1
  210. package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +1 -1
  211. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-changed-in-0-1-7.md +26 -0
  212. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-stopping.md +28 -0
  213. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-the-short-version.md +34 -0
  214. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-using-a-trading-bot-with-dexterm.md +33 -0
  215. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does-not-do.md +30 -0
  216. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does.md +29 -0
  217. package/dist/packages/mcp/assets/kb/docs/docs-live-trading.md +23 -0
  218. package/dist/packages/mcp/assets/kb/docs/docs-ollama-another-machine.md +28 -0
  219. package/dist/packages/mcp/assets/kb/docs/docs-ollama-autotrade-on-a-local-model.md +29 -0
  220. package/dist/packages/mcp/assets/kb/docs/docs-ollama-check-your-model-dexterm-ai-probe.md +38 -0
  221. package/dist/packages/mcp/assets/kb/docs/docs-ollama-how-we-checked.md +27 -0
  222. package/dist/packages/mcp/assets/kb/docs/docs-ollama-models-without-tool-calling.md +32 -0
  223. package/dist/packages/mcp/assets/kb/docs/docs-ollama-pick-the-engine-globally-or-per-surface.md +43 -0
  224. package/dist/packages/mcp/assets/kb/docs/docs-ollama-restriction-profiles.md +35 -0
  225. package/dist/packages/mcp/assets/kb/docs/docs-ollama-set-it-up-in-two-minutes.md +39 -0
  226. package/dist/packages/mcp/assets/kb/docs/docs-ollama-troubleshooting.md +35 -0
  227. package/dist/packages/mcp/assets/kb/docs/docs-ollama-what-runs-on-a-local-model.md +39 -0
  228. package/dist/packages/mcp/assets/kb/docs/docs-ollama-what-stays-outside-the-model.md +36 -0
  229. package/dist/packages/mcp/assets/kb/docs/docs-ollama-which-model.md +38 -0
  230. package/dist/packages/mcp/assets/kb/docs/docs-ollama-your-own-agent-on-the-local-model.md +28 -0
  231. package/dist/packages/mcp/assets/kb/docs/docs-ollama.md +28 -0
  232. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-letting-an-agent-paper-trade-experimental.md +3 -4
  233. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-link-a-wallet-read-only.md +30 -0
  234. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-order-plans-and-standing-mandates.md +3 -4
  235. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-panic.md +4 -7
  236. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-paper-only-mandates.md +6 -7
  237. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-pnl-cards.md +7 -7
  238. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-safety-model.md +34 -0
  239. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-ticket.md +3 -4
  240. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-trade-ledger.md +42 -0
  241. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-wallet.md +4 -5
  242. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-trade-moments.md +4 -5
  243. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +4 -5
  244. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-buy-and-sell-markers.md +37 -0
  245. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-what-it-shows.md +2 -1
  246. package/dist/packages/mcp/assets/kb/docs/docs-screens-autotrade-paper.md +26 -0
  247. package/dist/packages/mcp/assets/kb/docs/docs-screens-order-plans.md +1 -1
  248. package/dist/packages/mcp/assets/kb/docs/docs-screens-token-screen.md +2 -0
  249. package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-brain-view-and-the-pnl-chip.md +1 -1
  250. package/dist/packages/mcp/assets/kb/docs/docs-telegram-approve-one-tap-entries.md +50 -0
  251. package/dist/packages/mcp/assets/kb/docs/docs-telegram-control-the-commands.md +43 -0
  252. package/dist/packages/mcp/assets/kb/docs/docs-telegram-if-something-goes-wrong.md +30 -0
  253. package/dist/packages/mcp/assets/kb/docs/docs-telegram-notify-what-reaches-your-phone.md +41 -0
  254. package/dist/packages/mcp/assets/kb/docs/docs-telegram-set-it-up.md +47 -0
  255. package/dist/packages/mcp/assets/kb/docs/docs-telegram-the-record.md +26 -0
  256. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +33 -0
  257. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-will-not-do.md +31 -0
  258. package/dist/packages/mcp/assets/kb/docs/docs-telegram.md +23 -0
  259. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-common-questions.md +1 -1
  260. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-doctor-lines.md +4 -0
  261. package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +1 -1
  262. package/dist/packages/mcp/assets/kb/docs/docs-voice.md +1 -1
  263. package/dist/packages/mcp/assets/kb/docs/docs-yolo-1-replay-it-on-your-own-history.md +42 -0
  264. package/dist/packages/mcp/assets/kb/docs/docs-yolo-2-run-it-on-paper.md +34 -0
  265. package/dist/packages/mcp/assets/kb/docs/docs-yolo-3-watch-it-and-stop-it.md +32 -0
  266. package/dist/packages/mcp/assets/kb/docs/docs-yolo-the-rules.md +37 -0
  267. package/dist/packages/mcp/assets/kb/docs/docs-yolo-what-it-is.md +34 -0
  268. package/dist/packages/mcp/assets/kb/docs/docs-yolo-what-it-will-not-do.md +31 -0
  269. package/dist/packages/mcp/assets/kb/docs/docs-yolo.md +26 -0
  270. package/dist/packages/mcp/assets/kb/entries/autotrade.md +38 -0
  271. package/dist/packages/mcp/src/app-help.js +1 -1
  272. package/dist/packages/mcp/src/autotrade-tools.js +944 -0
  273. package/dist/packages/mcp/src/backtest-requests.js +71 -0
  274. package/dist/packages/mcp/src/engine/config.js +159 -0
  275. package/dist/packages/mcp/src/engine/index.js +10 -0
  276. package/dist/packages/mcp/src/engine/loop.js +101 -0
  277. package/dist/packages/mcp/src/engine/mcp-client.js +123 -0
  278. package/dist/packages/mcp/src/engine/ollama.js +258 -0
  279. package/dist/packages/mcp/src/engine/probe.js +110 -0
  280. package/dist/packages/mcp/src/engine/reason.js +35 -0
  281. package/dist/packages/mcp/src/engine/restrictions.js +111 -0
  282. package/dist/packages/mcp/src/index.js +7 -1
  283. package/dist/packages/mcp/src/ledger-tools.js +346 -0
  284. package/dist/packages/mcp/src/paper-agent.js +2 -0
  285. package/dist/packages/mcp/src/server.js +127 -19
  286. package/dist/packages/packs/src/backtest.js +3 -1
  287. package/dist/packages/popup/src/pages/chart-lib.js +134 -21
  288. package/dist/packages/popup/src/pages/chart.css +9 -0
  289. package/dist/packages/popup/src/pages/chart.html +3 -1
  290. package/dist/packages/popup/src/pages/chart.js +104 -7
  291. package/dist/packages/popup/src/protocol.js +2 -0
  292. package/dist/packages/popup/src/server.js +4 -0
  293. package/dist/packages/recorder/src/index.js +2 -0
  294. package/dist/packages/recorder/src/recorder.js +63 -6
  295. package/dist/packages/recorder/src/retention.js +100 -0
  296. package/dist/packages/recorder/src/schema.js +34 -1
  297. package/dist/packages/recorder/src/ticks.js +237 -0
  298. package/dist/packages/tui/src/index.js +1 -0
  299. package/dist/packages/tui/src/keymap.js +21 -1
  300. package/dist/packages/tui/src/markers.js +219 -0
  301. package/dist/packages/tui/src/widgets.js +149 -2
  302. package/package.json +2 -1
  303. package/dist/packages/data/src/trade/caps.js +0 -323
  304. package/dist/packages/data/src/trade/executor.js +0 -933
  305. package/dist/packages/data/src/trade/plans/executor-live.js +0 -220
  306. package/dist/packages/data/src/trade/rpc-exec.js +0 -229
  307. package/dist/packages/data/src/trade/signer/client.js +0 -197
  308. package/dist/packages/data/src/trade/signer/fast-wallet.js +0 -154
  309. package/dist/packages/data/src/trade/signer/host.js +0 -97
  310. package/dist/packages/data/src/trade/signer/main.js +0 -121
  311. package/dist/packages/data/src/trade/signer/probe-builder.js +0 -85
  312. package/dist/packages/data/src/trade/signer/protocol.js +0 -92
  313. package/dist/packages/data/src/trade/signer/service.js +0 -400
  314. package/dist/packages/data/src/trade/signer/sol-usd-feed.js +0 -98
  315. package/dist/packages/data/src/trade/sol-usd.js +0 -86
  316. package/dist/packages/data/src/trade/swap-build.js +0 -108
  317. package/dist/packages/data/src/trade/token-delta.js +0 -201
  318. package/dist/packages/data/vendor/solana-web3/LICENSE +0 -20
  319. package/dist/packages/data/vendor/solana-web3/NOTICE.md +0 -13
  320. package/dist/packages/data/vendor/solana-web3/index.iife.min.js +0 -20
  321. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-a-live-ticket.md +0 -42
  322. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-before-you-press-size-vs-caps.md +0 -34
  323. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-devnet-signer-mainnet-token.md +0 -35
  324. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-jupiter-rate-limits.md +0 -33
  325. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-link-a-wallet.md +0 -32
  326. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-live-trading-devnet-first.md +0 -30
  327. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-mainnet.md +0 -31
  328. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-live-fire-path-devnet-the-fast-wallet.md +0 -41
  329. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-signer-process.md +0 -34
  330. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-what-the-live-path-keeps.md +0 -37
  331. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-your-first-live-trade.md +0 -41
@@ -0,0 +1,248 @@
1
+ // dexterm autotrade chat — the service a runner host starts (task-1527, AUTOTRADE-SPEC §8.1).
2
+ //
3
+ // One process: the runner (`dexterm autotrade run … --paper`) starts the connector when Telegram is set up and
4
+ // paired, so chat is a remote control for THIS local engine. Pieces:
5
+ // ChatHub inbound updates: dedupe, allowlist, rate limits, commands → ControlRouter, taps → broker
6
+ // ApprovalBroker proposals and confirm buttons (the runner's ApprovalHook)
7
+ // NotifyBus the run ledger as it is written → messages
8
+ // HostControlPort the router's actions, through the SAME control file the CLI and the Q screen write
9
+ // (autotrade/control.json, by: "chat"), the runner's live state, and the strategy store
10
+ // The runner never waits on chat: the bus queues, the broker answers when a tap lands, and a dead connection means
11
+ // no entries that need an approval (UNDELIVERED = skip), never a blocked exit.
12
+ import { riskDelta } from '../../../mcp/src/index.js';
13
+ import { StrategyStore } from "../store.js";
14
+ import { requestControl } from "../safety.js";
15
+ import { badgeOf } from "../execution-port.js";
16
+ import { ApprovalBroker, CallbackStore, callbackDbPath } from "./approvals.js";
17
+ import { ChatAudit } from "./audit.js";
18
+ import { defaultTelegram, loadConnectors, saveConnectors } from "./config.js";
19
+ import { ChatHub } from "./hub.js";
20
+ import { NotifyBus } from "./notify.js";
21
+ /** connectors.json re-read when it changes (a local `dexterm telegram approvals off` lands without a restart). */
22
+ export class ConfigView {
23
+ cached = null;
24
+ at = 0;
25
+ home;
26
+ now;
27
+ constructor(home, now = Date.now, initial) { this.home = home; this.now = now; this.cached = initial ?? null; }
28
+ get = () => {
29
+ if (!this.cached || this.now() - this.at > 2000) {
30
+ const r = loadConnectors(this.home);
31
+ // A config that turns unreadable mid-run keeps the last good one for reads but takes away every tier.
32
+ this.cached = r.ok ? r.config.telegram : { ...(this.cached ?? defaultTelegram()), tiers: [] };
33
+ this.at = this.now();
34
+ }
35
+ return this.cached;
36
+ };
37
+ /** The one chat-side write: approvals off → on. */
38
+ approvalsOn() {
39
+ const r = loadConnectors(this.home);
40
+ if (!r.ok)
41
+ return false;
42
+ if (r.config.telegram.approvals !== 'on') {
43
+ r.config.telegram.approvals = 'on';
44
+ saveConnectors(this.home, r.config);
45
+ }
46
+ this.cached = null;
47
+ return true;
48
+ }
49
+ }
50
+ /** The router's actions for an in-process runner. Every write is the control file, as the CLI writes it. */
51
+ export class HostControlPort {
52
+ d;
53
+ constructor(d) { this.d = d; }
54
+ store() { return this.d.store ?? new StrategyStore({ home: this.d.home }); }
55
+ status() {
56
+ const st = this.d.state();
57
+ const runId = this.d.runId();
58
+ if (!st || !runId)
59
+ return null;
60
+ const start = st.book.equityUsd - st.book.realizedTodayUsd;
61
+ const s = this.d.strategy();
62
+ const cfg = this.d.config.get();
63
+ return {
64
+ runId, state: st.state, haltReason: st.haltReason, strategy: st.strategy, open: st.positions.length, candidates: st.candidates.length,
65
+ dayPnlPct: start > 0 ? (st.book.realizedTodayUsd / start) * 100 : null, spend: { todayUsd: st.spend.todayUsd, dayCapUsd: st.spend.dayCapUsd }, allowance: st.spend.allowance,
66
+ approvals: s?.entry.decide === 'agent+approve' ? 'required by the strategy' : cfg.approvals,
67
+ };
68
+ }
69
+ positions() {
70
+ return (this.d.state()?.positions ?? []).map((p) => ({ mint: p.mint, symbol: p.symbol, profile: p.profile, pnlPct: p.pnlPct, freeBag: !!p.freeBag, next: p.next }));
71
+ }
72
+ control(action) {
73
+ const runId = this.d.runId();
74
+ if (!runId)
75
+ return { ok: false, message: 'No runner is running on your computer.' };
76
+ const w = requestControl(this.d.home, runId, action, 'chat');
77
+ if (!w.ok)
78
+ return { ok: false, message: `Refused: ${w.why}.` };
79
+ return { ok: true, message: w.already ? 'Already requested.' : '' };
80
+ }
81
+ pause() {
82
+ const r = this.control('pause');
83
+ return r.ok ? { ok: true, message: 'Paused: no new entries. Open positions keep their exits. /resume lifts it.' } : r;
84
+ }
85
+ resume() {
86
+ const st = this.d.state();
87
+ if (!st)
88
+ return { ok: false, message: 'No runner is running on your computer.' };
89
+ // Only a pause lifts from chat. A kill, a stop, the panic switch, a breaker halt or a guard stays local.
90
+ if (st.state !== 'paused' || st.haltReason !== 'paused') {
91
+ return { ok: false, message: st.state === 'running' ? 'Not paused.' : `Refused from chat: the runner is ${st.state}${st.haltReason ? ` (${st.haltReason})` : ''}. That clears only on your computer.` };
92
+ }
93
+ const r = this.control('resume-entries');
94
+ return r.ok ? { ok: true, message: 'Resumed: new entries allowed again.' } : r;
95
+ }
96
+ kill() {
97
+ const r = this.control('stop');
98
+ return r.ok ? { ok: true, message: `${r.message || 'Stopped'}: no new entries, sessions closed, pending orders dropped. Open positions keep their exits; the run ends when nothing is held. Starting again is local.` } : r;
99
+ }
100
+ flatten() {
101
+ const r = this.control('flatten');
102
+ return r.ok ? { ok: true, message: 'Flatten requested: every position sells at its next tick, then the run ends.' } : r;
103
+ }
104
+ strategies() {
105
+ const cur = this.d.strategy();
106
+ const st = this.store();
107
+ const out = [];
108
+ for (const e of st.list()) {
109
+ if (!e.ok || !e.hash || !e.version)
110
+ continue;
111
+ const g = st.get(e.id);
112
+ if (!g || !g.ok)
113
+ continue;
114
+ const s = g.strategy;
115
+ let allowed = false;
116
+ let why = 'no strategy is running';
117
+ if (cur) {
118
+ if (s.hash === cur.hash) {
119
+ allowed = false;
120
+ why = 'running';
121
+ }
122
+ else if (s.mode !== cur.mode)
123
+ why = 'changes paper/live';
124
+ else {
125
+ const d = riskDelta(cur, s);
126
+ const looser = d.reasons.filter((r) => r.dir === 'looser');
127
+ allowed = looser.length === 0;
128
+ why = allowed ? (d.delta === 'tighter' ? 'tighter' : 'equal') : `looser: ${looser.slice(0, 2).map((r) => r.path).join(', ')}`;
129
+ }
130
+ }
131
+ out.push({ id: e.id, version: s.version, hash: s.hash, preset: e.source === 'preset', current: !!cur && cur.hash === s.hash, allowed, why });
132
+ }
133
+ return out;
134
+ }
135
+ use(id) {
136
+ const choice = this.strategies().find((x) => x.id === id);
137
+ if (!choice || !choice.allowed)
138
+ return { ok: false, message: `Refused: ${id} is not an equal-or-tighter validated strategy now.` };
139
+ const g = this.store().get(id);
140
+ if (!g || !g.ok || g.strategy.hash !== choice.hash)
141
+ return { ok: false, message: 'Refused: that strategy changed since it was listed.' };
142
+ const r = this.control('stop');
143
+ if (!r.ok)
144
+ return r;
145
+ this.d.onSwitch(g.strategy);
146
+ return { ok: true, message: `Switching to ${id}: no new entries now; it starts once the open positions have closed.` };
147
+ }
148
+ approvalsOn() {
149
+ return this.d.config.approvalsOn() ? { ok: true, message: 'Approvals on: every new entry now waits for your tap. Turning them off is local only.' } : { ok: false, message: 'Could not write the setting.' };
150
+ }
151
+ sell(mint) {
152
+ const held = (this.d.state()?.positions ?? []).some((p) => p.mint === mint);
153
+ if (!held)
154
+ return { ok: false, message: 'Not held any more.' };
155
+ const b = this.d.broker();
156
+ if (!b)
157
+ return { ok: false, message: 'Not available.' };
158
+ b.queueExit(mint, 'a chat button');
159
+ return { ok: true, message: 'Sell queued: it goes at the next tick.' };
160
+ }
161
+ }
162
+ /** What each confirm button runs once its tap is verified (the same actions as the commands, nothing more). */
163
+ export function wireConfirms(broker, port) {
164
+ broker.onConfirm('flatten', () => port.flatten().message);
165
+ broker.onConfirm('use', (r) => port.use(String(r.payload.id)).message);
166
+ broker.onConfirm('sell', (r) => port.sell(String(r.payload.mint)).message);
167
+ }
168
+ export class TelegramService {
169
+ hub;
170
+ broker;
171
+ bus;
172
+ audit;
173
+ config;
174
+ port;
175
+ o;
176
+ now;
177
+ ac = new AbortController();
178
+ polling = null;
179
+ ticker = null;
180
+ store;
181
+ constructor(o) {
182
+ this.o = o;
183
+ this.now = o.now ?? Date.now;
184
+ this.config = o.config ?? new ConfigView(o.home, this.now);
185
+ this.audit = o.audit ?? new ChatAudit(o.home, { knownToken: o.knownToken });
186
+ this.store = o.store ?? new CallbackStore(callbackDbPath(o.home));
187
+ const cfg = this.config.get;
188
+ this.broker = new ApprovalBroker({ connector: o.connector, store: this.store, audit: this.audit, port: o.port, config: cfg, now: this.now, maxOpen: o.maxOpen, timers: o.timers, alert: o.alert });
189
+ this.port = new HostControlPort({ home: o.home, runId: o.runId, state: o.state, strategy: o.strategy, config: this.config, broker: () => this.broker, onSwitch: o.onSwitch ?? (() => { }) });
190
+ this.hub = new ChatHub({ connector: o.connector, config: cfg, audit: this.audit, badge: badgeOf(o.port.network), broker: this.broker, port: this.port, now: this.now, alert: o.alert });
191
+ const delays = o.retryDelaysMs ?? [1000, 4000];
192
+ this.bus = new NotifyBus({
193
+ badge: badgeOf(o.port.network), config: cfg, now: this.now, card: o.card, approvalCounts: (since) => this.store.countsSince(since),
194
+ send: async (msg, x) => {
195
+ for (let i = 0;; i++) {
196
+ const failedBefore = this.audit.events.length;
197
+ await this.hub.broadcast(msg, x.template);
198
+ const failed = this.audit.events.slice(failedBefore).some((e) => e.kind === 'out_failed');
199
+ if (!failed || i >= delays.length)
200
+ return;
201
+ await new Promise((r) => setTimeout(r, delays[i]));
202
+ }
203
+ },
204
+ });
205
+ wireConfirms(this.broker, this.port);
206
+ }
207
+ /** Expire leftovers, start polling and the notify ticker. Resolves once started (polling runs in the background). */
208
+ async start() {
209
+ const n = await this.broker.expireLeftovers();
210
+ this.audit.add(this.now(), 'connector_start', { transport: this.o.connector.transport, expiredLeftovers: n, network: this.o.port.network });
211
+ this.polling = this.o.connector.start(this.ac.signal).catch((e) => { this.audit.add(this.now(), 'connector_error', { error: e instanceof Error ? e.message.slice(0, 160) : String(e) }); });
212
+ if (this.o.timers !== false) {
213
+ this.ticker = setInterval(() => { void this.tick(); }, 5000);
214
+ this.ticker.unref?.();
215
+ }
216
+ }
217
+ conflictAlerted = false;
218
+ async tick() {
219
+ const h = this.o.connector.health();
220
+ if (h.conflict && !this.conflictAlerted) {
221
+ this.conflictAlerted = true;
222
+ this.audit.add(this.now(), 'connector_conflict', { why: h.conflict });
223
+ this.o.alert?.(`telegram: ${h.conflict}. Approvals are stopped; local controls keep working. Revoke the token with BotFather (/revoke) and run \`dexterm telegram setup\` again.`);
224
+ this.broker.cancelAll('connector-conflict');
225
+ }
226
+ await this.broker.sweep();
227
+ await this.bus.tick();
228
+ }
229
+ /** The run ledger tap: every event, as it is written. */
230
+ onLedger = (e) => {
231
+ this.bus.onEvent(e);
232
+ if (e.kind === 'fill' && typeof e.proposalId === 'string')
233
+ this.broker.filled(e.proposalId);
234
+ };
235
+ onState = (st) => { this.bus.onState(st); };
236
+ async stop() {
237
+ this.broker.cancelAll('connector-stopped');
238
+ if (this.ticker)
239
+ clearInterval(this.ticker);
240
+ await this.bus.flush();
241
+ await this.hub.idle();
242
+ this.ac.abort();
243
+ await this.polling;
244
+ this.broker.close();
245
+ this.audit.add(this.now(), 'connector_stop', { transport: this.o.connector.transport });
246
+ this.store.close();
247
+ }
248
+ }
@@ -0,0 +1,290 @@
1
+ // dexterm autotrade chat — the Telegram transport (task-1527, AUTOTRADE-SPEC §8.2). The user's OWN bot.
2
+ //
3
+ // Long polling from the user's machine: `getUpdates` (timeout 50 s, allowed_updates message + callback_query,
4
+ // offset = last update_id + 1, a dedupe set on top). There is no webhook and no inbound port, so nothing outside can
5
+ // inject an update: every update comes from this process's own poll, and Telegram stamps `from.id` server-side.
6
+ //
7
+ // sendMessage plain text: NO parse_mode, link previews off. A token name cannot inject markup or a link.
8
+ // sendPhoto the PnL card (multipart, the PNG bytes from the card renderer).
9
+ // editMessageText a resolved proposal is edited to its final line and loses its buttons.
10
+ // answerCallbackQuery the toast after a tap.
11
+ // getWebhookInfo on start and every 10 minutes: a webhook that is set, or a 409 on getUpdates, means someone
12
+ // else holds the token. The connector reports a conflict: approvals stop (the broker refuses
13
+ // every callback while it is set), the user is alerted locally, local controls keep working.
14
+ //
15
+ // Rate limits: outbound 1 message/s per chat with a burst of 3 (the notify bus digests above 20/min); a 429's
16
+ // `retry_after` is honoured. The token is read once from the secret store and kept in this closure; every error that
17
+ // leaves this file goes through redactedError(), because the Bot API URL contains the token.
18
+ import { parseCommand } from "./connector.js";
19
+ import { clip, plain } from "./format.js";
20
+ import { redact, redactedError } from "./redact.js";
21
+ import { createHash } from 'node:crypto';
22
+ export const TELEGRAM_API = 'https://api.telegram.org';
23
+ export const POLL_TIMEOUT_SEC = 50;
24
+ export const WEBHOOK_CHECK_MS = 10 * 60_000;
25
+ /** Messages older than this when the poll starts are dropped (taps are checked against their own expiry anyway). */
26
+ export const STALE_COMMAND_MS = 120_000;
27
+ const relayed = (m) => m.forward_origin !== undefined || m.forward_from !== undefined || m.forward_from_chat !== undefined
28
+ || m.forward_sender_name !== undefined || m.forward_date !== undefined || m.via_bot !== undefined || m.is_automatic_forward === true;
29
+ export class TelegramApiError extends Error {
30
+ code;
31
+ retryAfter;
32
+ constructor(code, description, retryAfter, knownToken) {
33
+ super(`telegram ${code}: ${redact(description, knownToken).slice(0, 200)}`);
34
+ this.name = 'TelegramApiError';
35
+ this.code = code;
36
+ this.retryAfter = retryAfter;
37
+ }
38
+ }
39
+ const defaultSleep = (ms, signal) => new Promise((r) => {
40
+ if (signal?.aborted) {
41
+ r();
42
+ return;
43
+ }
44
+ const t = setTimeout(r, ms);
45
+ signal?.addEventListener('abort', () => { clearTimeout(t); r(); }, { once: true });
46
+ });
47
+ const kindOf = (c) => (c.type === 'private' ? 'private' : 'group');
48
+ export class TelegramConnector {
49
+ transport = 'telegram';
50
+ o;
51
+ fetchFn;
52
+ now;
53
+ sleep;
54
+ tokenValue = null;
55
+ offset = 0;
56
+ seen = new Set();
57
+ cmdH = [];
58
+ actH = [];
59
+ txtH = [];
60
+ h = { transport: 'telegram', lastPollAt: 0, conflict: null, rateLimited: 0, errors: 0, lastError: null };
61
+ /** Per chat send budget (1/s, burst 3). */
62
+ buckets = new Map();
63
+ lastWebhookCheck = 0;
64
+ startedAt = 0;
65
+ constructor(o) {
66
+ this.o = o;
67
+ this.fetchFn = o.fetch ?? ((u, i) => fetch(u, i));
68
+ this.now = o.now ?? Date.now;
69
+ this.sleep = o.sleep ?? defaultSleep;
70
+ }
71
+ token() {
72
+ if (this.tokenValue === null)
73
+ this.tokenValue = this.o.token();
74
+ if (!this.tokenValue)
75
+ throw new Error('no bot token: run `dexterm telegram setup`');
76
+ return this.tokenValue;
77
+ }
78
+ log(s) { this.o.onLog?.(redact(s, this.tokenValue)); }
79
+ /** One Bot API call. Requests are built here only; nothing outside sees the URL. */
80
+ async call(method, body, signal, timeoutMs = 15_000) {
81
+ const url = `${this.o.apiBase ?? TELEGRAM_API}/bot${this.token()}/${method}`;
82
+ const sig = signal ? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs);
83
+ let res;
84
+ try {
85
+ res = await this.fetchFn(url, body instanceof FormData
86
+ ? { method: 'POST', body, signal: sig }
87
+ : { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body ?? {}), signal: sig });
88
+ }
89
+ catch (e) {
90
+ throw redactedError(e, this.tokenValue);
91
+ }
92
+ let j;
93
+ try {
94
+ j = (await res.json());
95
+ }
96
+ catch (e) {
97
+ throw redactedError(new Error(`telegram ${res.status}: not JSON (${e.message})`), this.tokenValue);
98
+ }
99
+ if (!j.ok)
100
+ throw new TelegramApiError(j.error_code ?? res.status, j.description ?? 'error', j.parameters?.retry_after ?? null, this.tokenValue);
101
+ return j.result;
102
+ }
103
+ /** The bot's own account (setup prints the username only). */
104
+ async getMe() {
105
+ const me = await this.call('getMe');
106
+ return { id: String(me.id), username: me.username ?? '' };
107
+ }
108
+ /** 'clear' only when Telegram answered and no webhook is set; pairing refuses anything else. */
109
+ async checkWebhook() {
110
+ this.lastWebhookCheck = this.now();
111
+ try {
112
+ const w = await this.call('getWebhookInfo');
113
+ if (w.url) {
114
+ this.setConflict('a webhook is set on this bot: someone else may hold its token');
115
+ return 'webhook';
116
+ }
117
+ return 'clear';
118
+ }
119
+ catch (e) {
120
+ this.noteError(e);
121
+ return 'unknown';
122
+ }
123
+ }
124
+ setConflict(why) {
125
+ if (this.h.conflict)
126
+ return;
127
+ this.h.conflict = why;
128
+ this.log(`conflict: ${why}`);
129
+ }
130
+ noteError(e) {
131
+ this.h.errors++;
132
+ this.h.lastError = redact(e instanceof Error ? e.message : String(e), this.tokenValue).slice(0, 200);
133
+ this.log(`error: ${this.h.lastError}`);
134
+ }
135
+ /** Tell Telegram every update up to the last one seen is handled (one short call, no wait). */
136
+ async confirmOffset() {
137
+ if (this.offset > 0)
138
+ await this.call('getUpdates', { offset: this.offset, timeout: 0, limit: 1, allowed_updates: ['message', 'callback_query'] });
139
+ }
140
+ async start(signal) {
141
+ this.startedAt = this.now();
142
+ await this.checkWebhook();
143
+ let backoff = 1000;
144
+ while (!signal.aborted) {
145
+ if (this.now() - this.lastWebhookCheck >= WEBHOOK_CHECK_MS)
146
+ await this.checkWebhook();
147
+ try {
148
+ const timeout = this.o.pollTimeoutSec ?? POLL_TIMEOUT_SEC;
149
+ const ups = await this.call('getUpdates', { offset: this.offset, timeout, allowed_updates: ['message', 'callback_query'] }, signal, (timeout + 15) * 1000);
150
+ this.h.lastPollAt = this.now();
151
+ backoff = 1000;
152
+ for (const u of ups)
153
+ this.handle(u);
154
+ }
155
+ catch (e) {
156
+ if (signal.aborted)
157
+ break;
158
+ if (e instanceof TelegramApiError && e.code === 409)
159
+ this.setConflict('another getUpdates poll or a webhook holds this bot (409)');
160
+ if (e instanceof TelegramApiError && e.code === 429) {
161
+ this.h.rateLimited++;
162
+ await this.sleep(Math.max(1, e.retryAfter ?? 5) * 1000, signal);
163
+ continue;
164
+ }
165
+ this.noteError(e);
166
+ await this.sleep(backoff, signal);
167
+ backoff = Math.min(30_000, backoff * 2);
168
+ }
169
+ }
170
+ }
171
+ /** One update (exported for the transport tests). Duplicates and updates from bots are dropped. */
172
+ handle(u) {
173
+ if (typeof u.update_id !== 'number')
174
+ return;
175
+ this.offset = Math.max(this.offset, u.update_id + 1);
176
+ if (this.seen.has(u.update_id))
177
+ return;
178
+ this.seen.add(u.update_id);
179
+ if (this.seen.size > 2000) {
180
+ const first = this.seen.values().next().value;
181
+ if (first !== undefined)
182
+ this.seen.delete(first);
183
+ }
184
+ const updateId = String(u.update_id);
185
+ if (u.callback_query) {
186
+ const q = u.callback_query;
187
+ if (!q.message || q.from.is_bot || typeof q.data !== 'string')
188
+ return;
189
+ const a = { transport: this.transport, updateId, actionId: q.id, chatId: String(q.message.chat.id), chatKind: kindOf(q.message.chat), senderId: String(q.from.id), ref: { chatId: String(q.message.chat.id), messageId: String(q.message.message_id) }, data: q.data.slice(0, 64), t: (q.message.date ?? 0) * 1000 };
190
+ for (const h of this.actH)
191
+ h(a);
192
+ return;
193
+ }
194
+ const m = u.message;
195
+ if (!m || !m.from || m.from.is_bot || typeof m.text !== 'string')
196
+ return;
197
+ // A command typed while nothing was polling (minutes or days ago) is not acted on when the bot comes back.
198
+ if (this.startedAt && m.date * 1000 < this.startedAt - STALE_COMMAND_MS) {
199
+ this.h.staleSkipped = (this.h.staleSkipped ?? 0) + 1;
200
+ return;
201
+ }
202
+ const base = { transport: this.transport, updateId, chatId: String(m.chat.id), chatKind: kindOf(m.chat), senderId: String(m.from.id), t: m.date * 1000 };
203
+ const p = relayed(m) ? null : parseCommand(m.text);
204
+ if (p) {
205
+ const name = [m.from.first_name, m.from.last_name].filter((x) => typeof x === 'string' && x).join(' ');
206
+ const sender = { ...(typeof m.from.username === 'string' && m.from.username ? { username: m.from.username } : {}), ...(name ? { name } : {}) };
207
+ for (const h of this.cmdH)
208
+ h({ ...base, sender, command: p.command, args: p.args });
209
+ return;
210
+ }
211
+ const x = { ...base, length: m.text.length, sha256: createHash('sha256').update(m.text).digest('hex') };
212
+ for (const h of this.txtH)
213
+ h(x);
214
+ }
215
+ /** 1 message/s per chat with a burst of 3 (a token bucket): wait for a slot. */
216
+ async slot(chatId) {
217
+ for (;;) {
218
+ const now = this.now();
219
+ const b = this.buckets.get(chatId) ?? { tokens: 3, at: now };
220
+ b.tokens = Math.min(3, b.tokens + Math.max(0, now - b.at) / 1000);
221
+ b.at = now;
222
+ this.buckets.set(chatId, b);
223
+ if (b.tokens >= 1) {
224
+ b.tokens -= 1;
225
+ return;
226
+ }
227
+ await this.sleep(Math.ceil((1 - b.tokens) * 1000));
228
+ }
229
+ }
230
+ async withRetry(fn) {
231
+ for (let i = 0;; i++) {
232
+ try {
233
+ return await fn();
234
+ }
235
+ catch (e) {
236
+ if (e instanceof TelegramApiError && e.code === 429 && i < 2) {
237
+ this.h.rateLimited++;
238
+ await this.sleep(Math.max(1, e.retryAfter ?? 3) * 1000);
239
+ continue;
240
+ }
241
+ throw e instanceof TelegramApiError ? e : redactedError(e, this.tokenValue);
242
+ }
243
+ }
244
+ }
245
+ markup(msg) {
246
+ if (msg.kind !== 'proposal')
247
+ return undefined;
248
+ return { inline_keyboard: [msg.buttons.map((b) => ({ text: b.label.slice(0, 32), callback_data: b.data }))] };
249
+ }
250
+ async send(to, msg) {
251
+ await this.slot(to.chatId);
252
+ if (msg.kind === 'card') {
253
+ return this.withRetry(async () => {
254
+ const fd = new FormData();
255
+ fd.set('chat_id', to.chatId);
256
+ fd.set('caption', clip(plain(msg.badge, [msg.caption]), 1000));
257
+ fd.set('photo', new Blob([msg.png], { type: 'image/png' }), 'pnl-card.png');
258
+ const m = await this.call('sendPhoto', fd, undefined, 30_000);
259
+ return { chatId: to.chatId, messageId: String(m.message_id) };
260
+ });
261
+ }
262
+ return this.withRetry(async () => {
263
+ const m = await this.call('sendMessage', {
264
+ chat_id: to.chatId, text: clip(plain(msg.badge, msg.lines)), link_preview_options: { is_disabled: true },
265
+ ...(this.markup(msg) ? { reply_markup: this.markup(msg) } : {}),
266
+ });
267
+ return { chatId: to.chatId, messageId: String(m.message_id) };
268
+ });
269
+ }
270
+ async edit(ref, msg) {
271
+ if (msg.kind === 'card')
272
+ return;
273
+ await this.withRetry(() => this.call('editMessageText', {
274
+ chat_id: ref.chatId, message_id: Number(ref.messageId), text: clip(plain(msg.badge, msg.lines)), link_preview_options: { is_disabled: true },
275
+ reply_markup: this.markup(msg) ?? { inline_keyboard: [] },
276
+ }));
277
+ }
278
+ onCommand(h) { this.cmdH.push(h); }
279
+ onAction(h) { this.actH.push(h); }
280
+ onText(h) { this.txtH.push(h); }
281
+ async ackAction(a, toast) {
282
+ try {
283
+ await this.call('answerCallbackQuery', { callback_query_id: a.actionId, text: toast.slice(0, 190) });
284
+ }
285
+ catch (e) {
286
+ this.noteError(e);
287
+ }
288
+ }
289
+ health() { return { ...this.h }; }
290
+ }