dexterm-cli 0.1.7 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/dist/packages/agents/src/spectator.js +31 -3
  2. package/dist/packages/agents/src/tiers.js +12 -0
  3. package/dist/packages/agents/src/tools.js +13 -5
  4. package/dist/packages/autotrade/assets/limits.schema.json +569 -0
  5. package/dist/packages/autotrade/examples/executor/README.md +70 -0
  6. package/dist/packages/autotrade/examples/executor/jsonl-tail-executor.mjs +57 -0
  7. package/dist/packages/autotrade/examples/executor/kit.mjs +144 -0
  8. package/dist/packages/autotrade/examples/executor/signal-cmd-executor.mjs +25 -0
  9. package/dist/packages/autotrade/examples/executor/telegram-bot-recipe.md +47 -0
  10. package/dist/packages/autotrade/examples/executor/webhook-executor.mjs +47 -0
  11. package/dist/packages/autotrade/src/backtest.js +7 -2
  12. package/dist/packages/autotrade/src/chat/config.js +4 -2
  13. package/dist/packages/autotrade/src/chat/control.js +1 -0
  14. package/dist/packages/autotrade/src/chat/notify.js +16 -6
  15. package/dist/packages/autotrade/src/chat/secrets.js +25 -18
  16. package/dist/packages/autotrade/src/chat/service.js +3 -0
  17. package/dist/packages/autotrade/src/engine.js +16 -2
  18. package/dist/packages/autotrade/src/executor-health.js +217 -0
  19. package/dist/packages/autotrade/src/fleet.js +5 -2
  20. package/dist/packages/autotrade/src/forward.js +355 -0
  21. package/dist/packages/autotrade/src/index.js +21 -0
  22. package/dist/packages/autotrade/src/journey.js +477 -0
  23. package/dist/packages/autotrade/src/limits.js +682 -0
  24. package/dist/packages/autotrade/src/outbox.js +394 -0
  25. package/dist/packages/autotrade/src/paper-limits.js +87 -0
  26. package/dist/packages/autotrade/src/real-mode.js +688 -0
  27. package/dist/packages/autotrade/src/receipts.js +330 -0
  28. package/dist/packages/autotrade/src/report.js +168 -0
  29. package/dist/packages/autotrade/src/rugproof-probe.js +415 -0
  30. package/dist/packages/autotrade/src/rugproof.js +231 -0
  31. package/dist/packages/autotrade/src/runner.js +194 -28
  32. package/dist/packages/autotrade/src/service-guard.js +125 -0
  33. package/dist/packages/autotrade/src/service-install.js +215 -0
  34. package/dist/packages/autotrade/src/service-recover.js +232 -0
  35. package/dist/packages/autotrade/src/service.js +289 -0
  36. package/dist/packages/autotrade/src/sessions.js +16 -7
  37. package/dist/packages/autotrade/src/signal-channels.js +312 -0
  38. package/dist/packages/autotrade/src/signal.js +196 -0
  39. package/dist/packages/autotrade/src/strategy-stats.js +344 -0
  40. package/dist/packages/autotrade/src/strategy.js +15 -2
  41. package/dist/packages/autotrade/src/trade-ledger.js +2 -2
  42. package/dist/packages/classifier/src/calibrate.js +138 -0
  43. package/dist/packages/classifier/src/classify.js +107 -0
  44. package/dist/packages/classifier/src/config.js +220 -0
  45. package/dist/packages/classifier/src/dossier.js +95 -0
  46. package/dist/packages/classifier/src/hardware.js +63 -0
  47. package/dist/packages/classifier/src/index.js +18 -0
  48. package/dist/packages/classifier/src/models.js +111 -0
  49. package/dist/packages/classifier/src/providers/heuristic.js +77 -0
  50. package/dist/packages/classifier/src/providers/http.js +63 -0
  51. package/dist/packages/classifier/src/providers/jev-open.js +86 -0
  52. package/dist/packages/classifier/src/providers/llm.js +89 -0
  53. package/dist/packages/classifier/src/providers/systemone-http.js +95 -0
  54. package/dist/packages/classifier/src/questions.js +97 -0
  55. package/dist/packages/classifier/src/registry.js +42 -0
  56. package/dist/packages/classifier/src/snapshot.js +31 -0
  57. package/dist/packages/classifier/src/systemone.js +88 -0
  58. package/dist/packages/classifier/src/types.js +18 -0
  59. package/dist/packages/cli/assets/dash/dash.css +139 -0
  60. package/dist/packages/cli/assets/dash/dash.js +200 -0
  61. package/dist/packages/cli/assets/dash/icon.svg +1 -0
  62. package/dist/packages/cli/assets/dash/index.html +36 -0
  63. package/dist/packages/cli/src/analyst/backend.js +3 -2
  64. package/dist/packages/cli/src/analyst/codex.js +3 -2
  65. package/dist/packages/cli/src/app-help-gen.js +26 -2
  66. package/dist/packages/cli/src/autotrade-cmd/audit.js +19 -2
  67. package/dist/packages/cli/src/autotrade-cmd/backtest.js +21 -4
  68. package/dist/packages/cli/src/autotrade-cmd/executor.js +91 -0
  69. package/dist/packages/cli/src/autotrade-cmd/fleet.js +16 -5
  70. package/dist/packages/cli/src/autotrade-cmd/forward.js +283 -0
  71. package/dist/packages/cli/src/autotrade-cmd/journey.js +208 -0
  72. package/dist/packages/cli/src/autotrade-cmd/limits.js +393 -0
  73. package/dist/packages/cli/src/autotrade-cmd/receipt.js +63 -0
  74. package/dist/packages/cli/src/autotrade-cmd/report.js +79 -0
  75. package/dist/packages/cli/src/autotrade-cmd/run.js +268 -14
  76. package/dist/packages/cli/src/autotrade-cmd/service.js +873 -0
  77. package/dist/packages/cli/src/autotrade-cmd/signal.js +824 -0
  78. package/dist/packages/cli/src/autotrade-cmd.js +50 -4
  79. package/dist/packages/cli/src/classifier-cmd.js +353 -0
  80. package/dist/packages/cli/src/classifier-live.js +81 -0
  81. package/dist/packages/cli/src/dash/data.js +217 -0
  82. package/dist/packages/cli/src/dash/server.js +92 -0
  83. package/dist/packages/cli/src/dash-cmd.js +113 -0
  84. package/dist/packages/cli/src/doctor.js +34 -0
  85. package/dist/packages/cli/src/ledger-cmd.js +4 -2
  86. package/dist/packages/cli/src/live/screens/token-live.js +4 -0
  87. package/dist/packages/cli/src/live/signals.js +8 -0
  88. package/dist/packages/cli/src/main.js +98 -6
  89. package/dist/packages/cli/src/mcp-follow.js +21 -1
  90. package/dist/packages/cli/src/narratives-cmd.js +147 -0
  91. package/dist/packages/cli/src/narratives.js +114 -0
  92. package/dist/packages/cli/src/overlay/model.js +147 -0
  93. package/dist/packages/cli/src/overlay/page.js +146 -0
  94. package/dist/packages/cli/src/overlay/pnl.js +75 -0
  95. package/dist/packages/cli/src/overlay/server.js +170 -0
  96. package/dist/packages/cli/src/overlay-cmd.js +367 -0
  97. package/dist/packages/cli/src/pnl-card.js +4 -0
  98. package/dist/packages/cli/src/record-cmd.js +82 -11
  99. package/dist/packages/cli/src/rules-cmd.js +2 -1
  100. package/dist/packages/cli/src/rules.js +24 -4
  101. package/dist/packages/cli/src/screens/autotrade-journey.js +134 -0
  102. package/dist/packages/cli/src/screens/autotrade-keys.js +13 -0
  103. package/dist/packages/cli/src/screens/autotrade-screen.js +57 -6
  104. package/dist/packages/cli/src/screens/autotrade-ui.js +17 -7
  105. package/dist/packages/cli/src/screens/autotrade-worker.js +4 -2
  106. package/dist/packages/cli/src/screens/narrative.js +154 -2
  107. package/dist/packages/cli/src/state.js +1 -0
  108. package/dist/packages/cli/src/telegram-cmd.js +4 -1
  109. package/dist/packages/cli/src/version.js +1 -1
  110. package/dist/packages/custom/src/rules.js +5 -1
  111. package/dist/packages/data/src/trade/ledger/schema.js +1 -1
  112. package/dist/packages/mcp/assets/kb/app-help/autotrade-executor-kit.md +44 -0
  113. package/dist/packages/mcp/assets/kb/app-help/autotrade-journey.md +46 -0
  114. package/dist/packages/mcp/assets/kb/app-help/autotrade-limits.md +48 -0
  115. package/dist/packages/mcp/assets/kb/app-help/autotrade-real-signal.md +45 -0
  116. package/dist/packages/mcp/assets/kb/app-help/classification-models.md +41 -0
  117. package/dist/packages/mcp/assets/kb/app-help/live-trading.md +1 -1
  118. package/dist/packages/mcp/assets/kb/app-help/narratives-tracker.md +45 -0
  119. package/dist/packages/mcp/assets/kb/app-help/screen-autotrade.md +12 -0
  120. package/dist/packages/mcp/assets/kb/app-help/strategy-dashboard.md +38 -0
  121. package/dist/packages/mcp/assets/kb/app-help/streamer-overlay.md +36 -0
  122. package/dist/packages/mcp/assets/kb/app-help/trade-ledger.md +1 -1
  123. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-change-a-strategy-by-chat.md +3 -1
  124. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-on-the-q-screen.md +1 -0
  125. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-rug-proof-entries-experimental.md +52 -0
  126. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-running-it-all-day.md +2 -2
  127. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-stopping-it.md +2 -0
  128. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-always-on-service.md +59 -0
  129. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-strategy-dashboard-dexterm-dash.md +37 -0
  130. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-is-paper-self-directed.md +9 -2
  131. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-will-not-do.md +1 -1
  132. package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +3 -1
  133. package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
  134. package/dist/packages/mcp/assets/kb/docs/docs-beginners-copy-and-paste-without-surprises.md +0 -1
  135. package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-dexterm-says-command-not-found.md +2 -2
  136. package/dist/packages/mcp/assets/kb/docs/docs-beginners-reopen-dexterm-later.md +1 -1
  137. package/dist/packages/mcp/assets/kb/docs/docs-beginners-terminal-shell-prompt-command-output-and-path.md +2 -2
  138. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +1 -1
  139. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows.md +1 -1
  140. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-1-ask-for-a-strategy.md +27 -0
  141. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-2-backtest-it-held-out-split.md +31 -0
  142. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-3-forward-test-it-on-live-data.md +44 -0
  143. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-4-promote-it-to-always-on-paper.md +36 -0
  144. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-5-real-only-if-you-choose.md +36 -0
  145. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-ladder.md +41 -0
  146. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-report.md +31 -0
  147. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-what-the-analyst-knows.md +30 -0
  148. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat.md +26 -0
  149. package/dist/packages/mcp/assets/kb/docs/docs-classifier-hosted-jev-with-your-own-key.md +34 -0
  150. package/dist/packages/mcp/assets/kb/docs/docs-classifier-how-they-measured-on-dexterm-s-own-history.md +52 -0
  151. package/dist/packages/mcp/assets/kb/docs/docs-classifier-set-up-kev-on-your-own-machine.md +39 -0
  152. package/dist/packages/mcp/assets/kb/docs/docs-classifier-settings.md +35 -0
  153. package/dist/packages/mcp/assets/kb/docs/docs-classifier-the-choices.md +36 -0
  154. package/dist/packages/mcp/assets/kb/docs/docs-classifier-where-it-plugs-in.md +29 -0
  155. package/dist/packages/mcp/assets/kb/docs/docs-classifier-which-model-for-which-machine.md +42 -0
  156. package/dist/packages/mcp/assets/kb/docs/docs-classifier.md +32 -0
  157. package/dist/packages/mcp/assets/kb/docs/docs-cli-invocations.md +1 -1
  158. package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -0
  159. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
  160. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +1 -0
  161. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
  162. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +2 -2
  163. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +2 -11
  164. package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +2 -2
  165. package/dist/packages/mcp/assets/kb/docs/docs-going-real-executor-health.md +38 -0
  166. package/dist/packages/mcp/assets/kb/docs/docs-going-real-paper-vs-real.md +33 -0
  167. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-digest.md +36 -0
  168. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-executor-kit.md +48 -0
  169. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-whole-path.md +37 -0
  170. package/dist/packages/mcp/assets/kb/docs/docs-going-real-what-dexterm-does-and-never-does.md +44 -0
  171. package/dist/packages/mcp/assets/kb/docs/docs-going-real-your-limits.md +77 -0
  172. package/dist/packages/mcp/assets/kb/docs/docs-going-real.md +24 -0
  173. package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +2 -2
  174. package/dist/packages/mcp/assets/kb/docs/docs-larp-paste-a-coin-on-the-web.md +1 -1
  175. package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +1 -1
  176. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-changed-in-0-1-7.md +1 -1
  177. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-real-mode-your-strategy-s-trades-to-your.md +31 -0
  178. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-the-short-version.md +1 -1
  179. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-using-a-trading-bot-with-dexterm.md +0 -2
  180. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does-not-do.md +1 -1
  181. package/dist/packages/mcp/assets/kb/docs/docs-narratives-how-coins-are-grouped.md +39 -0
  182. package/dist/packages/mcp/assets/kb/docs/docs-narratives-limits.md +29 -0
  183. package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-is-measured.md +44 -0
  184. package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-testing-showed.md +43 -0
  185. package/dist/packages/mcp/assets/kb/docs/docs-narratives-where-to-find-it.md +37 -0
  186. package/dist/packages/mcp/assets/kb/docs/docs-narratives.md +29 -0
  187. package/dist/packages/mcp/assets/kb/docs/docs-overlay-before-you-start.md +33 -0
  188. package/dist/packages/mcp/assets/kb/docs/docs-overlay-limits.md +30 -0
  189. package/dist/packages/mcp/assets/kb/docs/docs-overlay-options.md +36 -0
  190. package/dist/packages/mcp/assets/kb/docs/docs-overlay-security-model.md +36 -0
  191. package/dist/packages/mcp/assets/kb/docs/docs-overlay-set-it-up-in-obs-step-by-step.md +49 -0
  192. package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-pnl-scene-paper-and-live.md +34 -0
  193. package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-three-scenes.md +35 -0
  194. package/dist/packages/mcp/assets/kb/docs/docs-overlay-what-is-redacted.md +31 -0
  195. package/dist/packages/mcp/assets/kb/docs/docs-overlay.md +25 -0
  196. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +1 -1
  197. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-receipts-what-your-executor-did.md +42 -0
  198. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-safety.md +34 -0
  199. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-set-it-up.md +47 -0
  200. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-channels.md +42 -0
  201. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-signal.md +48 -0
  202. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-is.md +37 -0
  203. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-will-not-do.md +34 -0
  204. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-where-you-see-it.md +34 -0
  205. package/dist/packages/mcp/assets/kb/docs/docs-real-mode.md +27 -0
  206. package/dist/packages/mcp/assets/kb/docs/docs-screens-bubble-map.md +1 -1
  207. package/dist/packages/mcp/assets/kb/docs/docs-spectator-streaming-it.md +1 -0
  208. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +1 -1
  209. package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +2 -2
  210. package/dist/packages/mcp/assets/kb/entries/autotrade.md +4 -2
  211. package/dist/packages/mcp/assets/kb/entries/narratives-tracker.md +45 -0
  212. package/dist/packages/mcp/src/autotrade-tools.js +168 -5
  213. package/dist/packages/mcp/src/engine/restrictions.js +3 -3
  214. package/dist/packages/mcp/src/index.js +1 -1
  215. package/dist/packages/mcp/src/ledger-tools.js +155 -7
  216. package/dist/packages/mcp/src/server.js +125 -10
  217. package/dist/packages/mcp/src/spectator-sink.js +27 -0
  218. package/dist/packages/narratives/src/cluster.js +193 -0
  219. package/dist/packages/narratives/src/embed.js +184 -0
  220. package/dist/packages/narratives/src/export.js +60 -0
  221. package/dist/packages/narratives/src/index.js +10 -0
  222. package/dist/packages/narratives/src/label.js +48 -0
  223. package/dist/packages/narratives/src/normalize.js +179 -0
  224. package/dist/packages/narratives/src/recorder.js +93 -0
  225. package/dist/packages/narratives/src/tracker.js +328 -0
  226. package/dist/packages/recorder/src/exit-probes.js +215 -0
  227. package/dist/packages/recorder/src/index.js +1 -0
  228. package/dist/packages/recorder/src/recorder.js +11 -1
  229. package/dist/packages/recorder/src/retention.js +4 -0
  230. package/dist/packages/recorder/src/schema.js +20 -1
  231. package/dist/packages/tui/src/keymap.js +12 -0
  232. package/package.json +3 -1
  233. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-powershell-experimental.md +0 -39
