dexterm-cli 0.1.6 → 0.2.0

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 (395) hide show
  1. package/dist/packages/agents/src/schema.js +22 -1
  2. package/dist/packages/agents/src/spectator.js +3 -1
  3. package/dist/packages/agents/src/story.js +11 -2
  4. package/dist/packages/agents/src/tiers.js +36 -2
  5. package/dist/packages/agents/src/tools.js +35 -2
  6. package/dist/packages/autotrade/assets/exit-profile.schema.json +492 -0
  7. package/dist/packages/autotrade/assets/limits.schema.json +569 -0
  8. package/dist/packages/autotrade/assets/profiles/moonbag.json +46 -0
  9. package/dist/packages/autotrade/assets/profiles/runner.json +88 -0
  10. package/dist/packages/autotrade/assets/profiles/scalp.json +88 -0
  11. package/dist/packages/autotrade/assets/profiles/swing.json +88 -0
  12. package/dist/packages/autotrade/assets/strategies/moonbag.json +101 -0
  13. package/dist/packages/autotrade/assets/strategies/runner.json +101 -0
  14. package/dist/packages/autotrade/assets/strategies/sample-dip-ladder.json +188 -0
  15. package/dist/packages/autotrade/assets/strategies/scalp.json +98 -0
  16. package/dist/packages/autotrade/assets/strategies/swing.json +97 -0
  17. package/dist/packages/autotrade/assets/strategies/yolo.json +157 -0
  18. package/dist/packages/autotrade/assets/tapes/yolo-rehearsal.json +1 -0
  19. package/dist/packages/autotrade/examples/executor/README.md +70 -0
  20. package/dist/packages/autotrade/examples/executor/jsonl-tail-executor.mjs +57 -0
  21. package/dist/packages/autotrade/examples/executor/kit.mjs +144 -0
  22. package/dist/packages/autotrade/examples/executor/signal-cmd-executor.mjs +25 -0
  23. package/dist/packages/autotrade/examples/executor/telegram-bot-recipe.md +47 -0
  24. package/dist/packages/autotrade/examples/executor/webhook-executor.mjs +47 -0
  25. package/dist/packages/autotrade/src/account.js +74 -0
  26. package/dist/packages/autotrade/src/backtest.js +358 -0
  27. package/dist/packages/autotrade/src/basis-text.js +32 -0
  28. package/dist/packages/autotrade/src/chat/approvals.js +484 -0
  29. package/dist/packages/autotrade/src/chat/audit.js +140 -0
  30. package/dist/packages/autotrade/src/chat/config.js +95 -0
  31. package/dist/packages/autotrade/src/chat/connector.js +24 -0
  32. package/dist/packages/autotrade/src/chat/control.js +104 -0
  33. package/dist/packages/autotrade/src/chat/fake.js +100 -0
  34. package/dist/packages/autotrade/src/chat/format.js +71 -0
  35. package/dist/packages/autotrade/src/chat/hub.js +187 -0
  36. package/dist/packages/autotrade/src/chat/index.js +18 -0
  37. package/dist/packages/autotrade/src/chat/notify.js +302 -0
  38. package/dist/packages/autotrade/src/chat/pairing.js +46 -0
  39. package/dist/packages/autotrade/src/chat/redact.js +48 -0
  40. package/dist/packages/autotrade/src/chat/secrets.js +106 -0
  41. package/dist/packages/autotrade/src/chat/service.js +251 -0
  42. package/dist/packages/autotrade/src/chat/telegram.js +290 -0
  43. package/dist/packages/autotrade/src/desk-executor.js +188 -0
  44. package/dist/packages/autotrade/src/engine.js +401 -0
  45. package/dist/packages/autotrade/src/execution-port.js +97 -0
  46. package/dist/packages/autotrade/src/executor-health.js +217 -0
  47. package/dist/packages/autotrade/src/fleet-config.js +169 -0
  48. package/dist/packages/autotrade/src/fleet-control.js +276 -0
  49. package/dist/packages/autotrade/src/fleet.js +642 -0
  50. package/dist/packages/autotrade/src/forward.js +355 -0
  51. package/dist/packages/autotrade/src/index.js +72 -0
  52. package/dist/packages/autotrade/src/journey.js +477 -0
  53. package/dist/packages/autotrade/src/ledger.js +108 -0
  54. package/dist/packages/autotrade/src/limits.js +682 -0
  55. package/dist/packages/autotrade/src/live-source.js +182 -0
  56. package/dist/packages/autotrade/src/monitor-daemon.js +715 -0
  57. package/dist/packages/autotrade/src/monitor.js +190 -0
  58. package/dist/packages/autotrade/src/ollama-sessions.js +137 -0
  59. package/dist/packages/autotrade/src/outbox.js +394 -0
  60. package/dist/packages/autotrade/src/paper-accounts.js +72 -0
  61. package/dist/packages/autotrade/src/paper-limits.js +87 -0
  62. package/dist/packages/autotrade/src/real-mode.js +688 -0
  63. package/dist/packages/autotrade/src/receipts.js +330 -0
  64. package/dist/packages/autotrade/src/replay.js +198 -0
  65. package/dist/packages/autotrade/src/report.js +168 -0
  66. package/dist/packages/autotrade/src/runner-config.js +99 -0
  67. package/dist/packages/autotrade/src/runner.js +923 -0
  68. package/dist/packages/autotrade/src/safety.js +243 -0
  69. package/dist/packages/autotrade/src/service-guard.js +125 -0
  70. package/dist/packages/autotrade/src/service-install.js +213 -0
  71. package/dist/packages/autotrade/src/service-recover.js +232 -0
  72. package/dist/packages/autotrade/src/service.js +289 -0
  73. package/dist/packages/autotrade/src/session.js +269 -0
  74. package/dist/packages/autotrade/src/sessions.js +503 -0
  75. package/dist/packages/autotrade/src/signal-channels.js +312 -0
  76. package/dist/packages/autotrade/src/signal.js +196 -0
  77. package/dist/packages/autotrade/src/store.js +250 -0
  78. package/dist/packages/autotrade/src/strategy.js +258 -0
  79. package/dist/packages/autotrade/src/trade-ledger.js +241 -0
  80. package/dist/packages/autotrade/src/yolo.js +140 -0
  81. package/dist/packages/cli/assets/claude/analyst-system.md +4 -4
  82. package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +1 -1
  83. package/dist/packages/cli/src/ai-cmd.js +412 -0
  84. package/dist/packages/cli/src/analyst/backend.js +83 -15
  85. package/dist/packages/cli/src/analyst/codex.js +15 -9
  86. package/dist/packages/cli/src/analyst/ollama.js +263 -0
  87. package/dist/packages/cli/src/analyst/session.js +5 -2
  88. package/dist/packages/cli/src/app-help-gen.js +56 -8
  89. package/dist/packages/cli/src/autotrade-cmd/audit.js +74 -0
  90. package/dist/packages/cli/src/autotrade-cmd/backtest.js +216 -0
  91. package/dist/packages/cli/src/autotrade-cmd/executor.js +91 -0
  92. package/dist/packages/cli/src/autotrade-cmd/flatten.js +64 -0
  93. package/dist/packages/cli/src/autotrade-cmd/fleet.js +401 -0
  94. package/dist/packages/cli/src/autotrade-cmd/forward.js +283 -0
  95. package/dist/packages/cli/src/autotrade-cmd/journey.js +208 -0
  96. package/dist/packages/cli/src/autotrade-cmd/limits.js +393 -0
  97. package/dist/packages/cli/src/autotrade-cmd/receipt.js +63 -0
  98. package/dist/packages/cli/src/autotrade-cmd/report.js +79 -0
  99. package/dist/packages/cli/src/autotrade-cmd/requests.js +79 -0
  100. package/dist/packages/cli/src/autotrade-cmd/run.js +657 -0
  101. package/dist/packages/cli/src/autotrade-cmd/service.js +873 -0
  102. package/dist/packages/cli/src/autotrade-cmd/signal.js +824 -0
  103. package/dist/packages/cli/src/autotrade-cmd/stop.js +76 -0
  104. package/dist/packages/cli/src/autotrade-cmd/strategies.js +198 -0
  105. package/dist/packages/cli/src/autotrade-cmd/strategy-control.js +174 -0
  106. package/dist/packages/cli/src/autotrade-cmd/telegram-host.js +101 -0
  107. package/dist/packages/cli/src/autotrade-cmd.js +214 -0
  108. package/dist/packages/cli/src/display.js +156 -17
  109. package/dist/packages/cli/src/doctor.js +62 -3
  110. package/dist/packages/cli/src/export.js +10 -2
  111. package/dist/packages/cli/src/index.js +1 -1
  112. package/dist/packages/cli/src/ledger-cmd.js +290 -0
  113. package/dist/packages/cli/src/live/chart-fills.js +46 -0
  114. package/dist/packages/cli/src/live/index.js +23 -89
  115. package/dist/packages/cli/src/live/screens/token-live.js +9 -6
  116. package/dist/packages/cli/src/live/trade.js +12 -457
  117. package/dist/packages/cli/src/live/watchlist.js +1 -1
  118. package/dist/packages/cli/src/main.js +147 -190
  119. package/dist/packages/cli/src/mcp-cmd.js +7 -1
  120. package/dist/packages/cli/src/plan-cmd.js +1 -1
  121. package/dist/packages/cli/src/pnl-app.js +31 -21
  122. package/dist/packages/cli/src/pnl-card.js +162 -21
  123. package/dist/packages/cli/src/popup.js +22 -6
  124. package/dist/packages/cli/src/record-cmd.js +141 -20
  125. package/dist/packages/cli/src/screens/agent-hud.js +34 -2
  126. package/dist/packages/cli/src/screens/analyst-session.js +7 -4
  127. package/dist/packages/cli/src/screens/analyst.js +3 -2
  128. package/dist/packages/cli/src/screens/autotrade-journey.js +134 -0
  129. package/dist/packages/cli/src/screens/autotrade-keys.js +182 -0
  130. package/dist/packages/cli/src/screens/autotrade-proposals.js +123 -0
  131. package/dist/packages/cli/src/screens/autotrade-screen.js +776 -0
  132. package/dist/packages/cli/src/screens/autotrade-ui.js +650 -0
  133. package/dist/packages/cli/src/screens/autotrade-worker.js +91 -0
  134. package/dist/packages/cli/src/screens/brain.js +37 -11
  135. package/dist/packages/cli/src/screens/overlay.js +5 -2
  136. package/dist/packages/cli/src/screens/plans.js +5 -17
  137. package/dist/packages/cli/src/screens/pnl-chip.js +9 -8
  138. package/dist/packages/cli/src/screens/positions.js +6 -28
  139. package/dist/packages/cli/src/screens/ticket.js +38 -205
  140. package/dist/packages/cli/src/screens/token.js +3 -3
  141. package/dist/packages/cli/src/state.js +25 -19
  142. package/dist/packages/cli/src/telegram-cmd.js +454 -0
  143. package/dist/packages/cli/src/trade.js +3 -128
  144. package/dist/packages/cli/src/version.js +1 -1
  145. package/dist/packages/cli/src/wallet-cmd.js +39 -340
  146. package/dist/packages/cli/src/wallet-command.js +8 -11
  147. package/dist/packages/cli/src/widgets.js +0 -12
  148. package/dist/packages/custom/src/config.js +20 -2
  149. package/dist/packages/custom/src/schema.js +35 -10
  150. package/dist/packages/custom/src/workspace.js +3 -3
  151. package/dist/packages/data/src/basis.js +191 -0
  152. package/dist/packages/data/src/detail.js +53 -2
  153. package/dist/packages/data/src/index.js +0 -14
  154. package/dist/packages/data/src/store.js +14 -5
  155. package/dist/packages/data/src/trade/agent/paper-desk.js +228 -16
  156. package/dist/packages/data/src/trade/agent/rendezvous.js +3 -2
  157. package/dist/packages/data/src/trade/bridge.js +4 -102
  158. package/dist/packages/data/src/trade/ledger/export.js +101 -0
  159. package/dist/packages/data/src/trade/ledger/file.js +135 -0
  160. package/dist/packages/data/src/trade/ledger/fold.js +191 -0
  161. package/dist/packages/data/src/trade/ledger/index.js +9 -0
  162. package/dist/packages/data/src/trade/ledger/schema.js +83 -0
  163. package/dist/packages/data/src/trade/ledger/sources.js +307 -0
  164. package/dist/packages/data/src/trade/ledger/stats.js +123 -0
  165. package/dist/packages/data/src/trade/ledger/tags.js +116 -0
  166. package/dist/packages/data/src/trade/ledger/view.js +51 -0
  167. package/dist/packages/data/src/trade/plans/index.js +0 -1
  168. package/dist/packages/data/src/trade/plans/service.js +3 -27
  169. package/dist/packages/mcp/assets/kb/app-help/autotrade-chat.md +48 -0
  170. package/dist/packages/mcp/assets/kb/app-help/autotrade-executor-kit.md +44 -0
  171. package/dist/packages/mcp/assets/kb/app-help/autotrade-journey.md +46 -0
  172. package/dist/packages/mcp/assets/kb/app-help/autotrade-limits.md +48 -0
  173. package/dist/packages/mcp/assets/kb/app-help/autotrade-parallel.md +42 -0
  174. package/dist/packages/mcp/assets/kb/app-help/autotrade-real-signal.md +45 -0
  175. package/dist/packages/mcp/assets/kb/app-help/autotrade-runner.md +35 -0
  176. package/dist/packages/mcp/assets/kb/app-help/autotrade-stop.md +37 -0
  177. package/dist/packages/mcp/assets/kb/app-help/autotrade-strategies.md +35 -0
  178. package/dist/packages/mcp/assets/kb/app-help/autotrade-telegram.md +40 -0
  179. package/dist/packages/mcp/assets/kb/app-help/chart-trade-markers.md +42 -0
  180. package/dist/packages/mcp/assets/kb/app-help/link-a-wallet.md +4 -10
  181. package/dist/packages/mcp/assets/kb/app-help/live-trading.md +38 -0
  182. package/dist/packages/mcp/assets/kb/app-help/live-yolo.md +34 -0
  183. package/dist/packages/mcp/assets/kb/app-help/order-plans.md +1 -1
  184. package/dist/packages/mcp/assets/kb/app-help/price-or-marketcap.md +5 -2
  185. package/dist/packages/mcp/assets/kb/app-help/screen-analyst.md +3 -0
  186. package/dist/packages/mcp/assets/kb/app-help/screen-autotrade.md +229 -0
  187. package/dist/packages/mcp/assets/kb/app-help/screen-kol.md +3 -0
  188. package/dist/packages/mcp/assets/kb/app-help/screen-newpairs.md +3 -0
  189. package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +3 -0
  190. package/dist/packages/mcp/assets/kb/app-help/screen-screener.md +3 -0
  191. package/dist/packages/mcp/assets/kb/app-help/screen-token.md +3 -0
  192. package/dist/packages/mcp/assets/kb/app-help/screen-trending.md +3 -0
  193. package/dist/packages/mcp/assets/kb/app-help/screen-watchlist.md +3 -0
  194. package/dist/packages/mcp/assets/kb/app-help/standing-mandates.md +1 -1
  195. package/dist/packages/mcp/assets/kb/app-help/trade-ledger.md +54 -0
  196. package/dist/packages/mcp/assets/kb/app-help/use-local-model.md +38 -0
  197. package/dist/packages/mcp/assets/kb/docs/docs-agents-agent-paper-trading.md +4 -1
  198. package/dist/packages/mcp/assets/kb/docs/docs-agents-reading-your-trade-ledger.md +31 -0
  199. package/dist/packages/mcp/assets/kb/docs/docs-agents-untrusted-by-default.md +1 -1
  200. package/dist/packages/mcp/assets/kb/docs/docs-analyst-if-claude-is-missing.md +1 -1
  201. package/dist/packages/mcp/assets/kb/docs/docs-analyst-use-a-local-model-instead.md +27 -0
  202. package/dist/packages/mcp/assets/kb/docs/docs-analyst-what-it-sees.md +2 -0
  203. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-backtests-and-their-limits.md +56 -0
  204. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-change-a-strategy-by-chat.md +51 -0
  205. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-checking-the-record-of-a-run.md +28 -0
  206. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-comparing-them.md +41 -0
  207. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-exits-and-wakes.md +76 -0
  208. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-how-a-stop-reaches-the-runner.md +32 -0
  209. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-on-the-q-screen.md +38 -0
  210. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-one-strategy-or-all-of-them.md +35 -0
  211. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-paper-accounts.md +50 -0
  212. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-presets.md +37 -0
  213. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-running-it-all-day.md +31 -0
  214. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-sessions-and-their-limits.md +31 -0
  215. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-several-strategies-at-once.md +33 -0
  216. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-stopping-it.md +37 -0
  217. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-strategies-and-presets.md +34 -0
  218. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-always-on-service.md +57 -0
  219. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-autotrade-json-file.md +48 -0
  220. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-heartbeat.md +27 -0
  221. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-run-file-and-the-heartbeat.md +34 -0
  222. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-runner-and-candidate-sessions.md +45 -0
  223. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-each-session-shows-first.md +31 -0
  224. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-costs-claude-usage-per-session.md +58 -0
  225. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-is-paper-self-directed.md +50 -0
  226. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-will-not-do.md +31 -0
  227. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-which-account-day-caps-and-the-usage-guard.md +44 -0
  228. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-your-strategies.md +37 -0
  229. package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +22 -0
  230. package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
  231. package/dist/packages/mcp/assets/kb/docs/docs-beginners.md +1 -1
  232. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-1-ask-for-a-strategy.md +27 -0
  233. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-2-backtest-it-held-out-split.md +31 -0
  234. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-3-forward-test-it-on-live-data.md +44 -0
  235. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-4-promote-it-to-always-on-paper.md +36 -0
  236. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-5-real-only-if-you-choose.md +36 -0
  237. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-ladder.md +39 -0
  238. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-report.md +31 -0
  239. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-what-the-analyst-knows.md +30 -0
  240. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat.md +24 -0
  241. package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-markers.md +30 -0
  242. package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-y-axis.md +1 -1
  243. package/dist/packages/mcp/assets/kb/docs/docs-configuration-environment-variables.md +4 -1
  244. package/dist/packages/mcp/assets/kb/docs/docs-configuration-files-on-disk.md +1 -0
  245. package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -1
  246. package/dist/packages/mcp/assets/kb/docs/docs-configuration-price-display-mode.md +8 -6
  247. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-2-the-one-key-mirror-ticket-a-proposal.md +1 -1
  248. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-3-capped-auto-mirror-default-off-less.md +1 -1
  249. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-what-is-proven-what-is-not.md +1 -1
  250. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
  251. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +36 -0
  252. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-sources.md +1 -0
  253. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-first-run.md +1 -1
  254. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
  255. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +1 -1
  256. package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +1 -1
  257. package/dist/packages/mcp/assets/kb/docs/docs-going-real-executor-health.md +38 -0
  258. package/dist/packages/mcp/assets/kb/docs/docs-going-real-paper-vs-real.md +33 -0
  259. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-digest.md +36 -0
  260. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-executor-kit.md +48 -0
  261. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-whole-path.md +37 -0
  262. package/dist/packages/mcp/assets/kb/docs/docs-going-real-what-dexterm-does-and-never-does.md +44 -0
  263. package/dist/packages/mcp/assets/kb/docs/docs-going-real-your-limits.md +77 -0
  264. package/dist/packages/mcp/assets/kb/docs/docs-going-real.md +24 -0
  265. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-changed-in-0-1-7.md +26 -0
  266. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-real-mode-your-strategy-s-trades-to-your.md +31 -0
  267. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-stopping.md +28 -0
  268. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-the-short-version.md +34 -0
  269. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-using-a-trading-bot-with-dexterm.md +31 -0
  270. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does-not-do.md +30 -0
  271. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does.md +29 -0
  272. package/dist/packages/mcp/assets/kb/docs/docs-live-trading.md +23 -0
  273. package/dist/packages/mcp/assets/kb/docs/docs-ollama-another-machine.md +28 -0
  274. package/dist/packages/mcp/assets/kb/docs/docs-ollama-autotrade-on-a-local-model.md +29 -0
  275. package/dist/packages/mcp/assets/kb/docs/docs-ollama-check-your-model-dexterm-ai-probe.md +38 -0
  276. package/dist/packages/mcp/assets/kb/docs/docs-ollama-how-we-checked.md +27 -0
  277. package/dist/packages/mcp/assets/kb/docs/docs-ollama-models-without-tool-calling.md +32 -0
  278. package/dist/packages/mcp/assets/kb/docs/docs-ollama-pick-the-engine-globally-or-per-surface.md +43 -0
  279. package/dist/packages/mcp/assets/kb/docs/docs-ollama-restriction-profiles.md +35 -0
  280. package/dist/packages/mcp/assets/kb/docs/docs-ollama-set-it-up-in-two-minutes.md +39 -0
  281. package/dist/packages/mcp/assets/kb/docs/docs-ollama-troubleshooting.md +35 -0
  282. package/dist/packages/mcp/assets/kb/docs/docs-ollama-what-runs-on-a-local-model.md +39 -0
  283. package/dist/packages/mcp/assets/kb/docs/docs-ollama-what-stays-outside-the-model.md +36 -0
  284. package/dist/packages/mcp/assets/kb/docs/docs-ollama-which-model.md +38 -0
  285. package/dist/packages/mcp/assets/kb/docs/docs-ollama-your-own-agent-on-the-local-model.md +28 -0
  286. package/dist/packages/mcp/assets/kb/docs/docs-ollama.md +28 -0
  287. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-letting-an-agent-paper-trade-experimental.md +3 -4
  288. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-link-a-wallet-read-only.md +30 -0
  289. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-order-plans-and-standing-mandates.md +3 -4
  290. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-panic.md +4 -7
  291. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-paper-only-mandates.md +6 -7
  292. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-pnl-cards.md +7 -7
  293. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-safety-model.md +34 -0
  294. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-ticket.md +3 -4
  295. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-trade-ledger.md +42 -0
  296. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-wallet.md +4 -5
  297. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-trade-moments.md +4 -5
  298. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +4 -5
  299. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-buy-and-sell-markers.md +37 -0
  300. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-what-it-shows.md +2 -1
  301. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-receipts-what-your-executor-did.md +42 -0
  302. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-safety.md +34 -0
  303. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-set-it-up.md +47 -0
  304. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-channels.md +42 -0
  305. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-signal.md +48 -0
  306. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-is.md +35 -0
  307. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-will-not-do.md +34 -0
  308. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-where-you-see-it.md +34 -0
  309. package/dist/packages/mcp/assets/kb/docs/docs-real-mode.md +27 -0
  310. package/dist/packages/mcp/assets/kb/docs/docs-screens-autotrade-paper.md +26 -0
  311. package/dist/packages/mcp/assets/kb/docs/docs-screens-order-plans.md +1 -1
  312. package/dist/packages/mcp/assets/kb/docs/docs-screens-token-screen.md +2 -0
  313. package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-brain-view-and-the-pnl-chip.md +1 -1
  314. package/dist/packages/mcp/assets/kb/docs/docs-telegram-approve-one-tap-entries.md +50 -0
  315. package/dist/packages/mcp/assets/kb/docs/docs-telegram-control-the-commands.md +43 -0
  316. package/dist/packages/mcp/assets/kb/docs/docs-telegram-if-something-goes-wrong.md +30 -0
  317. package/dist/packages/mcp/assets/kb/docs/docs-telegram-notify-what-reaches-your-phone.md +41 -0
  318. package/dist/packages/mcp/assets/kb/docs/docs-telegram-set-it-up.md +47 -0
  319. package/dist/packages/mcp/assets/kb/docs/docs-telegram-the-record.md +26 -0
  320. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +33 -0
  321. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-will-not-do.md +31 -0
  322. package/dist/packages/mcp/assets/kb/docs/docs-telegram.md +23 -0
  323. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-common-questions.md +1 -1
  324. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-doctor-lines.md +4 -0
  325. package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +1 -1
  326. package/dist/packages/mcp/assets/kb/docs/docs-voice.md +1 -1
  327. package/dist/packages/mcp/assets/kb/docs/docs-yolo-1-replay-it-on-your-own-history.md +42 -0
  328. package/dist/packages/mcp/assets/kb/docs/docs-yolo-2-run-it-on-paper.md +34 -0
  329. package/dist/packages/mcp/assets/kb/docs/docs-yolo-3-watch-it-and-stop-it.md +32 -0
  330. package/dist/packages/mcp/assets/kb/docs/docs-yolo-the-rules.md +37 -0
  331. package/dist/packages/mcp/assets/kb/docs/docs-yolo-what-it-is.md +34 -0
  332. package/dist/packages/mcp/assets/kb/docs/docs-yolo-what-it-will-not-do.md +31 -0
  333. package/dist/packages/mcp/assets/kb/docs/docs-yolo.md +26 -0
  334. package/dist/packages/mcp/assets/kb/entries/autotrade.md +40 -0
  335. package/dist/packages/mcp/src/app-help.js +1 -1
  336. package/dist/packages/mcp/src/autotrade-tools.js +1107 -0
  337. package/dist/packages/mcp/src/backtest-requests.js +71 -0
  338. package/dist/packages/mcp/src/engine/config.js +159 -0
  339. package/dist/packages/mcp/src/engine/index.js +10 -0
  340. package/dist/packages/mcp/src/engine/loop.js +101 -0
  341. package/dist/packages/mcp/src/engine/mcp-client.js +123 -0
  342. package/dist/packages/mcp/src/engine/ollama.js +258 -0
  343. package/dist/packages/mcp/src/engine/probe.js +110 -0
  344. package/dist/packages/mcp/src/engine/reason.js +35 -0
  345. package/dist/packages/mcp/src/engine/restrictions.js +111 -0
  346. package/dist/packages/mcp/src/index.js +7 -1
  347. package/dist/packages/mcp/src/ledger-tools.js +494 -0
  348. package/dist/packages/mcp/src/paper-agent.js +2 -0
  349. package/dist/packages/mcp/src/server.js +138 -19
  350. package/dist/packages/packs/src/backtest.js +3 -1
  351. package/dist/packages/popup/src/pages/chart-lib.js +134 -21
  352. package/dist/packages/popup/src/pages/chart.css +9 -0
  353. package/dist/packages/popup/src/pages/chart.html +3 -1
  354. package/dist/packages/popup/src/pages/chart.js +104 -7
  355. package/dist/packages/popup/src/protocol.js +2 -0
  356. package/dist/packages/popup/src/server.js +4 -0
  357. package/dist/packages/recorder/src/index.js +2 -0
  358. package/dist/packages/recorder/src/recorder.js +63 -6
  359. package/dist/packages/recorder/src/retention.js +100 -0
  360. package/dist/packages/recorder/src/schema.js +34 -1
  361. package/dist/packages/recorder/src/ticks.js +237 -0
  362. package/dist/packages/tui/src/index.js +1 -0
  363. package/dist/packages/tui/src/keymap.js +28 -1
  364. package/dist/packages/tui/src/markers.js +219 -0
  365. package/dist/packages/tui/src/widgets.js +149 -2
  366. package/package.json +2 -1
  367. package/dist/packages/data/src/trade/caps.js +0 -323
  368. package/dist/packages/data/src/trade/executor.js +0 -933
  369. package/dist/packages/data/src/trade/plans/executor-live.js +0 -220
  370. package/dist/packages/data/src/trade/rpc-exec.js +0 -229
  371. package/dist/packages/data/src/trade/signer/client.js +0 -197
  372. package/dist/packages/data/src/trade/signer/fast-wallet.js +0 -154
  373. package/dist/packages/data/src/trade/signer/host.js +0 -97
  374. package/dist/packages/data/src/trade/signer/main.js +0 -121
  375. package/dist/packages/data/src/trade/signer/probe-builder.js +0 -85
  376. package/dist/packages/data/src/trade/signer/protocol.js +0 -92
  377. package/dist/packages/data/src/trade/signer/service.js +0 -400
  378. package/dist/packages/data/src/trade/signer/sol-usd-feed.js +0 -98
  379. package/dist/packages/data/src/trade/sol-usd.js +0 -86
  380. package/dist/packages/data/src/trade/swap-build.js +0 -108
  381. package/dist/packages/data/src/trade/token-delta.js +0 -201
  382. package/dist/packages/data/vendor/solana-web3/LICENSE +0 -20
  383. package/dist/packages/data/vendor/solana-web3/NOTICE.md +0 -13
  384. package/dist/packages/data/vendor/solana-web3/index.iife.min.js +0 -20
  385. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-a-live-ticket.md +0 -42
  386. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-before-you-press-size-vs-caps.md +0 -34
  387. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-devnet-signer-mainnet-token.md +0 -35
  388. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-jupiter-rate-limits.md +0 -33
  389. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-link-a-wallet.md +0 -32
  390. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-live-trading-devnet-first.md +0 -30
  391. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-mainnet.md +0 -31
  392. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-live-fire-path-devnet-the-fast-wallet.md +0 -41
  393. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-signer-process.md +0 -34
  394. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-what-the-live-path-keeps.md +0 -37
  395. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-your-first-live-trade.md +0 -41
