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,232 @@
1
+ // dexterm autotrade — safe resume after a crash or a reboot (0.2.0 card B, AUTOTRADE-SPEC §7.4 "Restart safety").
2
+ //
3
+ // The service records the fleet it hosts in service/state.json. When the next start finds that fleet without a
4
+ // `fleet_end` (a crash, a kill -9, a power cut), recoverFleet() reads the fleet ledger for each strategy's runs and,
5
+ // for every member run without a `run_end`:
6
+ // - checks its hash chain (a torn last line from a crash mid-write is dropped first and reported);
7
+ // - rebuilds the paper positions still open from the run's own events: `position_adopted` snapshots (an earlier
8
+ // resume) plus every `fill` of that position, minus the positions a `trade_closed` closed;
9
+ // - collects every coin the run took (any buy, any adopted position, an earlier resume's taken list).
10
+ // The new run of the same strategy ADOPTS those positions (their exits keep running: stops, take-profits, trails,
11
+ // max hold from the original open time) and REFUSES a new entry on every taken coin (vet rule
12
+ // `service-resume-taken`), so a restart never buys a coin twice.
13
+ //
14
+ // Conservative choices, all in the direction of selling sooner and buying never:
15
+ // - an order still pending at the crash had no fill event, so it never happened: it is never re-sent;
16
+ // - an adopted position keeps no unfilled ladder tranche (no add after a restart) and its peak is the highest
17
+ // fill price (a trail measures from at most that);
18
+ // - a run whose chain is broken (an edited ledger) adopts nothing; its coins still count as taken.
19
+ // Mode-agnostic: a paper position is adopted into the paper book; a real-mode position (card A) is the user's
20
+ // executor's, and the same taken list keeps the next run from firing a second entry signal for it.
21
+ import { existsSync, readFileSync } from 'node:fs';
22
+ import { runFiles, verifyChain } from "./ledger.js";
23
+ export const RESUME_TAKEN_RULE = 'service-resume-taken';
24
+ /** Read a ledger file tolerating a torn final line (a crash mid-append); any other bad line is an error. */
25
+ export function readLedgerTolerant(path) {
26
+ const lines = readFileSync(path, 'utf8').split('\n');
27
+ const events = [];
28
+ let torn = false;
29
+ for (let i = 0; i < lines.length; i++) {
30
+ const line = lines[i];
31
+ if (!line.trim())
32
+ continue;
33
+ try {
34
+ events.push(JSON.parse(line));
35
+ }
36
+ catch (e) {
37
+ const rest = lines.slice(i + 1).some((l) => l.trim());
38
+ if (rest)
39
+ throw new Error(`line ${i + 1} is not JSON: ${e.message}`);
40
+ torn = true;
41
+ }
42
+ }
43
+ return { events, torn };
44
+ }
45
+ const num = (v, d = 0) => (typeof v === 'number' && Number.isFinite(v) ? v : d);
46
+ /** A placeholder tick at the last known price, until the live feed sends a real one (focus marks the coin held). */
47
+ export function placeholderTick(mint, symbol, t, price) {
48
+ return {
49
+ mint, t, kind: 'live', symbol, launchpad: 'unknown', createdAt: null, price, mcap: null, mcapResolved: null, mcapFlagged: false,
50
+ liq: null, vol5m: null, vol1h: null, vol24: null, buys5m: null, sells5m: null, buys24: null, sells24: null,
51
+ chg5m: null, chg1h: null, chg6h: null, chg24h: null, holders: null, top10: null, dev: null, mintAuth: null, freezeAuth: null, org: null,
52
+ };
53
+ }
54
+ /** Open positions and taken coins from ONE run's events (pure; the run is known to have no run_end). */
55
+ export function positionsFromEvents(events) {
56
+ const symbols = new Map();
57
+ const base = new Map();
58
+ const fills = new Map();
59
+ const closed = new Set();
60
+ const taken = new Set();
61
+ for (const e of events) {
62
+ const mint = typeof e.mint === 'string' ? e.mint : null;
63
+ if (e.kind === 'candidate' && mint && typeof e.symbol === 'string')
64
+ symbols.set(mint, e.symbol);
65
+ else if (e.kind === 'position_adopted' && e.position && typeof e.position === 'object') {
66
+ const p = e.position;
67
+ if (typeof p.id === 'string' && typeof p.mint === 'string') {
68
+ base.set(p.id, structuredClone(p));
69
+ taken.add(p.mint);
70
+ symbols.set(p.mint, p.symbol);
71
+ }
72
+ }
73
+ else if (e.kind === 'fill' && typeof e.positionId === 'string' && mint) {
74
+ const f = { t: num(e.t), side: e.side === 'sell' ? 'sell' : 'buy', usd: num(e.usd), tokens: num(e.tokens), price: num(e.price), avgPrice: num(e.avgPrice, num(e.price)), impactPct: num(e.impactPct), partial: !!e.partial, reason: String(e.reason ?? '') };
75
+ const cur = fills.get(e.positionId) ?? { mint, profile: null, fills: [] };
76
+ if (typeof e.profile === 'string')
77
+ cur.profile ??= e.profile;
78
+ cur.fills.push(f);
79
+ fills.set(e.positionId, cur);
80
+ if (f.side === 'buy')
81
+ taken.add(mint);
82
+ }
83
+ else if (e.kind === 'trade_closed' && e.trade && typeof e.trade === 'object') {
84
+ const t = e.trade;
85
+ if (typeof t.id === 'string')
86
+ closed.add(t.id);
87
+ if (typeof t.mint === 'string' && typeof t.symbol === 'string')
88
+ symbols.set(t.mint, t.symbol);
89
+ }
90
+ }
91
+ const out = [];
92
+ const ids = new Set([...base.keys(), ...fills.keys()]);
93
+ for (const id of ids) {
94
+ if (closed.has(id))
95
+ continue;
96
+ const b = base.get(id);
97
+ const f = fills.get(id);
98
+ const mint = b?.mint ?? f.mint;
99
+ const symbol = symbols.get(mint) ?? b?.symbol ?? `${mint.slice(0, 4)}…`;
100
+ const p = b ?? {
101
+ id, mint, symbol, profileId: f?.profile ?? 'default', openedAt: f.fills[0].t, sessionId: null,
102
+ tokens: 0, origTokens: 0, costUsd: 0, realizedUsd: 0, entryPrice: 0, peakPrice: 0, tpFilled: 0, trailArmed: false,
103
+ ladder: [], fills: [], status: 'open', stuck: false, closedAt: null, exitReason: null, liqWindow: [], lowTapeSince: null,
104
+ lastTick: placeholderTick(mint, symbol, f.fills[0].t, f.fills[0].price), wakeSent: {},
105
+ };
106
+ for (const x of f?.fills ?? []) {
107
+ if (x.side === 'buy') {
108
+ const tokens = p.tokens + x.tokens;
109
+ p.entryPrice = tokens > 0 ? (p.entryPrice * p.tokens + x.price * x.tokens) / tokens : x.price;
110
+ p.tokens = tokens;
111
+ p.origTokens += x.tokens;
112
+ p.costUsd += x.usd;
113
+ }
114
+ else {
115
+ p.tokens = Math.max(0, p.tokens - x.tokens);
116
+ p.realizedUsd += x.usd;
117
+ const tp = /^tp(\d+)$/.exec(x.reason);
118
+ if (tp)
119
+ p.tpFilled = Math.max(p.tpFilled, Number(tp[1]));
120
+ if (x.reason === 'cost-recovery')
121
+ p.costRecovered = true;
122
+ }
123
+ p.peakPrice = Math.max(p.peakPrice, x.price);
124
+ p.fills.push(x);
125
+ }
126
+ if (p.origTokens > 0 && p.tokens < p.origTokens * 1e-6)
127
+ continue; // sold out without a trade_closed line (torn)
128
+ if (!(p.tokens > 0))
129
+ continue;
130
+ const last = p.fills[p.fills.length - 1];
131
+ p.ladder = [];
132
+ p.sessionId = null;
133
+ p.status = 'open';
134
+ p.troughPrice = p.troughPrice ?? Math.min(...p.fills.map((x) => x.price));
135
+ if (last)
136
+ p.lastTick = placeholderTick(mint, p.symbol || symbol, last.t, last.price);
137
+ p.symbol = p.symbol || symbol;
138
+ out.push(p);
139
+ }
140
+ return { positions: out, taken: [...taken] };
141
+ }
142
+ /** What the crashed fleet left behind, per strategy (empty when it ended cleanly). */
143
+ export function recoverFleet(home, fleetId) {
144
+ const res = { fleetId, crashed: false, members: [], problems: [] };
145
+ let fpath;
146
+ try {
147
+ fpath = runFiles(home, fleetId).ledger;
148
+ }
149
+ catch (e) {
150
+ res.problems.push(e.message);
151
+ return res;
152
+ }
153
+ if (!existsSync(fpath)) {
154
+ res.problems.push(`no fleet ledger for ${fleetId}`);
155
+ return res;
156
+ }
157
+ let fleet;
158
+ try {
159
+ fleet = readLedgerTolerant(fpath).events;
160
+ }
161
+ catch (e) {
162
+ res.problems.push(`fleet ledger: ${e.message}`);
163
+ return res;
164
+ }
165
+ if (fleet.some((e) => e.kind === 'fleet_end'))
166
+ return res;
167
+ res.crashed = true;
168
+ const runsOf = new Map();
169
+ for (const e of fleet)
170
+ if (e.kind === 'member_start' && typeof e.member === 'string' && typeof e.runId === 'string')
171
+ runsOf.set(e.member, [...(runsOf.get(e.member) ?? []), e.runId]);
172
+ // An earlier resume's taken coins (the service writes them into the fleet ledger), so a second crash keeps them.
173
+ const takenBefore = new Map();
174
+ for (const e of fleet) {
175
+ if (e.kind !== 'service_resume' || !e.members || typeof e.members !== 'object')
176
+ continue;
177
+ for (const [id, v] of Object.entries(e.members)) {
178
+ if (Array.isArray(v?.taken))
179
+ takenBefore.set(id, [...(takenBefore.get(id) ?? []), ...v.taken.filter((x) => typeof x === 'string')]);
180
+ }
181
+ }
182
+ for (const id of takenBefore.keys())
183
+ if (!runsOf.has(id))
184
+ runsOf.set(id, []);
185
+ for (const [member, runIds] of runsOf) {
186
+ const m = { member, runIds: [], positions: [], taken: [], problems: [] };
187
+ const taken = new Set(takenBefore.get(member) ?? []);
188
+ for (const runId of runIds) {
189
+ let path;
190
+ try {
191
+ path = runFiles(home, runId).ledger;
192
+ }
193
+ catch (e) {
194
+ m.problems.push(e.message);
195
+ continue;
196
+ }
197
+ if (!existsSync(path))
198
+ continue;
199
+ let ev;
200
+ try {
201
+ ev = readLedgerTolerant(path);
202
+ }
203
+ catch (e) {
204
+ m.problems.push(`${runId}: ${e.message}; nothing adopted from it`);
205
+ continue;
206
+ }
207
+ if (ev.events.some((e) => e.kind === 'run_end'))
208
+ continue;
209
+ m.runIds.push(runId);
210
+ if (ev.torn)
211
+ m.problems.push(`${runId}: a torn last line (the crash) was dropped`);
212
+ const r = positionsFromEvents(ev.events);
213
+ for (const t of r.taken)
214
+ taken.add(t);
215
+ const chain = verifyChain(ev.events);
216
+ if (!chain.ok) {
217
+ m.problems.push(`${runId}: the hash chain breaks at event ${chain.at} (${chain.error}); nothing adopted from it, its coins stay taken`);
218
+ continue;
219
+ }
220
+ // A book numbers its positions p1, p2, …: an adopted one keeps its origin in its id so it never collides with
221
+ // the new run's own (an id that already names a run came from an earlier resume).
222
+ for (const p of r.positions)
223
+ if (!p.id.includes('/'))
224
+ p.id = `${runId}/${p.id}`;
225
+ m.positions.push(...r.positions);
226
+ }
227
+ m.taken = [...taken];
228
+ if (m.runIds.length || m.problems.length || m.taken.length)
229
+ res.members.push(m);
230
+ }
231
+ return res;
232
+ }
@@ -0,0 +1,289 @@
1
+ // dexterm autotrade — the always-on service: lock, heartbeat, stop marker, crash-loop guard and the `ensure`
2
+ // decision (0.2.0 card B, AUTOTRADE-SPEC §7.4 / AT-10). Mode-agnostic: the service hosts `run --all` and never
3
+ // decides paper or real itself.
4
+ //
5
+ // Shape (one per machine and DEXTERM_HOME):
6
+ // OS scheduler (LaunchAgent / systemd user timer / scheduled task), every 60 s and at login
7
+ // └─ `dexterm autotrade service ensure` exits at once when the heartbeat is fresh; otherwise starts the service
8
+ // └─ `dexterm autotrade service run` detached: service lock + heartbeat every 10 s + guard + the fleet
9
+ //
10
+ // Files under $DEXTERM_HOME/autotrade/service/:
11
+ // service.lock {pid, bootAt, startedAt}: a stale lock is reclaimed only when that pid is dead (or from an older boot)
12
+ // heartbeat.json dexterm-autotrade-service/1: state, fleet id, guard states; rewritten every 10 s; `stopped` on a
13
+ // clean exit (anything else with a dead pid is a crash)
14
+ // STOP the stop marker: `service stop`, a Telegram / Q-screen / CLI stop of the fleet, or a plain `touch`.
15
+ // A clean stop stays stopped: `ensure` does nothing until `service start` removes it.
16
+ // starts.json the crash-loop guard: every start after a crash, and the give-up record (`attention`)
17
+ // state.json the fleet the service is running (recovery after a crash reads its ledgers, service-recover.ts)
18
+ // Logs: $DEXTERM_HOME/autotrade/logs/service.log (+ .1), size-capped (BoundedLog).
19
+ //
20
+ // Crash-loop guard: a start that follows a crash waits a backoff (0, 0, 1, 2, 4 min for the 1st..5th crash in an
21
+ // hour); the 6th crash within an hour writes `attention`, sends ONE notification and leaves the service down until
22
+ // `dexterm autotrade service status --clear`. A clean stop and a manual `service start` never count.
23
+ //
24
+ // Nothing here holds a key, signs, touches a wallet or the network, and no model reaches any of it.
25
+ import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeSync } from 'node:fs';
26
+ import { uptime } from 'node:os';
27
+ import { join } from 'node:path';
28
+ import { writeJsonAtomic } from "./ledger.js";
29
+ export const SERVICE_SCHEMA = 'dexterm-autotrade-service/1';
30
+ export const SERVICE_HEARTBEAT_MS = 10_000;
31
+ /** `ensure` treats a heartbeat older than this as "not running" (three missed beats). */
32
+ export const SERVICE_STALE_MS = 35_000;
33
+ /** A live pid whose heartbeat is this old is hung: `ensure` ends it so the next tick restarts it. */
34
+ export const SERVICE_HUNG_MS = 180_000;
35
+ export const CRASH_WINDOW_MS = 60 * 60_000;
36
+ /** Crash restarts allowed in CRASH_WINDOW_MS; the next one gives up. */
37
+ export const MAX_CRASH_RESTARTS = 5;
38
+ export const serviceDir = (home) => join(home, 'autotrade', 'service');
39
+ export const serviceLogDir = (home) => join(home, 'autotrade', 'logs');
40
+ export const serviceLockPath = (home) => join(serviceDir(home), 'service.lock');
41
+ export const serviceHeartbeatPath = (home) => join(serviceDir(home), 'heartbeat.json');
42
+ export const serviceStopPath = (home) => join(serviceDir(home), 'STOP');
43
+ export const serviceStartsPath = (home) => join(serviceDir(home), 'starts.json');
44
+ export const serviceStatePath = (home) => join(serviceDir(home), 'state.json');
45
+ export const serviceLogPath = (home) => join(serviceLogDir(home), 'service.log');
46
+ const mkdirs = (home) => { mkdirSync(serviceDir(home), { recursive: true, mode: 0o700 }); };
47
+ const readJson = (path) => { try {
48
+ return JSON.parse(readFileSync(path, 'utf8'));
49
+ }
50
+ catch {
51
+ return null;
52
+ } };
53
+ export const pidAlive = (pid) => {
54
+ if (!Number.isInteger(pid) || pid <= 0)
55
+ return false;
56
+ try {
57
+ process.kill(pid, 0);
58
+ return true;
59
+ }
60
+ catch (e) {
61
+ return e.code === 'EPERM';
62
+ }
63
+ };
64
+ /** This boot's start, rounded to the minute (a pid from an older boot is never this process). */
65
+ export const bootAt = () => Math.round((Date.now() - uptime() * 1000) / 60_000) * 60_000;
66
+ /** Take the service lock (O_EXCL). A holder is stale only when its pid is dead or it is from an older boot. */
67
+ export function acquireServiceLock(home, d = {}) {
68
+ mkdirs(home);
69
+ const path = serviceLockPath(home);
70
+ const alive = d.alive ?? pidAlive;
71
+ const lock = { pid: d.pid ?? process.pid, bootAt: d.boot ?? bootAt(), startedAt: d.now ?? Date.now() };
72
+ let reclaimed = null;
73
+ for (let attempt = 0; attempt < 2; attempt++) {
74
+ try {
75
+ const fd = openSync(path, 'wx', 0o600);
76
+ writeSync(fd, `${JSON.stringify(lock)}\n`);
77
+ closeSync(fd);
78
+ return { ok: true, lock, reclaimed };
79
+ }
80
+ catch (e) {
81
+ if (e.code !== 'EEXIST')
82
+ throw e;
83
+ }
84
+ const held = readJson(path);
85
+ if (held && typeof held.pid === 'number' && alive(held.pid) && Math.abs((held.bootAt ?? 0) - lock.bootAt) <= 5 * 60_000)
86
+ return { ok: false, holder: held };
87
+ reclaimed = held;
88
+ rmSync(path, { force: true });
89
+ }
90
+ throw new Error(`could not take ${path}`);
91
+ }
92
+ export function releaseServiceLock(home, pid = process.pid) {
93
+ const held = readJson(serviceLockPath(home));
94
+ if (held && held.pid === pid)
95
+ rmSync(serviceLockPath(home), { force: true });
96
+ }
97
+ /** The lock holder when its pid is alive (and from this boot), else null. */
98
+ export function liveServiceHolder(home, alive = pidAlive, boot = bootAt()) {
99
+ const held = readJson(serviceLockPath(home));
100
+ if (!held || typeof held.pid !== 'number' || !alive(held.pid))
101
+ return null;
102
+ return Math.abs((held.bootAt ?? 0) - boot) <= 5 * 60_000 ? held : null;
103
+ }
104
+ export function writeServiceHeartbeat(home, hb) {
105
+ mkdirs(home);
106
+ writeJsonAtomic(serviceHeartbeatPath(home), hb);
107
+ }
108
+ export function readServiceHeartbeat(home) {
109
+ const d = readJson(serviceHeartbeatPath(home));
110
+ return d && d.schema === SERVICE_SCHEMA && typeof d.pid === 'number' && typeof d.heartbeatAt === 'number' ? d : null;
111
+ }
112
+ export function writeStopMarker(home, by, reason, now = Date.now()) {
113
+ mkdirs(home);
114
+ const m = { at: now, by, reason: reason.slice(0, 200) };
115
+ writeJsonAtomic(serviceStopPath(home), m);
116
+ return m;
117
+ }
118
+ /** The stop marker, or null. A file that is not our JSON (a plain `touch`) is still a stop. */
119
+ export function readStopMarker(home) {
120
+ const p = serviceStopPath(home);
121
+ if (!existsSync(p))
122
+ return null;
123
+ const d = readJson(p);
124
+ if (d && typeof d.at === 'number')
125
+ return { at: d.at, by: (d.by ?? 'file'), reason: typeof d.reason === 'string' ? d.reason : 'stop' };
126
+ let at = 0;
127
+ try {
128
+ at = statSync(p).mtimeMs;
129
+ }
130
+ catch { /* raced */ }
131
+ return { at, by: 'file', reason: 'the STOP file is present' };
132
+ }
133
+ export const clearStopMarker = (home) => { rmSync(serviceStopPath(home), { force: true }); };
134
+ const EMPTY_STARTS = { crashStarts: [], lastStartAt: null, attention: null, downNotifiedAt: null };
135
+ export function readStarts(home) {
136
+ const d = readJson(serviceStartsPath(home));
137
+ if (!d)
138
+ return { ...EMPTY_STARTS, crashStarts: [] };
139
+ return {
140
+ crashStarts: Array.isArray(d.crashStarts) ? d.crashStarts.filter((t) => typeof t === 'number') : [],
141
+ lastStartAt: typeof d.lastStartAt === 'number' ? d.lastStartAt : null,
142
+ attention: d.attention && typeof d.attention.at === 'number' ? { at: d.attention.at, why: String(d.attention.why ?? ''), notified: !!d.attention.notified } : null,
143
+ downNotifiedAt: typeof d.downNotifiedAt === 'number' ? d.downNotifiedAt : null,
144
+ };
145
+ }
146
+ export function writeStarts(home, d) { mkdirs(home); writeJsonAtomic(serviceStartsPath(home), d); }
147
+ /** Wait before the n-th crash restart within the window (1-based): 0, 0, 1, 2, 4 minutes. */
148
+ export function crashBackoffMs(n) { return n <= 2 ? 0 : Math.min(15, 2 ** (n - 3)) * 60_000; }
149
+ /** `service status --clear`: forget the crash history and lift the give-up. */
150
+ export function clearAttention(home) {
151
+ const d = { ...readStarts(home), crashStarts: [], attention: null };
152
+ writeStarts(home, d);
153
+ return d;
154
+ }
155
+ /** True when the last run did not end cleanly (a heartbeat that is not `stopped`, with no live holder). */
156
+ export function lastRunCrashed(hb) { return !!hb && hb.state !== 'stopped'; }
157
+ /**
158
+ * What one scheduler tick does. Pure: the caller reads the files and carries the action out (startService,
159
+ * the kill, the notification, writeStarts).
160
+ */
161
+ export function ensureDecision(i) {
162
+ if (i.holder) {
163
+ const age = i.heartbeat && i.heartbeat.pid === i.holder.pid ? i.now - i.heartbeat.heartbeatAt : i.now - i.holder.startedAt;
164
+ if (age <= SERVICE_STALE_MS)
165
+ return { action: 'noop', why: `running (pid ${i.holder.pid}, heartbeat ${Math.round(age / 1000)} s old)` };
166
+ if (age >= SERVICE_HUNG_MS)
167
+ return { action: 'kill-hung', pid: i.holder.pid, why: `pid ${i.holder.pid} is alive but its heartbeat is ${Math.round(age / 1000)} s old (hung)` };
168
+ return { action: 'noop', why: `pid ${i.holder.pid} is alive; heartbeat ${Math.round(age / 1000)} s old (starting or busy)` };
169
+ }
170
+ if (i.stop)
171
+ return { action: 'noop', why: `stopped (${i.stop.reason}); \`dexterm autotrade service start\` starts it again` };
172
+ if (i.starts.attention)
173
+ return { action: 'give-up', why: i.starts.attention.why, notify: !i.starts.attention.notified };
174
+ if (i.panic)
175
+ return { action: 'noop', why: `the panic switch is engaged (${i.panic}); nothing starts until \`dexterm wallet resume\`` };
176
+ const crashed = lastRunCrashed(i.heartbeat);
177
+ if (!crashed)
178
+ return { action: 'start', crashed: false, why: i.heartbeat ? 'the service is not running' : 'first start', notifyDown: false };
179
+ const recent = i.starts.crashStarts.filter((t) => i.now - t < CRASH_WINDOW_MS);
180
+ if (recent.length >= MAX_CRASH_RESTARTS) {
181
+ return { action: 'give-up', why: `crash loop: ${recent.length + 1} crashes within an hour; the service stays down until \`dexterm autotrade service status --clear\``, notify: true };
182
+ }
183
+ const wait = crashBackoffMs(recent.length + 1);
184
+ const last = recent.length ? Math.max(...recent) : 0;
185
+ if (last && i.now - last < wait)
186
+ return { action: 'wait', why: `crash ${recent.length + 1} within the hour: backing off ${Math.round(wait / 60_000)} min`, untilMs: last + wait };
187
+ const notifyDown = !i.starts.downNotifiedAt || i.now - i.starts.downNotifiedAt >= CRASH_WINDOW_MS;
188
+ return { action: 'start', crashed: true, why: `the last run ended without a clean stop (pid ${i.heartbeat.pid}); restarting`, notifyDown };
189
+ }
190
+ /** Record what a decision did in starts.json (the caller does the process work). */
191
+ export function applyEnsure(home, d, now) {
192
+ const s = readStarts(home);
193
+ s.crashStarts = s.crashStarts.filter((t) => now - t < CRASH_WINDOW_MS);
194
+ if (d.action === 'start') {
195
+ s.lastStartAt = now;
196
+ if (d.crashed)
197
+ s.crashStarts.push(now);
198
+ if (d.notifyDown)
199
+ s.downNotifiedAt = now;
200
+ }
201
+ else if (d.action === 'give-up') {
202
+ s.attention = { at: s.attention?.at ?? now, why: d.why, notified: true };
203
+ }
204
+ writeStarts(home, s);
205
+ return s;
206
+ }
207
+ export function writeServiceState(home, d) { mkdirs(home); writeJsonAtomic(serviceStatePath(home), d); }
208
+ export function readServiceState(home) {
209
+ const d = readJson(serviceStatePath(home));
210
+ return d && typeof d.startedAt === 'number' ? { fleetId: typeof d.fleetId === 'string' ? d.fleetId : null, startedAt: d.startedAt, pid: Number(d.pid) || 0 } : null;
211
+ }
212
+ // ───────────────────────────── the bounded log ─────────────────────────────
213
+ export const LOG_MAX_BYTES = 4 * 1024 * 1024;
214
+ /**
215
+ * An append-only log capped at maxBytes: past the cap the file moves to `.1` (replacing the older one) and a new
216
+ * file starts, so the log never holds more than about 2 × maxBytes. Each line is stamped; write errors are swallowed
217
+ * (a full disk must never stop the exits).
218
+ */
219
+ export class BoundedLog {
220
+ path;
221
+ max;
222
+ size;
223
+ constructor(path, maxBytes = LOG_MAX_BYTES) {
224
+ this.path = path;
225
+ this.max = maxBytes;
226
+ try {
227
+ mkdirSync(join(path, '..'), { recursive: true, mode: 0o700 });
228
+ }
229
+ catch { /* reported on write */ }
230
+ try {
231
+ this.size = statSync(path).size;
232
+ }
233
+ catch {
234
+ this.size = 0;
235
+ }
236
+ if (this.size >= this.max)
237
+ this.rotate();
238
+ }
239
+ rotate() {
240
+ try {
241
+ renameSync(this.path, `${this.path}.1`);
242
+ }
243
+ catch { /* nothing to move */ }
244
+ this.size = 0;
245
+ }
246
+ line(s, now = Date.now()) {
247
+ const text = `${new Date(now).toISOString()} ${s.replace(/\n+$/, '')}\n`;
248
+ const n = Buffer.byteLength(text);
249
+ if (this.size + n > this.max)
250
+ this.rotate();
251
+ try {
252
+ appendFileSync(this.path, text, { mode: 0o600 });
253
+ this.size += n;
254
+ }
255
+ catch { /* never the service's problem */ }
256
+ }
257
+ /** A stream-like face for code that writes chunks (the fleet's stdout / stderr). */
258
+ writer(prefix = '') {
259
+ let buf = '';
260
+ return {
261
+ write: (s) => {
262
+ buf += s;
263
+ let i;
264
+ while ((i = buf.indexOf('\n')) >= 0) {
265
+ this.line(prefix + buf.slice(0, i));
266
+ buf = buf.slice(i + 1);
267
+ }
268
+ if (buf.length > 8192) {
269
+ this.line(prefix + buf);
270
+ buf = '';
271
+ }
272
+ return true;
273
+ },
274
+ };
275
+ }
276
+ }
277
+ /** The last n lines of the service log (and the rotated one before it when short). */
278
+ export function tailServiceLog(home, n = 50) {
279
+ const read = (p) => { try {
280
+ return readFileSync(p, 'utf8').split('\n').filter(Boolean);
281
+ }
282
+ catch {
283
+ return [];
284
+ } };
285
+ const cur = read(serviceLogPath(home));
286
+ if (cur.length >= n)
287
+ return cur.slice(-n);
288
+ return [...read(`${serviceLogPath(home)}.1`), ...cur].slice(-n);
289
+ }