@@ -0,0 +1,215 @@
1
+ // dexterm autotrade — the always-on service's OS installers (0.2.0 card B, AUTOTRADE-SPEC §7.4).
2
+ //
3
+ // Every platform runs the same thing: `dexterm autotrade service ensure` once a minute and at login. `ensure` exits at
4
+ // once while the service's heartbeat is fresh and otherwise starts it detached, so a crash costs at most one tick and
5
+ // a clean stop stays stopped. The generators are pure (golden-file tested); the CLI shows the user every file and
6
+ // command before anything is written, and records what it wrote (install manifest) so uninstall removes exactly that.
7
+ //
8
+ // macOS ~/Library/LaunchAgents/<label>.plist: StartInterval 60, RunAtLoad, ProcessType Background, Nice 10,
9
+ // LowPriorityIO, AbandonProcessGroup (the started service outlives the tick). NO KeepAlive and never
10
+ // `launchctl submit` (a KeepAlive job relaunches forever). Loaded with `launchctl bootout` + `bootstrap`:
11
+ // `kickstart -k` does NOT re-read the plist's arguments.
12
+ // Linux ~/.config/systemd/user/<unit>.service (oneshot ensure, Nice 10, idle IO, KillMode=process so the started
13
+ // service survives the tick) + <unit>.timer (30 s after login, then every 60 s). Without
14
+ // `loginctl enable-linger` a user unit runs only while the user is logged in.
15
+ // Windows a scheduled task (XML for `schtasks /Create /XML`): at logon + every minute, ignore-new on overlap,
16
+ // below-normal priority, least privilege, 5-minute limit for the tick itself.
17
+ //
18
+ // One label per DEXTERM_HOME: the default home gets `xyz.dexterm.autotrade`; any other home gets a suffix from a hash
19
+ // of its path, so a test home never replaces (or removes) the real one. A non-default home is passed as `--home`.
20
+ // Only PATH is copied into the job's environment (the service needs to find `claude`); nothing else, no secrets.
21
+ import { createHash } from 'node:crypto';
22
+ import { join, posix } from 'node:path';
23
+ export const SERVICE_LABEL = 'xyz.dexterm.autotrade';
24
+ export const INSTALL_MANIFEST_SCHEMA = 'dexterm-autotrade-service-install/1';
25
+ /** The label for a DEXTERM_HOME: the default one, or one suffixed by a hash of the path. */
26
+ export function serviceLabel(dexHome, defaultHome) {
27
+ if (dexHome === defaultHome)
28
+ return SERVICE_LABEL;
29
+ return `${SERVICE_LABEL}.h${createHash('sha256').update(dexHome).digest('hex').slice(0, 8)}`;
30
+ }
31
+ /** The tick's command line (`--quiet`: a tick that changes nothing prints nothing, so the tick log stays small). */
32
+ export const ensureArgs = (s) => [s.nodePath, s.binPath, 'autotrade', 'service', 'ensure', '--quiet', ...(s.dexHome ? ['--home', s.dexHome] : [])];
33
+ // ───────────────────────────── macOS ─────────────────────────────
34
+ const xml = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
35
+ // LaunchAgent and systemd paths are macOS / Linux paths whatever host computes the plan (task-1712: a Windows host
36
+ // joined them with `\`); the Windows task and the manifest live under the local dexHome and use the host's join.
37
+ export const launchAgentPath = (s) => posix.join(s.userHome, 'Library', 'LaunchAgents', `${s.label}.plist`);
38
+ export function renderLaunchAgent(s) {
39
+ const args = ensureArgs(s).map((a) => ` <string>${xml(a)}</string>`).join('\n');
40
+ const env = s.path ? ` <key>EnvironmentVariables</key>\n <dict>\n <key>PATH</key>\n <string>${xml(s.path)}</string>\n </dict>\n` : '';
41
+ return `<?xml version="1.0" encoding="UTF-8"?>
42
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
43
+ <plist version="1.0">
44
+ <dict>
45
+ <key>Label</key>
46
+ <string>${xml(s.label)}</string>
47
+ <key>ProgramArguments</key>
48
+ <array>
49
+ ${args}
50
+ </array>
51
+ <key>WorkingDirectory</key>
52
+ <string>${xml(s.workingDir)}</string>
53
+ ${env} <key>StartInterval</key>
54
+ <integer>60</integer>
55
+ <key>RunAtLoad</key>
56
+ <true/>
57
+ <key>ProcessType</key>
58
+ <string>Background</string>
59
+ <key>Nice</key>
60
+ <integer>10</integer>
61
+ <key>LowPriorityIO</key>
62
+ <true/>
63
+ <key>AbandonProcessGroup</key>
64
+ <true/>
65
+ <key>StandardOutPath</key>
66
+ <string>${xml(s.tickLog)}</string>
67
+ <key>StandardErrorPath</key>
68
+ <string>${xml(s.tickLog)}</string>
69
+ </dict>
70
+ </plist>
71
+ `;
72
+ }
73
+ // ───────────────────────────── Linux (systemd user) ─────────────────────────────
74
+ export const systemdUnit = (s) => s.label.replace(/^xyz\./, '').replace(/\./g, '-');
75
+ export const systemdDir = (s) => posix.join(s.userHome, '.config', 'systemd', 'user');
76
+ /** systemd quoting: double quotes, backslash and quote escaped, `%` doubled (specifier expansion). */
77
+ const sd = (a) => `"${a.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/%/g, '%%')}"`;
78
+ export function renderSystemdService(s) {
79
+ return `[Unit]
80
+ Description=dexterm autotrade service: start it when it is not running (one tick)
81
+
82
+ [Service]
83
+ Type=oneshot
84
+ ExecStart=${ensureArgs(s).map(sd).join(' ')}
85
+ WorkingDirectory=${s.workingDir.replace(/%/g, '%%')}
86
+ ${s.path ? `Environment=${sd(`PATH=${s.path}`)}\n` : ''}Nice=10
87
+ IOSchedulingClass=idle
88
+ KillMode=process
89
+ StandardOutput=append:${s.tickLog.replace(/%/g, '%%')}
90
+ StandardError=append:${s.tickLog.replace(/%/g, '%%')}
91
+ `;
92
+ }
93
+ export function renderSystemdTimer(s) {
94
+ return `[Unit]
95
+ Description=dexterm autotrade service tick (every minute)
96
+
97
+ [Timer]
98
+ OnStartupSec=30
99
+ OnUnitActiveSec=60
100
+ AccuracySec=10
101
+ Unit=${systemdUnit(s)}.service
102
+
103
+ [Install]
104
+ WantedBy=timers.target
105
+ `;
106
+ }
107
+ // ───────────────────────────── Windows (scheduled task) ─────────────────────────────
108
+ export const windowsTaskName = (s) => `\\dexterm\\${s.label.replace(/^xyz\.dexterm\./, '')}`;
109
+ export const windowsTaskFile = (dexHome, s) => join(dexHome, 'autotrade', 'service', `${s.label}.task.xml`);
110
+ /** Windows command-line quoting for one argument (CommandLineToArgvW rules). */
111
+ export function winArg(a) {
112
+ if (a && !/[\s"]/.test(a))
113
+ return a;
114
+ let out = '"';
115
+ let bs = 0;
116
+ for (const ch of a) {
117
+ if (ch === '\\') {
118
+ bs++;
119
+ continue;
120
+ }
121
+ if (ch === '"') {
122
+ out += '\\'.repeat(bs * 2 + 1) + '"';
123
+ bs = 0;
124
+ continue;
125
+ }
126
+ out += '\\'.repeat(bs) + ch;
127
+ bs = 0;
128
+ }
129
+ return `${out}${'\\'.repeat(bs * 2)}"`;
130
+ }
131
+ export function renderWindowsTask(s) {
132
+ const [cmd, ...rest] = ensureArgs(s);
133
+ return `<?xml version="1.0" encoding="UTF-16"?>
134
+ <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
135
+ <RegistrationInfo>
136
+ <Description>dexterm autotrade service: start it when it is not running (one tick a minute)</Description>
137
+ </RegistrationInfo>
138
+ <Triggers>
139
+ <LogonTrigger>
140
+ <Enabled>true</Enabled>
141
+ </LogonTrigger>
142
+ <TimeTrigger>
143
+ <Repetition>
144
+ <Interval>PT1M</Interval>
145
+ <StopAtDurationEnd>false</StopAtDurationEnd>
146
+ </Repetition>
147
+ <StartBoundary>2026-01-01T00:00:00</StartBoundary>
148
+ <Enabled>true</Enabled>
149
+ </TimeTrigger>
150
+ </Triggers>
151
+ <Principals>
152
+ <Principal id="Author">
153
+ <LogonType>InteractiveToken</LogonType>
154
+ <RunLevel>LeastPrivilege</RunLevel>
155
+ </Principal>
156
+ </Principals>
157
+ <Settings>
158
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
159
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
160
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
161
+ <StartWhenAvailable>true</StartWhenAvailable>
162
+ <ExecutionTimeLimit>PT5M</ExecutionTimeLimit>
163
+ <Priority>7</Priority>
164
+ <Enabled>true</Enabled>
165
+ </Settings>
166
+ <Actions Context="Author">
167
+ <Exec>
168
+ <Command>${xml(cmd)}</Command>
169
+ <Arguments>${xml(rest.map(winArg).join(' '))}</Arguments>
170
+ <WorkingDirectory>${xml(s.workingDir)}</WorkingDirectory>
171
+ </Exec>
172
+ </Actions>
173
+ </Task>
174
+ `;
175
+ }
176
+ // ───────────────────────────── the plan ─────────────────────────────
177
+ export function installPlan(s, dexHome) {
178
+ if (s.platform === 'darwin') {
179
+ const path = launchAgentPath(s);
180
+ const domain = `gui/${s.uid}`;
181
+ return {
182
+ platform: s.platform, label: s.label, name: s.label,
183
+ files: [{ path, content: renderLaunchAgent(s) }],
184
+ activate: [['launchctl', 'bootout', `${domain}/${s.label}`], ['launchctl', 'bootstrap', domain, path]],
185
+ deactivate: [['launchctl', 'bootout', `${domain}/${s.label}`]],
186
+ notes: ['A LaunchAgent starts at login: the service runs while you are logged in on this Mac.', 'To change it later: edit nothing by hand; run `dexterm autotrade service install --load` again (bootout + bootstrap; `kickstart -k` would keep the old arguments).'],
187
+ };
188
+ }
189
+ if (s.platform === 'linux') {
190
+ const unit = systemdUnit(s);
191
+ const dir = systemdDir(s);
192
+ return {
193
+ platform: s.platform, label: s.label, name: `${unit}.timer`,
194
+ files: [{ path: posix.join(dir, `${unit}.service`), content: renderSystemdService(s) }, { path: posix.join(dir, `${unit}.timer`), content: renderSystemdTimer(s) }],
195
+ activate: [['systemctl', '--user', 'daemon-reload'], ['systemctl', '--user', 'enable', '--now', `${unit}.timer`]],
196
+ deactivate: [['systemctl', '--user', 'disable', '--now', `${unit}.timer`]],
197
+ notes: ['A systemd user timer runs while you are logged in; `loginctl enable-linger` (your choice) keeps it running after logout and from boot.'],
198
+ };
199
+ }
200
+ const file = windowsTaskFile(dexHome, s);
201
+ const name = windowsTaskName(s);
202
+ return {
203
+ platform: s.platform, label: s.label, name,
204
+ files: [{ path: file, content: renderWindowsTask(s), encoding: 'utf16le' }],
205
+ activate: [['schtasks', '/Create', '/TN', name, '/XML', file, '/F']],
206
+ deactivate: [['schtasks', '/Delete', '/TN', name, '/F']],
207
+ notes: ['The task runs at logon and every minute while you are logged in.'],
208
+ };
209
+ }
210
+ export const fileSha = (content, encoding = 'utf8') => createHash('sha256').update(encode(content, encoding)).digest('hex');
211
+ /** The bytes written for a file (UTF-16 LE with a BOM for the Windows task XML). */
212
+ export function encode(content, encoding = 'utf8') {
213
+ return encoding === 'utf16le' ? Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(content, 'utf16le')]) : Buffer.from(content, 'utf8');
214
+ }
215
+ export const installManifestPath = (dexHome) => join(dexHome, 'autotrade', 'service', 'install.json');
@@ -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
+ }