@@ -0,0 +1,95 @@
1
+ // dexterm autotrade chat — connectors.json (task-1527, AUTOTRADE-SPEC §8.3). It never holds a secret.
2
+ //
3
+ // $DEXTERM_HOME/connectors.json (0600):
4
+ // { schema, telegram: { enabled, bot: {id, username}, secretStore, tiers, allow: [{senderId, chatId, tiers,
5
+ // pairedAt}], notify: {…per event…}, dailySummaryAt, quietHours, approvals, approvalExpirySec, repriceTolPct,
6
+ // showAbsoluteBook }, signals: {…} }
7
+ // task-1645: `signals` is the real-mode trade-fire channel setup (signal-channels.ts parses it); it lives here so the
8
+ // Telegram copy and the executor channels are configured in one place. This module only carries it through.
9
+ // Only local commands write it (`dexterm telegram setup|pair|revoke|approvals|notify`). From chat, exactly one
10
+ // field can change: `approvals` from "off" to "on" (safer); everything else, and "off", is local only.
11
+ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+ import { writeJsonAtomic } from "../ledger.js";
14
+ export const CONNECTORS_SCHEMA = 'dexterm-connectors/1';
15
+ export const TIERS_ALL = ['notify', 'control', 'approve'];
16
+ export const DEFAULT_NOTIFY = { candidates: 'digest', entries: true, closes: true, halts: true, spend: true, daily: true };
17
+ export const defaultTelegram = () => ({
18
+ enabled: false, bot: null, secretStore: null, tiers: [...TIERS_ALL], allow: [], notify: { ...DEFAULT_NOTIFY },
19
+ dailySummaryAt: '21:00', quietHours: null, approvals: 'off', approvalExpirySec: 90, repriceTolPct: 3, showAbsoluteBook: false,
20
+ });
21
+ export const connectorsPath = (home) => join(home, 'connectors.json');
22
+ const isTier = (x) => x === 'notify' || x === 'control' || x === 'approve';
23
+ const hhmm = (x) => (typeof x === 'string' && /^([01]\d|2[0-3]):[0-5]\d$/.test(x) ? x : null);
24
+ const id = (x) => (typeof x === 'string' && /^-?\d{1,20}$/.test(x) ? x : typeof x === 'number' && Number.isInteger(x) ? String(x) : null);
25
+ /** Read connectors.json; a missing file is the defaults (disabled), a broken one is an error (the bot stays off). */
26
+ export function loadConnectors(home) {
27
+ const p = connectorsPath(home);
28
+ if (!existsSync(p))
29
+ return { ok: true, config: { schema: CONNECTORS_SCHEMA, telegram: defaultTelegram() }, exists: false };
30
+ let raw;
31
+ try {
32
+ raw = JSON.parse(readFileSync(p, 'utf8'));
33
+ }
34
+ catch (e) {
35
+ return { ok: false, error: `${p} is not JSON: ${e.message.slice(0, 120)}` };
36
+ }
37
+ const d = raw;
38
+ if (d.schema !== CONNECTORS_SCHEMA)
39
+ return { ok: false, error: `${p}: schema is not ${CONNECTORS_SCHEMA}` };
40
+ const t = d.telegram ?? {};
41
+ const base = defaultTelegram();
42
+ const allow = [];
43
+ for (const a of Array.isArray(t.allow) ? t.allow : []) {
44
+ const x = a;
45
+ const s = id(x.senderId);
46
+ const c = id(x.chatId);
47
+ if (!s || !c)
48
+ continue;
49
+ allow.push({ senderId: s, chatId: c, tiers: Array.isArray(x.tiers) ? x.tiers.filter(isTier) : [], pairedAt: typeof x.pairedAt === 'number' ? x.pairedAt : 0 });
50
+ }
51
+ const n = (t.notify ?? {});
52
+ const bool = (v, dflt) => (typeof v === 'boolean' ? v : dflt);
53
+ const bot = t.bot && typeof t.bot === 'object' ? t.bot : null;
54
+ const exp = Number(t.approvalExpirySec);
55
+ const tol = Number(t.repriceTolPct);
56
+ const qh = typeof t.quietHours === 'string' && /^([01]\d|2[0-3]):[0-5]\d-([01]\d|2[0-3]):[0-5]\d$/.test(t.quietHours) ? t.quietHours : null;
57
+ const telegram = {
58
+ enabled: t.enabled === true,
59
+ bot: bot && id(bot.id) && typeof bot.username === 'string' ? { id: id(bot.id), username: String(bot.username).slice(0, 64) } : null,
60
+ secretStore: t.secretStore === 'keychain' || t.secretStore === 'libsecret' || t.secretStore === 'file' ? t.secretStore : null,
61
+ tiers: Array.isArray(t.tiers) ? t.tiers.filter(isTier) : base.tiers,
62
+ allow,
63
+ notify: {
64
+ candidates: n.candidates === 'each' || n.candidates === 'off' ? n.candidates : 'digest',
65
+ entries: bool(n.entries, true), closes: bool(n.closes, true), halts: bool(n.halts, true), spend: bool(n.spend, true), daily: bool(n.daily, true),
66
+ },
67
+ dailySummaryAt: t.dailySummaryAt === null ? null : hhmm(t.dailySummaryAt) ?? base.dailySummaryAt,
68
+ quietHours: qh,
69
+ approvals: t.approvals === 'on' ? 'on' : 'off',
70
+ // §8.4: 60 to 120 s, default 90.
71
+ approvalExpirySec: Number.isFinite(exp) ? Math.min(120, Math.max(60, Math.round(exp))) : 90,
72
+ repriceTolPct: Number.isFinite(tol) && tol > 0 ? Math.min(10, tol) : 3,
73
+ showAbsoluteBook: t.showAbsoluteBook === true,
74
+ };
75
+ return { ok: true, config: { schema: CONNECTORS_SCHEMA, telegram, ...(d.signals !== undefined ? { signals: d.signals } : {}) }, exists: true };
76
+ }
77
+ export function saveConnectors(home, c) {
78
+ mkdirSync(home, { recursive: true, mode: 0o700 });
79
+ writeJsonAtomic(connectorsPath(home), c);
80
+ }
81
+ /** The allowlist entry for a sender in a chat, or null. A sender paired in one chat is not allowed in another. */
82
+ export function allowed(t, senderId, chatId, tier) {
83
+ if (!t.tiers.includes(tier))
84
+ return null;
85
+ return t.allow.find((a) => a.senderId === senderId && a.chatId === chatId && a.tiers.includes(tier)) ?? null;
86
+ }
87
+ /** True while local time is inside "HH:MM-HH:MM" (wrapping midnight). */
88
+ export function inQuietHours(q, t) {
89
+ if (!q)
90
+ return false;
91
+ const [a, b] = q.split('-').map((x) => { const [h, m] = x.split(':').map(Number); return h * 60 + m; });
92
+ const d = new Date(t);
93
+ const now = d.getHours() * 60 + d.getMinutes();
94
+ return a <= b ? now >= a && now < b : now >= a || now < b;
95
+ }
@@ -0,0 +1,24 @@
1
+ // dexterm autotrade chat — the transport-agnostic connector (task-1527, AUTOTRADE-SPEC §8.2).
2
+ //
3
+ // The engine never imports a transport. A connector delivers three things inward and two outward:
4
+ // inward commands from a closed list ("/status", "/pause", …), button callbacks (the ONLY thing that can
5
+ // authorise a trade), and the pairing message "/pair <code>". Free text is never parsed into anything:
6
+ // it arrives as `text` with its length only and is answered with the help text.
7
+ // outward plain-text messages (never markup), PnL card images, and proposals with buttons whose labels and
8
+ // callback data come from engine state only.
9
+ //
10
+ // Transports: telegram (long poll from the user's machine, telegram.ts) and fake (in memory, fake.ts, every test).
11
+ /** The closed command list (§8.4 tier 2). Anything else is `help`. */
12
+ export const COMMANDS = ['status', 'positions', 'pause', 'resume', 'kill', 'stop', 'flatten', 'strategies', 'use', 'approvals', 'help', 'pair', 'start'];
13
+ /** "/Status@my_bot arg" → {command, args}; null when it is not a slash command. Unknown names map to help. */
14
+ export function parseCommand(text) {
15
+ const t = text.trim();
16
+ if (!t.startsWith('/'))
17
+ return null;
18
+ const [head, ...rest] = t.split(/\s+/);
19
+ const name = head.slice(1).split('@')[0].toLowerCase();
20
+ const command = COMMANDS.includes(name) ? name : 'help';
21
+ // Args are short tokens only (a preset id, a pairing code, on/off): never prose.
22
+ const args = rest.slice(0, 3).map((a) => a.slice(0, 40));
23
+ return { command, args };
24
+ }
@@ -0,0 +1,104 @@
1
+ // dexterm autotrade chat — tier 2, control (task-1527, AUTOTRADE-SPEC §8.4).
2
+ //
3
+ // The command list is closed; anything else gets the help text. Chat can always make things SAFER and never less
4
+ // safe:
5
+ // /status /positions /strategies /help read-only (positions come with a [Sell] button each, bound like a proposal)
6
+ // /pause no new entries; exits keep running always
7
+ // /resume lifts a PAUSE only; never a kill, panic, breaker halt or guard
8
+ // /kill (= /stop) `autotrade stop`: sessions closed, no entries, exits running always
9
+ // /flatten sells everything at the next tick, after a [Yes, flatten] button
10
+ // /use <id> an equal-or-tighter validated strategy only, after a [Yes, switch] button;
11
+ // never changes paper/live; a looser one says "change it locally"
12
+ // /approvals on every entry waits for a tap (safer). `off` is local only
13
+ // Not from chat, ever: live mode, looser caps or strategies, approvals off, resuming after a kill or a panic.
14
+ // The router only calls the ControlPort the host gives it; the port writes the same control file the CLI and the
15
+ // Q screen write, so chat is a remote control for the same local engine and adds no path around its fences.
16
+ import { inert, pct, shortMint } from "./format.js";
17
+ export const HELP_LINES = [
18
+ 'dexterm bot · commands:',
19
+ '/status · /positions · /strategies',
20
+ '/pause (no new entries) · /resume (lifts a pause only)',
21
+ '/kill or /stop (stop the runner: no entries, exits keep running)',
22
+ '/flatten (sell everything, asks first)',
23
+ '/use <strategy> (equal or tighter only, asks first)',
24
+ '/approvals on (every entry waits for your tap)',
25
+ 'Trades are approved only with buttons. I never ask for keys or seed phrases.',
26
+ ];
27
+ export const NEVER_RATE_LIMITED = ['kill', 'stop', 'pause', 'flatten'];
28
+ export class ControlRouter {
29
+ port;
30
+ constructor(port) { this.port = port; }
31
+ handle(c) {
32
+ const p = this.port;
33
+ switch (c.command) {
34
+ case 'help':
35
+ case 'start': return { lines: HELP_LINES };
36
+ case 'pair': return { lines: ['This chat is already paired. Pairing happens on your computer (dexterm telegram pair).'] };
37
+ case 'status': {
38
+ const s = p.status();
39
+ if (!s)
40
+ return { lines: ['No runner is running on your computer.'] };
41
+ return {
42
+ lines: [
43
+ `STATUS · ${s.state}${s.haltReason ? ` (${inert(s.haltReason, 40)})` : ''}`,
44
+ `strategy ${inert(s.strategy.id, 32)} v${inert(s.strategy.version, 12)} (${s.strategy.hash.slice(0, 8)})`,
45
+ `open ${s.open} · candidates ${s.candidates} · day P/L ${pct(s.dayPnlPct)}`,
46
+ `model spend today $${s.spend.todayUsd.toFixed(2)}${Number.isFinite(s.spend.dayCapUsd) ? ` of $${s.spend.dayCapUsd}` : ''}${s.allowance?.fiveHour !== null && s.allowance?.fiveHour !== undefined ? ` · 5 h window ${Math.round(s.allowance.fiveHour * 100)}%` : ''}`,
47
+ `approvals: ${s.approvals}`,
48
+ ...(s.real ? [`executor: ${inert(s.real.executor, 160)}`, `limits: ${inert(s.real.limits, 160)}`, ...(s.real.pause ? [`NEW BUYS PAUSED: ${inert(s.real.pause, 120)}`] : [])] : []),
49
+ ],
50
+ };
51
+ }
52
+ case 'positions': {
53
+ const ps = p.positions();
54
+ if (!ps.length)
55
+ return { lines: ['No open positions.'] };
56
+ const shown = ps.slice(0, 5);
57
+ return {
58
+ lines: [`POSITIONS · ${ps.length} open`, ...ps.slice(0, 10).map((x) => `"${inert(x.symbol, 20)}" ${shortMint(x.mint)} · ${inert(x.profile, 24)} · ${pct(x.pnlPct)}${x.freeBag ? ' · cost back' : ''}${x.next ? ` · ${inert(x.next, 40)}` : ''}`)],
59
+ confirm: shown.map((x) => ({ kind: 'sell', lines: [`SELL "${inert(x.symbol, 20)}" ${shortMint(x.mint)} at the next tick? (${pct(x.pnlPct)} now)`], yes: 'Sell', payload: { mint: x.mint } })),
60
+ };
61
+ }
62
+ case 'pause': return { lines: [p.pause().message] };
63
+ case 'resume': return { lines: [p.resume().message] };
64
+ case 'kill':
65
+ case 'stop': return { lines: [p.kill().message] };
66
+ case 'flatten': {
67
+ const s = p.status();
68
+ if (!s)
69
+ return { lines: ['No runner is running on your computer.'] };
70
+ return { lines: [], confirm: [{ kind: 'flatten', lines: [`FLATTEN: stop the runner and sell all ${s.open} position${s.open === 1 ? '' : 's'} at the next tick?`], yes: 'Yes, flatten', payload: { runId: s.runId } }] };
71
+ }
72
+ case 'strategies': {
73
+ const list = p.strategies();
74
+ if (!list.length)
75
+ return { lines: ['No strategies found.'] };
76
+ return { lines: ['STRATEGIES (✓ = /use allowed: equal or tighter than the running one)', ...list.slice(0, 20).map((x) => `${x.current ? '▶' : x.allowed ? '✓' : '·'} ${inert(x.id, 32)} v${inert(x.version, 12)} (${x.hash.slice(0, 8)})${x.preset ? ' preset' : ''}${x.current ? ' (running)' : x.allowed ? '' : ` · ${inert(x.why, 60)}`}`)] };
77
+ }
78
+ case 'use': {
79
+ const id = c.args[0] ?? '';
80
+ if (!id)
81
+ return { lines: ['Usage: /use <strategy id> (see /strategies)'] };
82
+ const list = p.strategies();
83
+ const x = list.find((q) => q.id === id);
84
+ if (!x)
85
+ return { lines: [`No validated strategy "${inert(id, 32)}". See /strategies.`] };
86
+ if (x.current)
87
+ return { lines: [`${inert(id, 32)} is already running.`] };
88
+ if (!x.allowed)
89
+ return { lines: [`Refused: ${inert(id, 32)} is not equal or tighter (${inert(x.why, 80)}). Change it locally.`] };
90
+ return { lines: [], confirm: [{ kind: 'use', lines: [`SWITCH to ${inert(x.id, 32)} v${inert(x.version, 12)} (${x.hash.slice(0, 8)})? New entries stop now; the switch happens once the open positions have closed.`], yes: 'Yes, switch', payload: { id: x.id, hash: x.hash } }] };
91
+ }
92
+ case 'approvals': {
93
+ const a = (c.args[0] ?? '').toLowerCase();
94
+ if (a === 'on')
95
+ return { lines: [p.approvalsOn().message] };
96
+ if (a === 'off')
97
+ return { lines: ['Approvals can be turned off only on your computer (dexterm telegram approvals off).'] };
98
+ const s = p.status();
99
+ return { lines: [`approvals: ${s?.approvals ?? 'unknown'} · /approvals on makes every entry wait for your tap`] };
100
+ }
101
+ default: return { lines: HELP_LINES };
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,100 @@
1
+ // dexterm autotrade chat — the fake transport (task-1527, AUTOTRADE-SPEC §8.2). No network, for every test.
2
+ //
3
+ // An in-memory ChatConnector with a scriptable user: `user(id, chatId).command('/pause')`, `.tap(ref, data)`,
4
+ // `.text('buy it now')`. Every outbound message is kept in `sent` (with its plain text, buttons and edits), so a
5
+ // test can read exactly what a phone would show. Updates get increasing update ids like Telegram's; `redeliver`
6
+ // replays an old update (a duplicate delivery) to test the dedupe.
7
+ import { createHash } from 'node:crypto';
8
+ import { parseCommand } from "./connector.js";
9
+ import { plain } from "./format.js";
10
+ export class FakeConnector {
11
+ transport = 'fake';
12
+ sent = [];
13
+ toasts = [];
14
+ /** Make the next n sends fail (a Telegram outage, a 429). */
15
+ failSends = 0;
16
+ conflict = null;
17
+ cmdH = [];
18
+ actH = [];
19
+ txtH = [];
20
+ updateSeq = 1000;
21
+ msgSeq = 1;
22
+ delivered = [];
23
+ clock;
24
+ constructor(o = {}) { this.clock = o.now ?? Date.now; }
25
+ async start(signal) {
26
+ if (signal.aborted)
27
+ return;
28
+ await new Promise((r) => signal.addEventListener('abort', () => r(), { once: true }));
29
+ }
30
+ async send(to, msg) {
31
+ if (this.failSends > 0) {
32
+ this.failSends--;
33
+ throw new Error('fake transport: send failed');
34
+ }
35
+ const ref = { chatId: to.chatId, messageId: String(this.msgSeq++) };
36
+ const lines = msg.kind === 'card' ? [msg.caption] : msg.lines;
37
+ this.sent.push({ ref, msg, text: plain(msg.badge, lines), buttons: msg.kind === 'proposal' ? msg.buttons.map((b) => ({ ...b })) : [], edits: [], acks: [] });
38
+ return ref;
39
+ }
40
+ async edit(ref, msg) {
41
+ const s = this.sent.find((x) => x.ref.chatId === ref.chatId && x.ref.messageId === ref.messageId);
42
+ if (!s)
43
+ throw new Error('fake transport: no such message');
44
+ s.edits.push(msg);
45
+ s.buttons = msg.kind === 'proposal' ? msg.buttons.map((b) => ({ ...b })) : [];
46
+ s.text = plain(msg.badge, msg.kind === 'card' ? [msg.caption] : msg.lines);
47
+ }
48
+ onCommand(h) { this.cmdH.push(h); }
49
+ onAction(h) { this.actH.push(h); }
50
+ onText(h) { this.txtH.push(h); }
51
+ async ackAction(a, toast) {
52
+ this.toasts.push({ actionId: a.actionId, toast });
53
+ const s = this.sent.find((x) => x.ref.chatId === a.ref.chatId && x.ref.messageId === a.ref.messageId);
54
+ s?.acks.push(toast);
55
+ }
56
+ health() { return { transport: this.transport, lastPollAt: this.clock(), conflict: this.conflict, rateLimited: 0, errors: 0, lastError: null }; }
57
+ /** A scripted person in a chat (`sender`: the @username / display name Telegram would report with a command). */
58
+ user(senderId, chatId = senderId, chatKind = 'private', sender) {
59
+ const base = () => ({ transport: this.transport, updateId: String(++this.updateSeq), chatId, chatKind, senderId, t: this.clock() });
60
+ return {
61
+ /** Send "/cmd args" (or any text: non-commands arrive as text, never parsed). */
62
+ command: (text) => {
63
+ const p = parseCommand(text);
64
+ if (!p) {
65
+ this.deliver({ kind: 'text', x: { ...base(), length: text.length, sha256: createHash('sha256').update(text).digest('hex') } });
66
+ return;
67
+ }
68
+ this.deliver({ kind: 'command', c: { ...base(), ...(sender ? { sender } : {}), command: p.command, args: p.args } });
69
+ },
70
+ text: (text) => this.deliver({ kind: 'text', x: { ...base(), length: text.length, sha256: createHash('sha256').update(text).digest('hex') } }),
71
+ /** Tap a button: by its message ref and data (a forged tap can carry any data). */
72
+ tap: (ref, data) => this.deliver({ kind: 'action', a: { ...base(), actionId: `cb${this.updateSeq}`, ref, data } }),
73
+ };
74
+ }
75
+ /** The newest message whose text matches, for tests. */
76
+ last(re) {
77
+ for (let i = this.sent.length - 1; i >= 0; i--)
78
+ if (!re || re.test(this.sent[i].text))
79
+ return this.sent[i];
80
+ return undefined;
81
+ }
82
+ /** Deliver an old update again (Telegram can redeliver; the router dedupes by update id). */
83
+ redeliver(index = -1) {
84
+ const u = this.delivered.at(index);
85
+ if (u)
86
+ this.dispatch(u);
87
+ }
88
+ deliver(u) { this.delivered.push(u); this.dispatch(u); }
89
+ dispatch(u) {
90
+ if (u.kind === 'command')
91
+ for (const h of this.cmdH)
92
+ h(u.c);
93
+ else if (u.kind === 'action')
94
+ for (const h of this.actH)
95
+ h(u.a);
96
+ else
97
+ for (const h of this.txtH)
98
+ h(u.x);
99
+ }
100
+ }
@@ -0,0 +1,71 @@
1
+ // dexterm autotrade chat — inert text and the message templates' small helpers (task-1527, AUTOTRADE-SPEC §8.6).
2
+ //
3
+ // Every token-derived string (symbol, name, the agent's reason, which quotes token text) goes through the app's
4
+ // prompt firewall (`@dexterm/mcp firewall()`: control and bidi characters stripped, whitespace collapsed, links
5
+ // and addresses replaced, length capped), then the chat rules: sent as plain text (no parse_mode, so no markup or
6
+ // link can be injected), quoted, and with the characters Telegram turns into tappable things in plain text
7
+ // (`/command`, `@mention`) swapped for look-alikes, so a token named "/kill" or "@phish" is only ever text.
8
+ // Buttons never carry token text: their labels and data are built from engine state only (approvals.ts).
9
+ import { firewall } from '../../../mcp/src/index.js';
10
+ import { redact } from "./redact.js";
11
+ const UNESCAPE = { '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"' };
12
+ /**
13
+ * What chat never shows (§8.6 "Hidden"), replaced before the firewall: a keypair as a JSON byte array, 0x-hex and
14
+ * long bare hex, API-key-shaped strings (vendor prefixes, `api_key=…`, UUIDs), and file paths (absolute, `~/`,
15
+ * `./`, Windows). Engine strings (an exit detail, a halt reason, a guard effect) can quote any of these.
16
+ */
17
+ const HIDDEN = [
18
+ [/\[\s*\d{1,3}(?:\s*,\s*\d{1,3}){15,}\s*\]/g, '[key]'],
19
+ [/\b0x[0-9a-fA-F]{8,}\b/g, '[hex]'],
20
+ [/\b[0-9a-fA-F]{32,}\b/g, '[hex]'],
21
+ [/\b(?:sk|pk|rk)[-_](?:[A-Za-z0-9]+[-_])*[A-Za-z0-9]{12,}/g, '[key]'],
22
+ [/\b(?:AKIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9]{20,}|xox[abprs]-[A-Za-z0-9-]{10,}|AIza[0-9A-Za-z_-]{30,})\b/g, '[key]'],
23
+ [/\b(api[-_]?key|access[-_]?token|auth[-_]?token|secret[-_]?key|client[-_]?secret|private[-_]?key|password|passwd)(\s*[=:]\s*)[^\s'",;&]+/gi, '$1$2[key]'],
24
+ [/\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b/g, '[key]'],
25
+ // Paths: home- or dot-relative, under a filesystem root, any absolute one ending in a file name, or Windows. A JSON
26
+ // pointer (`/bankroll/startUsd`, a /strategies reason) and a slash command stay text.
27
+ [/(^|[\s'"(=:,[])(?:~|\.{1,2})\/[^\s'"()]*/g, '$1[path]'],
28
+ [/(^|[\s'"(=:,[])\/(?:Users|home|root|var|tmp|private|etc|opt|usr|mnt|media|Volumes|Library|System|Applications|srv|run|dev|proc)\/[^\s'"()]*/g, '$1[path]'],
29
+ [/(^|[\s'"(=:,[])\/(?:[^\s/'"()]+\/)+[^\s/'"()]*\.[A-Za-z0-9]{1,8}\b/g, '$1[path]'],
30
+ [/(^|[\s'"(=:,[])[A-Za-z]:\\[^\s'"()]*/g, '$1[path]'],
31
+ ];
32
+ /** Secrets, keys and paths taken out of an engine or token string (inert() applies it first). */
33
+ export function hideSecrets(s) {
34
+ let out = redact(s);
35
+ for (const [re, to] of HIDDEN)
36
+ out = out.replace(re, to);
37
+ return out;
38
+ }
39
+ /** Token-derived text made inert for a chat message: secrets and paths hidden, firewalled, capped, no tappable command or mention. */
40
+ export function inert(text, max = 32, source = 'token') {
41
+ const fw = firewall(text === null || text === undefined ? '' : hideSecrets(String(text)), source, 'description');
42
+ if (!fw)
43
+ return '';
44
+ const m = fw.match(/^<untrusted [^>]*>([\s\S]*)<\/untrusted>$/);
45
+ let s = (m ? m[1] : fw).replace(/&(amp|lt|gt|quot);/g, (x) => UNESCAPE[x] ?? x);
46
+ s = s.replace(/\//g, '∕').replace(/@/g, '@').replace(/"/g, "'");
47
+ if (s.length > max)
48
+ s = `${s.slice(0, Math.max(1, max - 1))}…`;
49
+ return s;
50
+ }
51
+ /** `abcd…wxyz`: the most of an address a message ever shows. */
52
+ export const shortMint = (m) => (m.length > 10 ? `${m.slice(0, 4)}…${m.slice(-4)}` : m);
53
+ export const pct = (v, digits = 1) => (v === null || v === undefined || !Number.isFinite(v) ? '?' : `${v >= 0 ? '+' : ''}${v.toFixed(digits)}%`);
54
+ export function price(v) {
55
+ if (v === null || v === undefined || !Number.isFinite(v) || v <= 0)
56
+ return '?';
57
+ return v >= 1 ? v.toFixed(4) : v.toPrecision(4);
58
+ }
59
+ export function usdShort(v) {
60
+ if (v === null || v === undefined || !Number.isFinite(v))
61
+ return '?';
62
+ const a = Math.abs(v);
63
+ return a >= 1e6 ? `$${(v / 1e6).toFixed(2)}M` : a >= 1e3 ? `$${(v / 1e3).toFixed(1)}K` : `$${v.toFixed(2)}`;
64
+ }
65
+ /** Plain text of a message for the fake transport and the audit (lines joined, badge first). */
66
+ export function plain(badge, lines) {
67
+ return [badge ? `${badge} · ${lines[0] ?? ''}` : lines[0] ?? '', ...lines.slice(1)].join('\n');
68
+ }
69
+ /** Telegram's limit is 4096 characters; stay well under it. */
70
+ export const MAX_MESSAGE = 3500;
71
+ export const clip = (s, max = MAX_MESSAGE) => (s.length > max ? `${s.slice(0, max - 1)}…` : s);
@@ -0,0 +1,187 @@
1
+ // dexterm autotrade chat — the hub: one connector, the allowlist, the rate limits, the audit (task-1527, §8.1, §8.6).
2
+ //
3
+ // Inbound, in order, for every update from the connector's own poll:
4
+ // dedupe by update id (a redelivered update is recorded once and never acted on twice);
5
+ // audit (update id, sender, chat, the command or the action's record id; free text as length + hash only);
6
+ // pairing (only while a local pairing window is open: `/pair <code>` from a private chat);
7
+ // the allowlist: an unknown sender gets NO reply and nothing happens (five in an hour raise a local alert:
8
+ // "possible token leak"); a paired sender needs the tier the command needs, in the chat it was paired in;
9
+ // rate limit: 10 commands a minute per sender, except /kill, /stop, /pause and /flatten (never limited);
10
+ // commands → the ControlRouter; button callbacks → the ApprovalBroker (the only trade authority);
11
+ // free text → the help text, never parsed.
12
+ // Outbound: notify messages go to every chat paired with the notify tier; replies go to the asking chat only.
13
+ import { allowed } from "./config.js";
14
+ import { ControlRouter, HELP_LINES, NEVER_RATE_LIMITED } from "./control.js";
15
+ export const COMMANDS_PER_MIN = 10;
16
+ export const UNKNOWN_ALERT_PER_HOUR = 5;
17
+ const READ_ONLY = ['status', 'positions', 'strategies', 'help', 'start'];
18
+ export class ChatHub {
19
+ o;
20
+ now;
21
+ seen = new Set();
22
+ cmdTimes = new Map();
23
+ unknownTimes = [];
24
+ lastUnknownAlert = 0;
25
+ router;
26
+ pairing = null;
27
+ inflight = new Set();
28
+ constructor(o) {
29
+ this.o = o;
30
+ this.now = o.now ?? Date.now;
31
+ this.router = o.port ? new ControlRouter(o.port) : null;
32
+ o.connector.onCommand((c) => this.track(this.onCommand(c)));
33
+ o.connector.onAction((a) => this.track(this.onAction(a)));
34
+ o.connector.onText((t) => this.track(this.onText(t)));
35
+ }
36
+ track(p) {
37
+ const q = p.catch((e) => { this.o.audit.add(this.now(), 'hub_error', { error: e instanceof Error ? e.message.slice(0, 160) : String(e).slice(0, 160) }); }).finally(() => this.inflight.delete(q));
38
+ this.inflight.add(q);
39
+ }
40
+ /** Wait for every inbound handler in flight (tests, shutdown). */
41
+ async idle() { while (this.inflight.size)
42
+ await Promise.allSettled([...this.inflight]); }
43
+ setPort(port) { this.router = port ? new ControlRouter(port) : null; }
44
+ /** Open a local pairing window: `/pair <code>` from a private chat is checked against it. */
45
+ openPairing(window, onResult) { this.pairing = { window, onResult }; }
46
+ closePairing() { this.pairing = null; }
47
+ dup(updateId) {
48
+ if (this.seen.has(updateId))
49
+ return true;
50
+ this.seen.add(updateId);
51
+ if (this.seen.size > 5000) {
52
+ const f = this.seen.values().next().value;
53
+ if (f !== undefined)
54
+ this.seen.delete(f);
55
+ }
56
+ return false;
57
+ }
58
+ unknown(senderId, what) {
59
+ const now = this.now();
60
+ this.unknownTimes = this.unknownTimes.filter((t) => now - t < 3600_000);
61
+ this.unknownTimes.push(now);
62
+ this.o.audit.add(now, 'in_unknown_sender', { senderId, what });
63
+ if (this.unknownTimes.length >= UNKNOWN_ALERT_PER_HOUR && now - this.lastUnknownAlert > 3600_000) {
64
+ this.lastUnknownAlert = now;
65
+ this.o.alert?.(`telegram: ${this.unknownTimes.length} messages from unknown accounts in the last hour: possible bot token leak. Revoke it with BotFather (/revoke), then run \`dexterm telegram setup\` again.`);
66
+ }
67
+ }
68
+ entry(senderId, chatId, tier) { return allowed(this.o.config(), senderId, chatId, tier); }
69
+ limited(senderId, command) {
70
+ if (NEVER_RATE_LIMITED.includes(command))
71
+ return false;
72
+ const now = this.now();
73
+ const ts = (this.cmdTimes.get(senderId) ?? []).filter((t) => now - t < 60_000);
74
+ const over = ts.length >= COMMANDS_PER_MIN;
75
+ if (!over)
76
+ ts.push(now);
77
+ this.cmdTimes.set(senderId, ts);
78
+ return over;
79
+ }
80
+ async reply(chatId, lines, template) {
81
+ if (!lines.length)
82
+ return;
83
+ try {
84
+ const ref = await this.o.connector.send({ chatId }, { kind: 'text', badge: this.o.badge, lines });
85
+ this.o.audit.add(this.now(), 'out', { template, chatId, messageId: ref.messageId });
86
+ }
87
+ catch (e) {
88
+ this.o.audit.add(this.now(), 'out_failed', { template, chatId, error: e.message.slice(0, 160) });
89
+ }
90
+ }
91
+ async onCommand(c) {
92
+ if (this.dup(c.updateId)) {
93
+ this.o.audit.add(this.now(), 'in_duplicate', { updateId: c.updateId });
94
+ return;
95
+ }
96
+ this.o.audit.add(this.now(), 'in_command', { updateId: c.updateId, senderId: c.senderId, chatId: c.chatId, chatKind: c.chatKind, command: c.command, args: c.command === 'pair' ? c.args.length : c.args.map((a) => a.slice(0, 32)) });
97
+ if (c.command === 'pair' && this.pairing) {
98
+ // Another poller or a webhook on this bot means someone else holds the token and may have read the code:
99
+ // the window closes for everyone (A-B1). The local side starts over once the token is revoked.
100
+ const conflict = this.o.connector.health().conflict;
101
+ const r = conflict ? this.pairing.window.abort() : this.pairing.window.attempt(c.senderId, c.chatId, c.chatKind, c.args);
102
+ this.o.audit.add(this.now(), 'pairing', { senderId: c.senderId, chatId: c.chatId, result: r.ok ? 'code-accepted' : r.why });
103
+ this.pairing.onResult(r, c);
104
+ // The code alone does not pair: the person at the computer confirms the account first (telegram-cmd.ts).
105
+ if (r.ok)
106
+ await this.reply(c.chatId, ['Code accepted. Confirm this account on your computer to finish pairing. I never ask for keys or seed phrases.'], 'pair_code_accepted');
107
+ else if (r.why === 'group-chat')
108
+ await this.reply(c.chatId, ['Pair from a private chat with the bot, not a group.'], 'pair_refused');
109
+ return;
110
+ }
111
+ // Commands count only in a private chat (the allowlist pins the chat id too; this is the second fence).
112
+ if (c.chatKind !== 'private') {
113
+ this.unknown(c.senderId, `command:${c.command}:${c.chatKind}`);
114
+ return;
115
+ }
116
+ const tier = READ_ONLY.includes(c.command) ? 'notify' : 'control';
117
+ const e = this.entry(c.senderId, c.chatId, tier) ?? (tier === 'notify' ? this.entry(c.senderId, c.chatId, 'control') : null);
118
+ if (!e) {
119
+ this.unknown(c.senderId, `command:${c.command}`);
120
+ return;
121
+ }
122
+ if (this.limited(c.senderId, c.command)) {
123
+ this.o.audit.add(this.now(), 'rate_limited', { senderId: c.senderId, command: c.command });
124
+ await this.reply(c.chatId, ['Too many commands: wait a minute. /kill and /pause always work.'], 'rate_limited');
125
+ return;
126
+ }
127
+ if (!this.router) {
128
+ await this.reply(c.chatId, ['No runner is connected right now.'], 'no_runner');
129
+ return;
130
+ }
131
+ const r = this.router.handle(c);
132
+ this.o.audit.add(this.now(), 'command_result', { updateId: c.updateId, command: c.command, lines: r.lines.length, confirms: r.confirm?.length ?? 0 });
133
+ await this.reply(c.chatId, r.lines, `cmd_${c.command}`);
134
+ for (const cf of r.confirm ?? []) {
135
+ if (!this.o.broker) {
136
+ await this.reply(c.chatId, ['Confirm buttons are not available here: use dexterm on your computer.'], 'no_broker');
137
+ break;
138
+ }
139
+ await this.o.broker.confirm(cf.kind, { senderId: c.senderId, chatId: c.chatId }, cf.lines, cf.yes, cf.payload);
140
+ }
141
+ }
142
+ async onAction(a) {
143
+ if (this.dup(a.updateId)) {
144
+ this.o.audit.add(this.now(), 'in_duplicate', { updateId: a.updateId });
145
+ return;
146
+ }
147
+ const recId = /^[pc]:([A-Za-z0-9_-]{8}):/.exec(a.data)?.[1] ?? null;
148
+ this.o.audit.add(this.now(), 'in_action', { updateId: a.updateId, senderId: a.senderId, chatId: a.chatId, chatKind: a.chatKind, messageId: a.ref.messageId, record: recId, bytes: a.data.length });
149
+ // Unknown senders: dropped (counted), no toast beyond the transport's spinner end.
150
+ if (!this.o.config().allow.some((x) => x.senderId === a.senderId)) {
151
+ this.unknown(a.senderId, 'action');
152
+ await this.o.connector.ackAction(a, '');
153
+ return;
154
+ }
155
+ if (!this.o.broker) {
156
+ await this.o.connector.ackAction(a, 'Not available.');
157
+ return;
158
+ }
159
+ await this.o.broker.handleAction(a);
160
+ }
161
+ async onText(t) {
162
+ if (this.dup(t.updateId))
163
+ return;
164
+ this.o.audit.add(this.now(), 'in_text', { updateId: t.updateId, senderId: t.senderId, chatId: t.chatId, length: t.length, sha256: t.sha256.slice(0, 16) });
165
+ if (!this.entry(t.senderId, t.chatId, 'notify') && !this.entry(t.senderId, t.chatId, 'control')) {
166
+ this.unknown(t.senderId, 'text');
167
+ return;
168
+ }
169
+ await this.reply(t.chatId, ['I only take /commands and buttons; messages are never read as trades.', ...HELP_LINES.slice(1, 3)], 'text_help');
170
+ }
171
+ /** Send to every chat paired with the notify tier. */
172
+ async broadcast(msg, template) {
173
+ const t = this.o.config();
174
+ if (!t.tiers.includes('notify'))
175
+ return;
176
+ const chats = [...new Set(t.allow.filter((a) => a.tiers.includes('notify')).map((a) => a.chatId))];
177
+ for (const chatId of chats) {
178
+ try {
179
+ const ref = await this.o.connector.send({ chatId }, msg);
180
+ this.o.audit.add(this.now(), 'out', { template, chatId, messageId: ref.messageId, kind: msg.kind, lines: msg.kind === 'card' ? 1 : msg.lines.length });
181
+ }
182
+ catch (e) {
183
+ this.o.audit.add(this.now(), 'out_failed', { template, chatId, error: e.message.slice(0, 160) });
184
+ }
185
+ }
186
+ }
187
+ }
@@ -0,0 +1,18 @@
1
+ // dexterm autotrade chat (task-1527, AUTOTRADE-SPEC §8): notify, control and approve-to-trade through the user's own
2
+ // bot. connector (the interface) · fake (tests) · telegram (long poll) · redact · format · config (connectors.json) ·
3
+ // secrets (keychain / libsecret / 0600 file) · pairing · audit (hash chain) · notify · control · approvals · hub ·
4
+ // service (what a runner host starts).
5
+ export * from "./connector.js";
6
+ export * from "./fake.js";
7
+ export * from "./telegram.js";
8
+ export * from "./redact.js";
9
+ export * from "./format.js";
10
+ export * from "./config.js";
11
+ export * from "./secrets.js";
12
+ export * from "./pairing.js";
13
+ export * from "./audit.js";
14
+ export * from "./notify.js";
15
+ export * from "./control.js";
16
+ export * from "./approvals.js";
17
+ export * from "./hub.js";
18
+ export * from "./service.js";