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,484 @@
1
+ // dexterm autotrade chat — approve-to-trade (task-1527, AUTOTRADE-SPEC §8.4 tier 3, §8.5). PAPER today.
2
+ //
3
+ // The runner's clamped entry becomes a PROPOSAL: a card with [Approve] [Half size] [Skip]. ONLY a button callback
4
+ // can approve; free text never reaches this file. Each callback is bound to a one-time nonce (128 bits, only its
5
+ // hash is stored until it is consumed), the proposal hash (sha256 of the canonical order fields + price + liquidity
6
+ // + expiry + network), the local-clock expiry, the paired sender and the private chat. The same store binds the
7
+ // command confirms ([Yes, flatten], [Yes, switch]) and the [Sell] buttons of /positions.
8
+ //
9
+ // proposed ──sent──▶ OPEN ──[Approve]/[Half]──▶ VERIFYING ──checks pass──▶ APPROVED ──fill──▶ FILLED
10
+ // │ send failed │ [Skip] ▶ SKIPPED │ price moved > tol / liq −10 % / impact > cap
11
+ // ▼ │ expiry ▶ EXPIRED ├──▶ REPRICED: new numbers, hash, nonce, expiry ▶ OPEN (once)
12
+ // UNDELIVERED │ └──▶ REFUSED (re-clamp or the port's caps say no)
13
+ // (= skip) └ kill / pause / halt ▶ CANCELLED every terminal state: buttons removed
14
+ //
15
+ // Verification of a tap, in order; any failure is refused, audited and answered with a toast:
16
+ // 1. the update came from this connector's own poll (the transport has no inbound endpoint), and the connector
17
+ // reports no conflict (a webhook or a second poller on the token stops every approval);
18
+ // 2. the sender is paired with the `approve` tier, the chat is private and is the proposal's chat;
19
+ // 3. the record is OPEN, now < expiresAt on the LOCAL clock, and the nonce matches: it is consumed in one SQLite
20
+ // UPDATE … WHERE state = 'OPEN' AND nonce_hash = ?, so a double tap, a second device or a redelivered update
21
+ // gets "already handled";
22
+ // 4. the action is A, H or S (Half only ever halves);
23
+ // 5. re-quote: price moved more than the tolerance, liquidity fell more than 10 %, or impact over the cap →
24
+ // REPRICED once with fresh numbers, a fresh nonce and a fresh expiry; a second move → EXPIRED;
25
+ // 6. re-clamp against the book as it is now (the size can shrink, or the entry is refused);
26
+ // 7. the execution port's confirm for exactly this order, with `remote-button` evidence; its single-use proof
27
+ // rides with the order to the executor. A live port checks its own caps there, whatever this file passed.
28
+ //
29
+ // Restarts expire every open record (never resurrected). Exits never wait on any of this.
30
+ import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
31
+ import { mkdirSync } from 'node:fs';
32
+ import { dirname, join } from 'node:path';
33
+ import { DatabaseSync } from 'node:sqlite';
34
+ import { canonical } from "../strategy.js";
35
+ import { badgeOf, orderHash } from "../execution-port.js";
36
+ import { allowed } from "./config.js";
37
+ import { inert, price, shortMint, usdShort } from "./format.js";
38
+ const TERMINAL = ['FILLED', 'SKIPPED', 'EXPIRED', 'REFUSED', 'CANCELLED', 'UNDELIVERED', 'CONFIRMED', 'DECLINED'];
39
+ const sha = (s) => createHash('sha256').update(s).digest('hex');
40
+ // ───────────────────────────── the store (SQLite) ─────────────────────────────
41
+ /** $DEXTERM_HOME/autotrade/chat/callbacks.db, or ':memory:' for tests. */
42
+ export const callbackDbPath = (home) => join(home, 'autotrade', 'chat', 'callbacks.db');
43
+ export class CallbackStore {
44
+ db;
45
+ constructor(path) {
46
+ if (path !== ':memory:')
47
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
48
+ this.db = new DatabaseSync(path);
49
+ this.db.exec(`PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 2000;
50
+ CREATE TABLE IF NOT EXISTS callbacks (
51
+ id TEXT PRIMARY KEY, kind TEXT NOT NULL, hash TEXT NOT NULL, nonce_hash TEXT, chat_id TEXT NOT NULL, sender_id TEXT NOT NULL,
52
+ network TEXT NOT NULL, created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, state TEXT NOT NULL, repriced INTEGER NOT NULL DEFAULT 0,
53
+ message_id TEXT, payload TEXT NOT NULL, updated_at INTEGER NOT NULL)`);
54
+ }
55
+ insert(r, nonceHash, now) {
56
+ this.db.prepare('INSERT INTO callbacks (id, kind, hash, nonce_hash, chat_id, sender_id, network, created_at, expires_at, state, repriced, message_id, payload, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)')
57
+ .run(r.id, r.kind, r.hash, nonceHash, r.chatId, r.senderId, r.network, r.createdAt, r.expiresAt, r.state, r.repriced, r.messageId, JSON.stringify(r.payload), now);
58
+ }
59
+ get(id) {
60
+ const row = this.db.prepare('SELECT * FROM callbacks WHERE id = ?').get(id);
61
+ if (!row)
62
+ return null;
63
+ return {
64
+ id: String(row.id), kind: row.kind, hash: String(row.hash), nonceHash: row.nonce_hash === null ? null : String(row.nonce_hash), chatId: String(row.chat_id), senderId: String(row.sender_id),
65
+ network: String(row.network), createdAt: Number(row.created_at), expiresAt: Number(row.expires_at), state: row.state, repriced: Number(row.repriced),
66
+ messageId: row.message_id === null ? null : String(row.message_id), payload: JSON.parse(String(row.payload)),
67
+ };
68
+ }
69
+ /** Consume the nonce: OPEN → VERIFYING only if it is still OPEN and the nonce matches. True for exactly one caller. */
70
+ consume(id, nonceHash, now) {
71
+ const r = this.db.prepare("UPDATE callbacks SET state = 'VERIFYING', nonce_hash = NULL, updated_at = ? WHERE id = ? AND state = 'OPEN' AND nonce_hash = ? AND expires_at > ?").run(now, id, nonceHash, now);
72
+ return Number(r.changes) === 1;
73
+ }
74
+ setState(id, state, now, from) {
75
+ const r = from?.length
76
+ ? this.db.prepare(`UPDATE callbacks SET state = ?, nonce_hash = CASE WHEN ? THEN NULL ELSE nonce_hash END, updated_at = ? WHERE id = ? AND state IN (${from.map(() => '?').join(',')})`).run(state, TERMINAL.includes(state) ? 1 : 0, now, id, ...from)
77
+ : this.db.prepare('UPDATE callbacks SET state = ?, nonce_hash = CASE WHEN ? THEN NULL ELSE nonce_hash END, updated_at = ? WHERE id = ?').run(state, TERMINAL.includes(state) ? 1 : 0, now, id);
78
+ return Number(r.changes) === 1;
79
+ }
80
+ /** A REPRICED record goes back to OPEN once, with new numbers, hash, nonce and expiry. */
81
+ reopen(id, hash, nonceHash, expiresAt, payload, now) {
82
+ const r = this.db.prepare("UPDATE callbacks SET state = 'OPEN', hash = ?, nonce_hash = ?, expires_at = ?, payload = ?, repriced = repriced + 1, updated_at = ? WHERE id = ? AND state = 'REPRICED' AND repriced = 0").run(hash, nonceHash, expiresAt, JSON.stringify(payload), now, id);
83
+ return Number(r.changes) === 1;
84
+ }
85
+ setMessage(id, messageId) { this.db.prepare('UPDATE callbacks SET message_id = ? WHERE id = ?').run(messageId, id); }
86
+ open() {
87
+ return this.db.prepare("SELECT id FROM callbacks WHERE state IN ('OPEN', 'VERIFYING', 'REPRICED', 'APPROVED')").all().map((x) => this.get(x.id)).filter(Boolean);
88
+ }
89
+ /** States by count for today (the daily summary's approval line). */
90
+ countsSince(t) {
91
+ const out = {};
92
+ for (const r of this.db.prepare("SELECT state, COUNT(*) AS n FROM callbacks WHERE kind = 'proposal' AND created_at >= ? GROUP BY state").all(t))
93
+ out[r.state] = Number(r.n);
94
+ return out;
95
+ }
96
+ close() { try {
97
+ this.db.close();
98
+ }
99
+ catch { /* closed */ } }
100
+ }
101
+ // ───────────────────────────── callback data ─────────────────────────────
102
+ /** `p:<id8>:<nonce22>:<A|H|S>` (36 bytes) and `c:<id8>:<nonce22>:<Y|N>`: well under Telegram's 64-byte limit. */
103
+ export function encodeCallback(kind, id, nonce, action) { return `${kind}:${id}:${nonce}:${action}`; }
104
+ export function decodeCallback(data) {
105
+ const m = /^([pc]):([A-Za-z0-9_-]{8}):([A-Za-z0-9_-]{22}):([A-Z])$/.exec(data);
106
+ return m ? { kind: m[1], id: m[2], nonce: m[3], action: m[4] } : null;
107
+ }
108
+ const newId = () => randomBytes(6).toString('base64url');
109
+ const newNonce = () => randomBytes(16).toString('base64url');
110
+ const nonceHashOf = (nonce) => sha(`dexterm-chat-nonce:${nonce}`);
111
+ /** The proposal hash (§8.5): sha256 of the canonical order + the numbers it was shown with. Full 64 hex: the live
112
+ * signer's remote evidence requires it. It never rides in callback_data (that carries id + nonce only). */
113
+ export function proposalHash(f) {
114
+ return sha(canonical(f));
115
+ }
116
+ const LIQ_DROP_PCT = 10;
117
+ export class ApprovalBroker {
118
+ o;
119
+ now;
120
+ waiters = new Map();
121
+ timers = new Map();
122
+ confirmHandlers = new Map();
123
+ skippedOverCap = 0;
124
+ constructor(o) {
125
+ this.o = o;
126
+ this.now = o.now ?? Date.now;
127
+ }
128
+ get badge() { return badgeOf(this.o.port.network); }
129
+ get overCapSkips() { return this.skippedOverCap; }
130
+ /** At start: every record left open by a previous process is expired (never resurrected). */
131
+ async expireLeftovers() {
132
+ const left = this.o.store.open();
133
+ for (const r of left) {
134
+ this.o.store.setState(r.id, 'EXPIRED', this.now());
135
+ this.o.audit.add(this.now(), 'callback_state', { id: r.id, kind: r.kind, hash: r.hash, state: 'EXPIRED', why: 'restart' });
136
+ if (r.messageId)
137
+ await this.editQuiet({ chatId: r.chatId, messageId: r.messageId }, { kind: 'text', badge: this.badge, lines: [`${r.kind === 'proposal' ? 'PROPOSAL' : 'CONFIRM'} #${r.id} expired: dexterm restarted.`] });
138
+ }
139
+ return left.length;
140
+ }
141
+ onConfirm(kind, h) { this.confirmHandlers.set(kind, h); }
142
+ required(s) { return s.entry.decide === 'agent+approve' || this.o.config().approvals === 'on'; }
143
+ expiryMs() { return this.o.expiryMs ?? this.o.config().approvalExpirySec * 1000; }
144
+ /** The paired private chat(s) that may approve. */
145
+ approvers() {
146
+ const t = this.o.config();
147
+ return t.allow.filter((a) => a.tiers.includes('approve') && t.tiers.includes('approve') && a.chatId === a.senderId).map((a) => ({ senderId: a.senderId, chatId: a.chatId }));
148
+ }
149
+ conflict() { return this.o.connector.health().conflict; }
150
+ // ── proposals ──
151
+ async request(p, live) {
152
+ const now = this.now();
153
+ const openNow = [...this.waiters.keys()].length;
154
+ if (openNow >= (this.o.maxOpen ?? 2)) {
155
+ this.skippedOverCap++;
156
+ this.o.audit.add(now, 'proposal_skipped', { mint: p.mint, why: 'max-open-proposals', open: openNow });
157
+ return { approved: false, state: 'SKIPPED', why: 'too many proposals open at once' };
158
+ }
159
+ const conflict = this.conflict();
160
+ if (conflict) {
161
+ this.o.audit.add(now, 'proposal_skipped', { mint: p.mint, why: 'connector-conflict' });
162
+ return { approved: false, state: 'UNDELIVERED', why: `approvals are stopped: ${conflict}` };
163
+ }
164
+ const to = this.approvers()[0];
165
+ if (!to)
166
+ return { approved: false, state: 'UNDELIVERED', why: 'no paired private chat with the approve tier' };
167
+ const id = newId();
168
+ const nonce = newNonce();
169
+ const expiresAt = now + this.expiryMs();
170
+ const payload = this.payloadOf(p, p.usd, p.price, p.liq, p.impactPct, expiresAt);
171
+ const hash = proposalHash(this.hashFields(payload));
172
+ const rec = { id, kind: 'proposal', hash, chatId: to.chatId, senderId: to.senderId, network: this.o.port.network, createdAt: now, expiresAt, state: 'OPEN', repriced: 0, messageId: null, payload };
173
+ this.o.store.insert(rec, nonceHashOf(nonce), now);
174
+ this.o.audit.add(now, 'callback_state', { id, kind: 'proposal', hash, nonceHash: nonceHashOf(nonce).slice(0, 16), state: 'OPEN', mint: p.mint, usd: p.usd, expiresAt });
175
+ const out = new Promise((resolve) => { this.waiters.set(id, { resolve, proposal: p, live, strategy: { maxSlipPct: p.slippageBps / 100 } }); });
176
+ try {
177
+ const ref = await this.o.connector.send({ chatId: to.chatId }, this.card(id, nonce, payload, 0));
178
+ this.o.store.setMessage(id, ref.messageId);
179
+ this.o.audit.add(this.now(), 'out', { template: 'proposal', id, messageId: ref.messageId, chatId: to.chatId });
180
+ this.arm(id, expiresAt);
181
+ }
182
+ catch (e) {
183
+ this.finish(id, 'UNDELIVERED', `the proposal could not be sent (${e.message.slice(0, 80)})`, null);
184
+ }
185
+ return out;
186
+ }
187
+ payloadOf(p, usd, px, liq, impactPct, expiresAt) {
188
+ return {
189
+ mint: p.mint, symbol: p.symbol, strategyId: p.strategy.id, strategyVersion: p.strategy.version, strategyHash: p.strategy.hash, profileId: p.profileId,
190
+ usd, firstTrancheUsd: p.usd > 0 ? (p.firstTrancheUsd * usd) / p.usd : 0, pctOfBook: p.bookUsd > 0 ? Math.round((usd / p.bookUsd) * 1000) / 10 : 0,
191
+ price: px, liq, impactPct, slippageBps: p.slippageBps, maxImpactPct: p.maxImpactPct, exitPlan: p.exitPlan, reason: p.reason, runId: p.runId, expiresAt, clamps: p.clamps,
192
+ };
193
+ }
194
+ hashFields(x) {
195
+ return { mint: String(x.mint), side: 'buy', usd: Number(x.usd), slippageBps: Number(x.slippageBps), maxImpactPct: Number(x.maxImpactPct), strategyHash: String(x.strategyHash), profileId: String(x.profileId), priceAtProposal: x.price ?? null, liqAtProposal: x.liq ?? null, expiresAt: Number(x.expiresAt), network: this.o.port.network };
196
+ }
197
+ /** The proposal card. Every number is engine state; the symbol and the reason are inert quoted data. */
198
+ card(id, nonce, x, repriced) {
199
+ const secs = Math.max(0, Math.round((Number(x.expiresAt) - this.now()) / 1000));
200
+ const lines = [
201
+ `${repriced ? 'PROPOSAL (re-quoted, confirm again)' : 'PROPOSAL'} · ${inert(x.strategyId, 32)} v${inert(x.strategyVersion, 12)} (${String(x.strategyHash).slice(0, 8)})`,
202
+ `token "${inert(x.symbol, 20)}" ${shortMint(String(x.mint))}`,
203
+ `buy ${usdShort(Number(x.usd))} (${x.pctOfBook}% of book) @ ${price(x.price)} · liq ${usdShort(x.liq)} · impact ~${Number(x.impactPct).toFixed(2)}%`,
204
+ `plan: ${inert(x.exitPlan, 120)}`,
205
+ `why (the agent's words, unverified): "${inert(x.reason, 300, 'agent')}"`,
206
+ `expires in ${secs} s · #${id}`,
207
+ 'Only these buttons approve. Replies are ignored.',
208
+ ];
209
+ return {
210
+ kind: 'proposal', badge: this.badge, lines, expiresAt: Number(x.expiresAt),
211
+ buttons: [{ label: 'Approve', data: encodeCallback('p', id, nonce, 'A') }, { label: 'Half size', data: encodeCallback('p', id, nonce, 'H') }, { label: 'Skip', data: encodeCallback('p', id, nonce, 'S') }],
212
+ };
213
+ }
214
+ arm(id, expiresAt) {
215
+ if (this.o.timers === false)
216
+ return;
217
+ const old = this.timers.get(id);
218
+ if (old)
219
+ clearTimeout(old);
220
+ const t = setTimeout(() => { void this.sweep(); }, Math.max(0, expiresAt - this.now()) + 50);
221
+ t.unref?.();
222
+ this.timers.set(id, t);
223
+ }
224
+ /** Expire every record past its expiry (the timers call it; tests call it with a moved clock). */
225
+ async sweep() {
226
+ const now = this.now();
227
+ for (const r of this.o.store.open()) {
228
+ if (r.state === 'OPEN' && now >= r.expiresAt) {
229
+ if (r.kind === 'proposal')
230
+ await this.finish(r.id, 'EXPIRED', 'no answer before the expiry (expired means skip)', null);
231
+ else
232
+ await this.finishConfirm(r, 'EXPIRED', 'expired');
233
+ }
234
+ }
235
+ }
236
+ /** Terminal state for a proposal: store, audit, edit the card (buttons removed), resolve the runner's request. */
237
+ async finish(id, state, why, approved) {
238
+ const t = this.timers.get(id);
239
+ if (t) {
240
+ clearTimeout(t);
241
+ this.timers.delete(id);
242
+ }
243
+ const rec = this.o.store.get(id);
244
+ if (!rec)
245
+ return;
246
+ // APPROVED was set by the tap (from VERIFYING); every other end state wins only over a live state, once.
247
+ if (!approved && !this.o.store.setState(id, state, this.now(), ['OPEN', 'VERIFYING', 'REPRICED']))
248
+ return;
249
+ this.o.audit.add(this.now(), 'callback_state', { id, kind: 'proposal', hash: rec.hash, state, why, ...(approved ? { usd: approved.usd, proofId: `${approved.proofId.slice(0, 6)}…` } : {}) });
250
+ const w = this.waiters.get(id);
251
+ this.waiters.delete(id);
252
+ const sym = inert(rec.payload.symbol, 20);
253
+ const line = approved ? `APPROVED "${sym}" ${usdShort(approved.usd)} · the order goes to the ${this.o.port.network} executor · #${id}` : `${state} "${sym}" · ${why} · #${id}`;
254
+ if (rec.messageId)
255
+ await this.editQuiet({ chatId: rec.chatId, messageId: rec.messageId }, { kind: 'text', badge: this.badge, lines: [line] });
256
+ if (w)
257
+ w.resolve(approved ? { approved: true, proposalId: id, usd: approved.usd, proofId: approved.proofId, orderHash: approved.orderHash, via: approved.via } : { approved: false, proposalId: id, state, why });
258
+ }
259
+ async editQuiet(ref, msg) {
260
+ try {
261
+ await this.o.connector.edit(ref, msg);
262
+ }
263
+ catch (e) {
264
+ this.o.audit.add(this.now(), 'edit_failed', { messageId: ref.messageId, error: e.message.slice(0, 120) });
265
+ }
266
+ }
267
+ /** The runner filled an approved order (from the run ledger's fill event). */
268
+ filled(proposalId) {
269
+ if (this.o.store.setState(proposalId, 'FILLED', this.now(), ['APPROVED']))
270
+ this.o.audit.add(this.now(), 'callback_state', { id: proposalId, kind: 'proposal', state: 'FILLED' });
271
+ }
272
+ cancelAll(reason) {
273
+ for (const id of [...this.waiters.keys()])
274
+ void this.finish(id, 'CANCELLED', `cancelled: ${reason}`, null);
275
+ // A strategy switch waiting for its confirm is cancelled too; a flatten or a sell confirm (both safer) is not.
276
+ for (const r of this.o.store.open())
277
+ if (r.kind === 'use' && r.state === 'OPEN')
278
+ void this.finishConfirm(r, 'CANCELLED', `cancelled: ${reason}`);
279
+ }
280
+ // ── confirms (flatten, use, sell) ──
281
+ /** A confirm card bound like a proposal. `payload` must be engine data only. */
282
+ async confirm(kind, to, lines, yes, payload, ttlMs = 60_000) {
283
+ const now = this.now();
284
+ const id = newId();
285
+ const nonce = newNonce();
286
+ const expiresAt = now + ttlMs;
287
+ const hash = sha(canonical({ kind, ...payload, expiresAt })).slice(0, 32);
288
+ this.o.store.insert({ id, kind, hash, chatId: to.chatId, senderId: to.senderId, network: this.o.port.network, createdAt: now, expiresAt, state: 'OPEN', repriced: 0, messageId: null, payload }, nonceHashOf(nonce), now);
289
+ this.o.audit.add(now, 'callback_state', { id, kind, hash, nonceHash: nonceHashOf(nonce).slice(0, 16), state: 'OPEN', expiresAt });
290
+ try {
291
+ const ref = await this.o.connector.send({ chatId: to.chatId }, { kind: 'proposal', badge: this.badge, lines: [...lines, `expires in ${Math.round(ttlMs / 1000)} s · #${id}`], expiresAt, buttons: [{ label: yes, data: encodeCallback('c', id, nonce, 'Y') }, { label: 'No', data: encodeCallback('c', id, nonce, 'N') }] });
292
+ this.o.store.setMessage(id, ref.messageId);
293
+ this.arm(id, expiresAt);
294
+ return id;
295
+ }
296
+ catch {
297
+ this.o.store.setState(id, 'UNDELIVERED', this.now());
298
+ return null;
299
+ }
300
+ }
301
+ async finishConfirm(r, state, line) {
302
+ const t = this.timers.get(r.id);
303
+ if (t) {
304
+ clearTimeout(t);
305
+ this.timers.delete(r.id);
306
+ }
307
+ if (!this.o.store.setState(r.id, state, this.now(), ['OPEN', 'VERIFYING']))
308
+ return;
309
+ this.o.audit.add(this.now(), 'callback_state', { id: r.id, kind: r.kind, hash: r.hash, state, why: line.slice(0, 160) });
310
+ if (r.messageId)
311
+ await this.editQuiet({ chatId: r.chatId, messageId: r.messageId }, { kind: 'text', badge: this.badge, lines: [`${line} · #${r.id}`] });
312
+ }
313
+ // ── a tap ──
314
+ /** Verify and act on one button callback. Returns the toast (also sent with ackAction). */
315
+ async handleAction(a) {
316
+ const now = this.now();
317
+ const toast = async (s, result, extra = {}) => {
318
+ this.o.audit.add(this.now(), 'verify', { updateId: a.updateId, senderId: a.senderId, chatId: a.chatId, result, ...extra });
319
+ await this.o.connector.ackAction(a, s);
320
+ return s;
321
+ };
322
+ const cb = decodeCallback(a.data);
323
+ if (!cb)
324
+ return toast('Not a dexterm button.', 'refused:malformed');
325
+ const rec = this.o.store.get(cb.id);
326
+ if (!rec)
327
+ return toast('Unknown or old button.', 'refused:unknown-id', { id: cb.id });
328
+ const ctx = { id: rec.id, kind: rec.kind };
329
+ // 1. own poll (by construction) and no conflict on the token.
330
+ const conflict = this.conflict();
331
+ if (conflict)
332
+ return toast('Approvals are stopped on this bot (a token conflict). Use dexterm locally.', 'refused:connector-conflict', ctx);
333
+ // 2. sender, tier, private chat, the record's chat.
334
+ const tier = rec.kind === 'proposal' ? 'approve' : 'control';
335
+ if (a.chatKind !== 'private')
336
+ return toast('Buttons work only in your private chat with the bot.', 'refused:group-chat', ctx);
337
+ if (!allowed(this.o.config(), a.senderId, a.chatId, tier))
338
+ return toast('This account is not paired for that.', 'refused:sender-not-allowed', ctx);
339
+ if (a.chatId !== rec.chatId || a.senderId !== rec.senderId || (rec.messageId !== null && a.ref.messageId !== rec.messageId))
340
+ return toast('This button belongs to another chat or message.', 'refused:wrong-chat-or-message', ctx);
341
+ // 3. state, local-clock expiry, nonce (consumed once).
342
+ if (TERMINAL.includes(rec.state) || rec.state === 'APPROVED')
343
+ return toast('Already handled.', `refused:state-${rec.state}`, ctx);
344
+ if (rec.state !== 'OPEN')
345
+ return toast('Already being handled.', `refused:state-${rec.state}`, ctx);
346
+ if (now >= rec.expiresAt) {
347
+ if (rec.kind === 'proposal')
348
+ await this.finish(rec.id, 'EXPIRED', 'the tap arrived after the expiry', null);
349
+ else
350
+ await this.finishConfirm(rec, 'EXPIRED', 'expired');
351
+ return toast('Expired: nothing happened.', 'refused:expired', ctx);
352
+ }
353
+ const nh = nonceHashOf(cb.nonce);
354
+ if (!rec.nonceHash || !safeEq(nh, rec.nonceHash))
355
+ return toast('Old or forged button: nothing happened.', 'refused:nonce', ctx);
356
+ // 4. the action.
357
+ const okActions = rec.kind === 'proposal' ? ['A', 'H', 'S'] : ['Y', 'N'];
358
+ if ((rec.kind === 'proposal') !== (cb.kind === 'p') || !okActions.includes(cb.action))
359
+ return toast('Not a valid choice.', 'refused:action', ctx);
360
+ if (!this.o.store.consume(rec.id, nh, now))
361
+ return toast('Already handled.', 'refused:nonce-consumed', ctx);
362
+ this.o.audit.add(this.now(), 'verify', { updateId: a.updateId, senderId: a.senderId, chatId: a.chatId, result: 'nonce-consumed', ...ctx, action: cb.action });
363
+ if (rec.kind !== 'proposal')
364
+ return this.runConfirm(rec, cb.action, a, toast);
365
+ return this.runProposal(rec, cb.action, a, toast);
366
+ }
367
+ async runConfirm(rec, action, a, toast) {
368
+ if (action === 'N') {
369
+ await this.finishConfirm(rec, 'DECLINED', 'cancelled, nothing changed');
370
+ return toast('Nothing changed.', 'declined', { id: rec.id });
371
+ }
372
+ const h = this.confirmHandlers.get(rec.kind);
373
+ if (!h) {
374
+ await this.finishConfirm(rec, 'REFUSED', 'not available here');
375
+ return toast('Not available.', 'refused:no-handler', { id: rec.id });
376
+ }
377
+ let msg;
378
+ try {
379
+ msg = await h(rec, a);
380
+ }
381
+ catch (e) {
382
+ msg = `refused: ${e.message.slice(0, 120)}`;
383
+ }
384
+ await this.finishConfirm(rec, 'CONFIRMED', msg);
385
+ return toast(msg.slice(0, 180), 'confirmed', { id: rec.id, kind: rec.kind });
386
+ }
387
+ async runProposal(rec, action, a, toast) {
388
+ const w = this.waiters.get(rec.id);
389
+ const ctx = { id: rec.id, action };
390
+ if (!w) {
391
+ await this.finish(rec.id, 'EXPIRED', 'the runner is no longer waiting for it', null);
392
+ return toast('Expired: nothing happened.', 'refused:no-waiter', ctx);
393
+ }
394
+ if (action === 'S') {
395
+ await this.finish(rec.id, 'SKIPPED', 'skipped by tap', null);
396
+ return toast('Skipped.', 'skipped', ctx);
397
+ }
398
+ const blocked = w.live.blocked();
399
+ if (blocked) {
400
+ await this.finish(rec.id, 'CANCELLED', `entries are stopped (${blocked})`, null);
401
+ return toast('Entries are stopped: nothing happened.', 'refused:blocked', { ...ctx, blocked });
402
+ }
403
+ const shownUsd = Number(rec.payload.usd);
404
+ const wantUsd = action === 'H' ? shownUsd / 2 : shownUsd;
405
+ // 5. re-quote.
406
+ const k = w.live.tick();
407
+ const p0 = rec.payload.price;
408
+ const l0 = rec.payload.liq;
409
+ const tol = Math.min(this.o.config().repriceTolPct, w.strategy.maxSlipPct > 0 ? w.strategy.maxSlipPct : this.o.config().repriceTolPct);
410
+ const R = (k?.liq ?? 0) / 2;
411
+ const firstUsd = Number(rec.payload.firstTrancheUsd) * (wantUsd / (shownUsd || 1));
412
+ const impactNow = R > 0 ? (firstUsd / R) * 100 : Infinity;
413
+ const movedPct = k?.price && p0 ? Math.abs(k.price / p0 - 1) * 100 : Infinity;
414
+ const liqDropPct = k?.liq && l0 ? (1 - k.liq / l0) * 100 : Infinity;
415
+ const moved = !(movedPct <= tol) || !(liqDropPct <= LIQ_DROP_PCT) || impactNow > Number(rec.payload.maxImpactPct);
416
+ this.o.audit.add(this.now(), 'verify', { updateId: a.updateId, result: moved ? 'requote-moved' : 'requote-ok', id: rec.id, priceAtProposal: p0, priceNow: k?.price ?? null, movedPct: Number.isFinite(movedPct) ? Math.round(movedPct * 100) / 100 : null, liqDropPct: Number.isFinite(liqDropPct) ? Math.round(liqDropPct * 10) / 10 : null, impactPct: Number.isFinite(impactNow) ? Math.round(impactNow * 100) / 100 : null, tolPct: tol });
417
+ if (moved) {
418
+ if (rec.repriced >= 1 || !k?.price) {
419
+ await this.finish(rec.id, 'EXPIRED', 'the price moved again after a re-quote (expired means skip)', null);
420
+ return toast('The price moved again: skipped.', 'expired:second-move', ctx);
421
+ }
422
+ // REPRICED: fresh numbers, a fresh nonce, a fresh expiry, the same buttons; confirm once more.
423
+ this.o.store.setState(rec.id, 'REPRICED', this.now(), ['VERIFYING']);
424
+ const expiresAt = this.now() + this.expiryMs();
425
+ const re = this.payloadOf(w.proposal, shownUsd, k.price, k.liq ?? null, R > 0 ? Math.round((Number(rec.payload.firstTrancheUsd) / R) * 1e4) / 100 : 0, expiresAt);
426
+ const hash = proposalHash(this.hashFields(re));
427
+ const nonce = newNonce();
428
+ this.o.store.reopen(rec.id, hash, nonceHashOf(nonce), expiresAt, re, this.now());
429
+ this.o.audit.add(this.now(), 'callback_state', { id: rec.id, kind: 'proposal', hash, nonceHash: nonceHashOf(nonce).slice(0, 16), state: 'OPEN', repriced: 1, why: `moved ${Number.isFinite(movedPct) ? movedPct.toFixed(2) : '?'}% (tolerance ${tol}%)` });
430
+ if (rec.messageId)
431
+ await this.editQuiet({ chatId: rec.chatId, messageId: rec.messageId }, this.card(rec.id, nonce, re, 1));
432
+ this.arm(rec.id, expiresAt);
433
+ return toast('The price moved: check the new numbers and tap again.', 'repriced', ctx);
434
+ }
435
+ // 6. re-clamp against the book as it is now.
436
+ const rc = w.live.reclamp(wantUsd);
437
+ if (!rc.ok) {
438
+ await this.finish(rec.id, 'REFUSED', `the strategy's limits refuse it now (${rc.refused})`, null);
439
+ return toast('Refused by your limits: nothing happened.', 'refused:reclamp', { ...ctx, refused: rc.refused });
440
+ }
441
+ const usd = Math.min(rc.usd, wantUsd);
442
+ // 7. the port's confirm (its own caps), with remote-button evidence.
443
+ const order = {
444
+ mint: String(rec.payload.mint), side: 'buy', usd, slippageBps: Number(rec.payload.slippageBps), maxImpactPct: Number(rec.payload.maxImpactPct),
445
+ strategyId: String(rec.payload.strategyId), strategyVersion: String(rec.payload.strategyVersion), strategyHash: String(rec.payload.strategyHash), profileId: String(rec.payload.profileId),
446
+ runId: rec.payload.runId ?? null, network: this.o.port.network,
447
+ };
448
+ const ev = { tier: 'remote-button', transport: a.transport, proposalId: rec.id, proposalHash: rec.hash, senderId: a.senderId, chatId: a.chatId, updateId: a.updateId, at: this.now() };
449
+ const proof = await this.o.port.confirmRemote(order, ev);
450
+ this.o.audit.add(this.now(), 'verify', { updateId: a.updateId, result: proof.ok ? 'port-confirmed' : `port-refused:${proof.refused}`, id: rec.id, usd, clamps: rc.clamps, port: this.o.port.name, proofId: proof.ok ? `${proof.proofId.slice(0, 6)}…` : null });
451
+ if (!proof.ok) {
452
+ await this.finish(rec.id, 'REFUSED', `the ${this.o.port.network} executor refused it (${proof.refused})`, null);
453
+ return toast('Refused by the executor: nothing happened.', 'refused:port', { ...ctx, refused: proof.refused });
454
+ }
455
+ if (!this.o.store.setState(rec.id, 'APPROVED', this.now(), ['VERIFYING']))
456
+ return toast('Cancelled while it was being checked: nothing happened.', 'refused:cancelled-mid-check', ctx);
457
+ await this.finish(rec.id, 'APPROVED', 'approved', { usd, proofId: proof.proofId, orderHash: orderHash(order), via: `${a.transport} button (${action === 'H' ? 'half size' : 'approve'})` });
458
+ return toast(action === 'H' ? `Approved at half size (${usdShort(usd)}).` : `Approved (${usdShort(usd)}).`, 'approved', { ...ctx, usd });
459
+ }
460
+ // ── chat sells (/positions [Sell]) ──
461
+ exits = new Map();
462
+ /** A confirmed [Sell]: the runner takes it at the coin's next tick. */
463
+ queueExit(mint, via) {
464
+ const id = newId();
465
+ this.exits.set(mint, { id, via });
466
+ this.o.audit.add(this.now(), 'exit_queued', { id, mint, via });
467
+ return id;
468
+ }
469
+ takeExit(mint) {
470
+ const x = this.exits.get(mint) ?? null;
471
+ if (x)
472
+ this.exits.delete(mint);
473
+ return x;
474
+ }
475
+ exitResult(id, result) { this.o.audit.add(this.now(), 'exit_result', { id, result }); }
476
+ /** Stop every timer (end of the host). */
477
+ close() { for (const t of this.timers.values())
478
+ clearTimeout(t); this.timers.clear(); }
479
+ }
480
+ function safeEq(a, b) {
481
+ const x = Buffer.from(a);
482
+ const y = Buffer.from(b);
483
+ return x.length === y.length && timingSafeEqual(x, y);
484
+ }
@@ -0,0 +1,140 @@
1
+ // dexterm autotrade chat — the chat audit chain (task-1527, AUTOTRADE-SPEC §8.6).
2
+ //
3
+ // $DEXTERM_HOME/autotrade/chat/<YYYY-MM-DD>.jsonl (0600, the directory 0700), one hash-chained event per line with
4
+ // the run ledger's chain rule (ledger.ts chainHash/verifyChain): editing, dropping or reordering a line breaks the
5
+ // chain from there. A restart on the same day continues the day's chain. The run ledger anchors it: the runner
6
+ // writes a `chat_audit` event (day file, head hash, event count) when a run starts and when it ends, and
7
+ // verifyChatAudit() cross-checks every anchor, so a day file that was truncated past an anchor, or deleted, or
8
+ // re-chained after one, no longer verifies. Not caught: lines added after the last anchor that are then cut (the
9
+ // tail since the last run ended), and a forger who re-chains both files (neither chain is keyed).
10
+ //
11
+ // Recorded: every inbound update (update id, sender id, chat id, the command or the action's proposal id; free text
12
+ // as length + hash only), every outbound message (template id, its fields, the message id), every proposal state
13
+ // change with its hash and the nonce HASH, every verification step with its result.
14
+ // Never recorded: the bot token (every line passes redact() as a last net), keys, raw nonces, balances.
15
+ import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ import { GENESIS, chainHash, readLedgerFile, runsDir, verifyChain } from "../ledger.js";
18
+ import { redact } from "./redact.js";
19
+ export const chatDir = (home) => join(home, 'autotrade', 'chat');
20
+ /** Local calendar day, YYYY-MM-DD. */
21
+ export function localDay(t) {
22
+ const d = new Date(t);
23
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
24
+ }
25
+ /** Keys that must never be written, whatever a caller passes (defence in depth; nothing passes them on purpose). */
26
+ const FORBIDDEN = /^(token|botToken|nonce|secret|key|privateKey|seed|balance|balances)$/i;
27
+ export class ChatAudit {
28
+ home;
29
+ day = '';
30
+ prev = GENESIS;
31
+ seq = 0;
32
+ known;
33
+ /** In-memory copy (tests, and the home-less audit of a dry run). */
34
+ events = [];
35
+ /** home = null keeps the chain in memory only. `knownToken` lets the redactor also catch the exact token. */
36
+ constructor(home, o = {}) {
37
+ this.home = home;
38
+ this.known = o.knownToken ?? (() => null);
39
+ }
40
+ file(day = this.day) { return this.home ? join(chatDir(this.home), `${day}.jsonl`) : null; }
41
+ roll(t) {
42
+ const day = localDay(t);
43
+ if (day === this.day)
44
+ return;
45
+ this.day = day;
46
+ this.prev = GENESIS;
47
+ this.seq = 0;
48
+ const f = this.file(day);
49
+ if (!f)
50
+ return;
51
+ mkdirSync(chatDir(this.home), { recursive: true, mode: 0o700 });
52
+ if (!existsSync(f)) {
53
+ closeSync(openSync(f, 'a', 0o600));
54
+ return;
55
+ }
56
+ // Continue today's chain from its last line.
57
+ const lines = readFileSync(f, 'utf8').split('\n').filter((l) => l.trim());
58
+ const last = lines.length ? JSON.parse(lines[lines.length - 1]) : null;
59
+ if (last && typeof last.hash === 'string' && typeof last.seq === 'number') {
60
+ this.prev = last.hash;
61
+ this.seq = last.seq;
62
+ }
63
+ }
64
+ add(t, kind, data = {}) {
65
+ this.roll(t);
66
+ const clean = {};
67
+ for (const [k, v] of Object.entries(data))
68
+ if (!FORBIDDEN.test(k) && v !== undefined)
69
+ clean[k] = v;
70
+ // The redactor runs over the serialized body; a value that changed is stored redacted (the hash covers that).
71
+ const body = JSON.parse(redact(JSON.stringify(clean), this.known()));
72
+ const e = { ...body, seq: this.seq + 1, t, kind };
73
+ delete e.prev;
74
+ delete e.hash;
75
+ const h = chainHash(this.prev, e);
76
+ e.prev = this.prev;
77
+ e.hash = h;
78
+ this.prev = h;
79
+ this.seq = e.seq;
80
+ this.events.push(e);
81
+ const f = this.file();
82
+ if (f)
83
+ appendFileSync(f, `${JSON.stringify(e)}\n`);
84
+ return e;
85
+ }
86
+ head() { return { day: this.day, head: this.prev, events: this.seq, file: this.file() ? `${this.day}.jsonl` : null }; }
87
+ }
88
+ /** Every `chat_audit` anchor in the run ledgers under this home (an unreadable ledger is skipped). */
89
+ export function chatAuditAnchors(home) {
90
+ const dir = runsDir(home);
91
+ if (!existsSync(dir))
92
+ return [];
93
+ const out = [];
94
+ for (const f of readdirSync(dir).filter((x) => x.endsWith('.jsonl') && !x.endsWith('.stream.jsonl')).sort()) {
95
+ let events;
96
+ try {
97
+ events = readLedgerFile(join(dir, f));
98
+ }
99
+ catch {
100
+ continue;
101
+ }
102
+ for (const e of events) {
103
+ if (e.kind !== 'chat_audit' || typeof e.day !== 'string' || typeof e.head !== 'string' || typeof e.events !== 'number')
104
+ continue;
105
+ out.push({ runId: f.slice(0, -'.jsonl'.length), day: e.day, head: e.head, events: e.events });
106
+ }
107
+ }
108
+ return out;
109
+ }
110
+ /** Verify every chat audit day file (or one day): its own chain, then every run-ledger anchor that names it. */
111
+ export function verifyChatAudit(home, day) {
112
+ const dir = chatDir(home);
113
+ const anchors = chatAuditAnchors(home).filter((a) => !day || a.day === day);
114
+ const onDisk = existsSync(dir) ? readdirSync(dir).filter((f) => /^\d{4}-\d{2}-\d{2}\.jsonl$/.test(f)).map((f) => f.slice(0, 10)) : [];
115
+ const days = [...new Set([...onDisk, ...anchors.map((a) => a.day)])].filter((d) => /^\d{4}-\d{2}-\d{2}$/.test(d) && (!day || d === day)).sort();
116
+ return days.map((d) => {
117
+ const file = join(dir, `${d}.jsonl`);
118
+ const mine = anchors.filter((a) => a.day === d);
119
+ if (!onDisk.includes(d))
120
+ return { day: d, file, check: { ok: false, events: 0, at: 0, error: `missing: run ${mine[0].runId} anchored ${mine[0].events} events in it` } };
121
+ let events;
122
+ try {
123
+ events = readFileSync(file, 'utf8').split('\n').filter((l) => l.trim()).map((l) => JSON.parse(l));
124
+ }
125
+ catch (e) {
126
+ return { day: d, file, check: { ok: false, events: 0, at: 0, error: `cannot read: ${e.message.slice(0, 120)}` } };
127
+ }
128
+ const check = verifyChain(events);
129
+ if (!check.ok)
130
+ return { day: d, file, check };
131
+ for (const a of mine) {
132
+ const at = events.find((e) => e.seq === a.events);
133
+ if (!at)
134
+ return { day: d, file, check: { ok: false, events: events.length, at: events.length, error: `truncated: run ${a.runId} anchored ${a.events} events, the file has ${events.length}` } };
135
+ if (at.hash !== a.head)
136
+ return { day: d, file, check: { ok: false, events: events.length, at: a.events, error: `rewritten: event ${a.events} does not match the head run ${a.runId} anchored` } };
137
+ }
138
+ return { day: d, file, check };
139
+ });
140
+ }