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,367 @@
1
+ // `dexterm overlay` — the STREAMER OVERLAY (task-1427): OBS browser-source scenes fed by the spectator stream.
2
+ //
3
+ // dexterm overlay [--scene activity|coin|pnl|all]… [--theme cyberspace|ice|neon]
4
+ // [--replay FILE [--speed N] [--loop]] [--ledger TRADE_ID [--sol-usd N]]
5
+ // [--port N] [--ticket-ttl MIN] [--json]
6
+ //
7
+ // Data: the live, read-only, redacted spectator stream (the one `dexterm --spectate-mcp` or `dexterm agent serve
8
+ // --spectate` opens; found through spectator.json, token sent in the hello, loopback only), or a saved recording
9
+ // with --replay (every scene then carries a SAMPLE DATA tag). The PnL scene is the agent's PAPER fills on the coin
10
+ // (stamped PAPER), or with --ledger one round trip of the user's own trade ledger (PAPER or LIVE by wallet mode).
11
+ // Scenes: a loopback page per scene with a single-use ticket → token (overlay/server.ts). No network beyond
12
+ // loopback, no inbound control path: the overlay watches; nothing here can arm, sign, size, trade or raise a cap.
13
+ import { readFileSync } from 'node:fs';
14
+ import { loadSpectatorRecording, sanitizeDetail } from '../../agents/src/index.js';
15
+ import { spectatorInfoPath } from "./spectate-cmd.js";
16
+ import { emptyOverlayState, foldOverlay, activityView, coinView } from "./overlay/model.js";
17
+ import { pnlView, ledgerOverlayInput } from "./overlay/pnl.js";
18
+ import { createOverlayServer, OVERLAY_PAGES, OVERLAY_TICKET_TTL_MS } from "./overlay/server.js";
19
+ import { OVERLAY_THEME_NAMES } from "./overlay/page.js";
20
+ export const OVERLAY_USAGE = `dexterm overlay [options] streamer overlay: OBS browser-source scenes of the agent's session
21
+ --scene NAME activity | coin | pnl | all (repeatable; default: the three scenes)
22
+ --theme NAME ${OVERLAY_THEME_NAMES.join(' | ')} (default cyberspace)
23
+ --replay FILE play a saved spectator recording instead of the live stream (scenes say SAMPLE DATA)
24
+ --speed N replay speed (default 1) · --loop replay again from the start when it ends
25
+ --ledger TRADE_ID PnL scene from your own trade ledger (PAPER or LIVE stamp by wallet mode) instead of the
26
+ agent's paper fills · --sol-usd N for a USD-booked trade
27
+ --port N fixed loopback port (default random) · --ticket-ttl MIN minutes a URL stays valid (default 10)
28
+ --json print the scene URLs as JSON
29
+ Each URL works ONCE (paste it into an OBS Browser source). Press n for fresh URLs, q to quit.
30
+ Loopback only · read-only · addresses redacted · every scene shows its data age.
31
+ Full docs: https://dexterm.xyz/docs/overlay/`;
32
+ export function parseOverlayArgs(rest) {
33
+ const o = { scenes: [], theme: 'cyberspace', replay: null, speed: 1, loop: false, ledger: null, solUsd: null, port: undefined, ttlMin: OVERLAY_TICKET_TTL_MS / 60_000, json: false };
34
+ for (let i = 0; i < rest.length; i++) {
35
+ const a = rest[i];
36
+ const val = () => { const v = rest[++i]; return v === undefined || v.startsWith('--') ? null : v; };
37
+ switch (a) {
38
+ case '--scene': {
39
+ const v = val();
40
+ if (!v || !OVERLAY_PAGES.includes(v))
41
+ return `--scene is one of ${OVERLAY_PAGES.join(', ')}`;
42
+ if (!o.scenes.includes(v))
43
+ o.scenes.push(v);
44
+ break;
45
+ }
46
+ case '--theme': {
47
+ const v = val();
48
+ if (!v || !OVERLAY_THEME_NAMES.includes(v))
49
+ return `--theme is one of ${OVERLAY_THEME_NAMES.join(', ')}`;
50
+ o.theme = v;
51
+ break;
52
+ }
53
+ case '--replay': {
54
+ const v = val();
55
+ if (!v)
56
+ return '--replay needs a FILE';
57
+ o.replay = v;
58
+ break;
59
+ }
60
+ case '--speed': {
61
+ const n = Number(val());
62
+ if (!(n > 0 && n <= 100))
63
+ return '--speed is a number from 0 to 100';
64
+ o.speed = n;
65
+ break;
66
+ }
67
+ case '--loop':
68
+ o.loop = true;
69
+ break;
70
+ case '--ledger': {
71
+ const v = val();
72
+ if (!v)
73
+ return '--ledger needs a TRADE_ID (from `dexterm ledger trades`)';
74
+ o.ledger = v;
75
+ break;
76
+ }
77
+ case '--sol-usd': {
78
+ const n = Number(val());
79
+ if (!(n > 0))
80
+ return '--sol-usd is a positive number';
81
+ o.solUsd = n;
82
+ break;
83
+ }
84
+ case '--port': {
85
+ const n = Number(val());
86
+ if (!Number.isInteger(n) || n < 1024 || n > 65535)
87
+ return '--port is an integer from 1024 to 65535';
88
+ o.port = n;
89
+ break;
90
+ }
91
+ case '--ticket-ttl': {
92
+ const n = Number(val());
93
+ if (!(n >= 1 && n <= 240))
94
+ return '--ticket-ttl is minutes from 1 to 240';
95
+ o.ttlMin = n;
96
+ break;
97
+ }
98
+ case '--json':
99
+ o.json = true;
100
+ break;
101
+ default: return `unknown option ${JSON.stringify(a)}`;
102
+ }
103
+ }
104
+ if (!o.scenes.length)
105
+ o.scenes = ['activity', 'coin', 'pnl'];
106
+ if (o.loop && !o.replay)
107
+ return '--loop only applies to --replay';
108
+ return o;
109
+ }
110
+ /**
111
+ * Folds events into the overlay state and pushes the scene views to the server. Public for the tests (they drive it
112
+ * with fixture events and a fake clock); the command wires it to the stream or a recording.
113
+ */
114
+ export class OverlayEngine {
115
+ state;
116
+ server;
117
+ now;
118
+ pnlInput;
119
+ pnlEveryMs;
120
+ lastPnlAt = -Infinity;
121
+ lastPnlKey = '';
122
+ constructor(o) {
123
+ this.state = emptyOverlayState(o.sample);
124
+ this.server = o.server;
125
+ this.now = o.now ?? (() => Date.now());
126
+ this.pnlInput = o.pnlInput;
127
+ this.pnlEveryMs = o.pnlEveryMs ?? 10_000;
128
+ }
129
+ /** Start over (a looped replay): the scenes forget every coin, fill and call. */
130
+ reset() { Object.assign(this.state, emptyOverlayState(this.state.sample)); }
131
+ /** Fold one event read off the stream or a recording (untrusted data: the detail is re-validated first). */
132
+ feed(raw) {
133
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
134
+ return false;
135
+ const e = raw;
136
+ if (typeof e.phase !== 'string' || typeof e.t !== 'number' || !Number.isFinite(e.t))
137
+ return false;
138
+ // the stream and saved files are redacted (mints are head…tail), so the detail is validated in that shape
139
+ const ev = { ...e, detail: sanitizeDetail(e.detail, { masked: true }) };
140
+ foldOverlay(this.state, ev);
141
+ return true;
142
+ }
143
+ /** Push the current views (activity + coin always; the PnL card when its fills changed or it aged). */
144
+ tick() {
145
+ const now = this.now();
146
+ this.server.push('activity', activityView(this.state, now));
147
+ this.server.push('coin', coinView(this.state, now));
148
+ const p = this.state.paperCurrent ? this.state.paper.get(this.state.paperCurrent) : undefined;
149
+ const key = this.pnlInput ? 'ledger' : `${this.state.paperCurrent}:${p?.fills.length ?? 0}:${this.state.coins.get(this.state.paperCurrent ?? '')?.priceUsd?.at ?? 0}`;
150
+ if (key !== this.lastPnlKey || now - this.lastPnlAt >= this.pnlEveryMs) {
151
+ this.lastPnlKey = key;
152
+ this.lastPnlAt = now;
153
+ let input = null;
154
+ let note = null;
155
+ if (this.pnlInput) {
156
+ try {
157
+ input = this.pnlInput();
158
+ }
159
+ catch (err) {
160
+ note = err.message;
161
+ }
162
+ }
163
+ const view = pnlView(this.state, now, input);
164
+ this.server.push('pnl', note ? { ...view, note } : view);
165
+ }
166
+ }
167
+ }
168
+ function ledgerLoader(io, tradeId, solUsd) {
169
+ let row = null;
170
+ let problem = 'reading the trade ledger…';
171
+ return {
172
+ async load() {
173
+ try {
174
+ const at = await import('../../autotrade/src/index.js');
175
+ const v = await at.loadLedgerView({ env: io.env, now: Date.now() });
176
+ const t = v.trades.find((x) => x.tradeId === tradeId);
177
+ if (!t) {
178
+ row = null;
179
+ problem = `no trade ${tradeId} in the trade ledger`;
180
+ return;
181
+ }
182
+ row = t;
183
+ problem = null;
184
+ }
185
+ catch (e) {
186
+ row = null;
187
+ problem = `trade ledger: ${e.message}`;
188
+ }
189
+ },
190
+ input() {
191
+ if (!row)
192
+ throw new Error(problem ?? 'no ledger trade');
193
+ return ledgerOverlayInput(row, solUsd ? { usd: solUsd, source: '--sol-usd' } : null);
194
+ },
195
+ };
196
+ }
197
+ export async function runOverlay(rest, io) {
198
+ const out = (s) => io.stdout.write(s + '\n');
199
+ const errOut = (s) => io.stderr.write(s + '\n');
200
+ if (rest.includes('--help') || rest.includes('-h')) {
201
+ out(OVERLAY_USAGE);
202
+ return 0;
203
+ }
204
+ const o = parseOverlayArgs(rest);
205
+ if (typeof o === 'string') {
206
+ errOut(`dexterm overlay: ${o}`);
207
+ errOut(OVERLAY_USAGE);
208
+ return 2;
209
+ }
210
+ let recording = null;
211
+ if (o.replay) {
212
+ try {
213
+ recording = loadSpectatorRecording(o.replay);
214
+ }
215
+ catch (e) {
216
+ errOut(`dexterm overlay: cannot read ${o.replay}: ${e.message}`);
217
+ return 1;
218
+ }
219
+ if (!recording.events.length) {
220
+ errOut(`dexterm overlay: ${o.replay} has no events`);
221
+ return 1;
222
+ }
223
+ }
224
+ let server;
225
+ try {
226
+ server = await createOverlayServer({
227
+ port: o.port, ticketTtlMs: o.ttlMin * 60_000,
228
+ onEvent: (ev) => { if (ev.ev === 'probe.rejected')
229
+ errOut(`overlay: refused ${ev.method} ${ev.path} (${ev.reason})`); },
230
+ });
231
+ }
232
+ catch (e) {
233
+ errOut(`dexterm overlay: cannot listen on 127.0.0.1${o.port ? `:${o.port}` : ''}: ${e.message}`);
234
+ return 1;
235
+ }
236
+ const ledger = o.ledger ? ledgerLoader(io, o.ledger, o.solUsd) : null;
237
+ if (ledger)
238
+ await ledger.load();
239
+ const engine = new OverlayEngine({ sample: !!recording, server, ...(ledger ? { pnlInput: () => ledger.input() } : {}) });
240
+ const printUrls = () => {
241
+ const urls = o.scenes.map((s) => ({ scene: s, url: server.issueTicket(s, o.theme) }));
242
+ if (o.json) {
243
+ out(JSON.stringify({ theme: o.theme, sample: !!recording, ttlMin: o.ttlMin, scenes: urls }));
244
+ return;
245
+ }
246
+ out(`dexterm overlay · theme ${o.theme}${recording ? ' · SAMPLE DATA (replay)' : ''} · each URL works once, for ${o.ttlMin} min`);
247
+ for (const u of urls)
248
+ out(` ${u.scene.padEnd(9)} ${u.url}`);
249
+ };
250
+ printUrls();
251
+ if (!o.json)
252
+ out(recording ? `replaying ${recording.events.length} event(s) from ${o.replay}` : 'watching the live spectator stream (start one with `dexterm --spectate-mcp` or `dexterm agent serve --spectate`)');
253
+ const timers = [];
254
+ let stopped = false;
255
+ let ws = null;
256
+ // ---- data source
257
+ if (recording) {
258
+ const evs = recording.events;
259
+ let i = 0;
260
+ const step = () => {
261
+ if (stopped)
262
+ return;
263
+ if (i >= evs.length) {
264
+ if (!o.loop)
265
+ return;
266
+ i = 0;
267
+ engine.reset();
268
+ }
269
+ const ev = evs[i];
270
+ // replayed events are re-timed to the playback clock (a signal's own `at` shifts with them), so their ages
271
+ // read as "this long ago in the replay"
272
+ const shift = Date.now() - ev.t;
273
+ const flags = ev.detail?.liveFlags;
274
+ engine.feed({ ...ev, t: ev.t + shift, ...(flags ? { detail: { ...ev.detail, liveFlags: flags.map((f) => ({ ...f, at: (typeof f.at === 'number' ? f.at : ev.t) + shift })) } } : {}) });
275
+ const next = evs[i + 1];
276
+ i++;
277
+ // the end of a looped replay holds its final scenes (the PnL card lands last) before it starts again
278
+ const gap = next ? Math.min(5000, Math.max(0, next.t - ev.t)) / o.speed : 10_000;
279
+ timers.push(setTimeout(step, gap));
280
+ };
281
+ step();
282
+ }
283
+ else {
284
+ const connect = () => {
285
+ if (stopped)
286
+ return;
287
+ let info;
288
+ try {
289
+ info = JSON.parse(readFileSync(spectatorInfoPath(io.env), 'utf8'));
290
+ }
291
+ catch {
292
+ timers.push(setTimeout(connect, 3000));
293
+ return;
294
+ }
295
+ if (!Number.isInteger(info.port) || typeof info.token !== 'string') {
296
+ timers.push(setTimeout(connect, 3000));
297
+ return;
298
+ }
299
+ try {
300
+ ws = new WebSocket(`ws://127.0.0.1:${info.port}`);
301
+ }
302
+ catch {
303
+ timers.push(setTimeout(connect, 3000));
304
+ return;
305
+ }
306
+ const token = info.token;
307
+ ws.onopen = () => ws?.send(JSON.stringify({ method: 'hello', token }));
308
+ ws.onmessage = (m) => {
309
+ let msg;
310
+ try {
311
+ msg = JSON.parse(String(m.data));
312
+ }
313
+ catch {
314
+ return;
315
+ }
316
+ if (msg && msg.type === 'event')
317
+ engine.feed(msg.event);
318
+ };
319
+ ws.onclose = () => { ws = null; if (!stopped)
320
+ timers.push(setTimeout(connect, 3000)); };
321
+ ws.onerror = () => { };
322
+ };
323
+ connect();
324
+ }
325
+ const ticker = setInterval(() => engine.tick(), 1000);
326
+ engine.tick();
327
+ const ledgerTimer = ledger ? setInterval(() => { void ledger.load(); }, 30_000) : null;
328
+ return new Promise((resolve) => {
329
+ const stop = () => {
330
+ if (stopped)
331
+ return;
332
+ stopped = true;
333
+ clearInterval(ticker);
334
+ if (ledgerTimer)
335
+ clearInterval(ledgerTimer);
336
+ for (const t of timers)
337
+ clearTimeout(t);
338
+ try {
339
+ ws?.close();
340
+ }
341
+ catch { /* ignore */ }
342
+ if (io.stdin.isTTY) {
343
+ try {
344
+ io.stdin.setRawMode(false);
345
+ }
346
+ catch { /* ignore */ }
347
+ io.stdin.pause();
348
+ }
349
+ process.off('SIGINT', stop);
350
+ process.off('SIGTERM', stop);
351
+ void server.close().then(() => resolve(0));
352
+ };
353
+ process.on('SIGINT', stop);
354
+ process.on('SIGTERM', stop);
355
+ if (io.stdin.isTTY) {
356
+ io.stdin.setRawMode(true);
357
+ io.stdin.resume();
358
+ io.stdin.on('data', (d) => {
359
+ const k = d.toString();
360
+ if (k === 'q' || k === '\u0003')
361
+ stop();
362
+ else if (k === 'n')
363
+ printUrls();
364
+ });
365
+ }
366
+ });
367
+ }
@@ -138,6 +138,10 @@ export function autotradeCardInput(events, o) {
138
138
  * needs `solUsd`; a fill with no token amount refuses the card rather than guess.
139
139
  */
140
140
  export function ledgerCardInput(row, o = {}) {
141
+ // task-1655: a real-mode row is dexterm's model of a trade it signalled; the real fill is the executor's receipt.
142
+ // A card would stamp it PAPER or LIVE, and both are wrong, so there is none.
143
+ if (row.walletMode === 'real')
144
+ throw new CardError('a real-mode trade: dexterm only modelled this fill (the signal it sent your executor); your executor\'s receipt is the record, so dexterm makes no card for it');
141
145
  const decimals = row.decimals ?? 6;
142
146
  const usdDesk = row.fills.some((f) => f.sol === null);
143
147
  if (usdDesk && !(o.solUsd && o.solUsd.usd > 0))
@@ -1,3 +1,11 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
1
9
  // dexterm — `dexterm record` (task-1414): a HEADLESS collector that keeps the history recorder fed when
2
10
  // nobody has the TUI open. The recorder (task-1329) only grows while the interactive app runs; the backtest
3
11
  // (task-1401) is only worth re-running on fresh data, so this command runs the exact same recorder jobs
@@ -20,13 +28,17 @@
20
28
  // re-installed with them: `--retention-mb N` replaces that pause with eviction of the oldest history (observation
21
29
  // never pauses), `--retention-days D` is the age rule for snapshot rows, and `--focus-ticks` runs the autotrade
22
30
  // tick lane (5–10 s DexScreener samples of watchlist / held / candidate / open coins, plus 1m candles).
31
+ // task-1786 (winner-recall DATA-GAPS items 1-2), opt-in the same way: `--focus-module PATH` loads an ESM module whose
32
+ // `focus(now)` adds focus coins to the tick lane (the challenge / control coins live outside the app; the module that
33
+ // reads them lives with them), and `--exit-probes` adds a keyless Jupiter sell quote for every held focus coin once a
34
+ // minute (exit-probes.ts). Both imply --focus-ticks.
23
35
  import { mkdirSync, writeFileSync, existsSync, rmSync } from 'node:fs';
24
36
  import { spawnSync } from 'node:child_process';
25
37
  import { dirname, join, resolve } from 'node:path';
26
- import { fileURLToPath } from 'node:url';
38
+ import { fileURLToPath, pathToFileURL } from 'node:url';
27
39
  import { defaultDbPath, resolveRpcUrl, extraRpcUrls, redactSecrets } from '../../data/src/index.js';
28
40
  import { PumpPortalClient } from '../../data/src/index.js';
29
- import { Recorder, recorderEnabled, dbFocus, focusFilePath, readFocusFile, DEFAULT_TICKS } from '../../recorder/src/index.js';
41
+ import { Recorder, recorderEnabled, dbFocus, focusFilePath, readFocusFile, exitProbeDeps, DEFAULT_TICKS } from '../../recorder/src/index.js';
30
42
  import { dextermHome } from "./boot-state.js";
31
43
  import { createLiveFeed } from "./feed.js";
32
44
  import { NewPairsLive } from "./live/index.js";
@@ -50,13 +62,30 @@ export function buildLiveEngine(env, opts) {
50
62
  // Focus coins for the tick lane: the watchlist + paper positions in this DB, and whatever the autotrade runner /
51
63
  // the app publish in the focus file. Read on every poll (two indexed reads + one small file).
52
64
  const focusFile = focusFilePath(dextermHome(env));
65
+ // task-1786: the focus module's coins. A throwing module costs its own coins only (logged at most every 10 min).
66
+ let extraFailedAt = -Infinity;
67
+ const extra = () => {
68
+ if (!opts.extraFocus)
69
+ return [];
70
+ try {
71
+ return [...opts.extraFocus()];
72
+ }
73
+ catch (e) {
74
+ if (Date.now() - extraFailedAt > 600_000) {
75
+ extraFailedAt = Date.now();
76
+ opts.onLog?.(`record: focus module failed: ${redactSecrets(e.message)}\n`);
77
+ }
78
+ return [];
79
+ }
80
+ };
53
81
  const recorder = new Recorder({
54
82
  path: opts.dbPath, onLog: opts.verbose ? opts.onLog : undefined,
55
83
  // The cap reclaims the data layer's expired cache entries (its own rule: a day past their TTL) before it evicts
56
84
  // any history; `feed` is built above, so the hook can reach it.
57
85
  ...(opts.retentionBytes ? { cap: { maxBytes: opts.retentionBytes, reclaim: () => feed.dl.cache.prune() } } : {}),
58
86
  ...(opts.retentionDays ? { retention: { columnsDays: opts.retentionDays } } : {}),
59
- ...(opts.focusTicks ? { ticks: { focus: () => [...dbFocus(recorder.database), ...readFocusFile(focusFile, Date.now())] } } : {}),
87
+ ...(opts.focusTicks ? { ticks: { focus: () => [...dbFocus(recorder.database), ...readFocusFile(focusFile, Date.now()), ...extra()] } } : {}),
88
+ ...(opts.focusTicks && opts.exitProbes ? { exitProbes: {} } : {}),
60
89
  });
61
90
  recorder.attach(feed.dl.store);
62
91
  let newpairs = null;
@@ -71,6 +100,7 @@ export function buildLiveEngine(env, opts) {
71
100
  feed.extraMints = () => [...np.rows.keys()];
72
101
  }
73
102
  const dl = feed.dl;
103
+ const probeDeps = exitProbeDeps(dl);
74
104
  return {
75
105
  async observe() {
76
106
  const before = recorder.counters.written;
@@ -83,7 +113,14 @@ export function buildLiveEngine(env, opts) {
83
113
  }
84
114
  catch { /* logged inside; never fatal */ } },
85
115
  enforceCap() { return recorder.enforceCap(); },
86
- ...(opts.focusTicks ? { pollTicks: () => recorder.pollTicks(dl) } : {}),
116
+ ...(opts.focusTicks ? { pollTicks: async () => {
117
+ const r = await recorder.pollTicks(dl);
118
+ // Detached: a quote waiting on the Jupiter bucket must never delay the next 5 s tick (the lane's own in-flight
119
+ // guard keeps one probe pass at a time; it never throws).
120
+ if (opts.exitProbes)
121
+ void recorder.probeExits(probeDeps).catch(() => { });
122
+ return r;
123
+ } } : {}),
87
124
  dbBytes() {
88
125
  try {
89
126
  const db = recorder.database;
@@ -261,6 +298,19 @@ function parseRetention(t, next, a) {
261
298
  a.focusTicks = true;
262
299
  return null;
263
300
  }
301
+ if (t === '--exit-probes') {
302
+ a.exitProbes = true;
303
+ a.focusTicks = true;
304
+ return null;
305
+ }
306
+ if (t === '--focus-module') {
307
+ const v = next();
308
+ if (!v || v.startsWith('--'))
309
+ return '--focus-module needs a PATH';
310
+ a.focusModule = resolve(v);
311
+ a.focusTicks = true;
312
+ return null;
313
+ }
264
314
  if (t === '--retention-mb') {
265
315
  const n = Number(next());
266
316
  if (!Number.isFinite(n) || n < 1)
@@ -278,7 +328,7 @@ function parseRetention(t, next, a) {
278
328
  return undefined;
279
329
  }
280
330
  export function parseRun(argv) {
281
- const a = { intervalMs: DEFAULT_INTERVAL_MS, ticks: Infinity, maintainMs: DEFAULT_MAINTAIN_MS, maxDbBytes: DEFAULT_MAX_DB_MB * 1_000_000, noNewpairs: false, db: null, verbose: false, retentionBytes: null, retentionDays: null, focusTicks: false };
331
+ const a = { intervalMs: DEFAULT_INTERVAL_MS, ticks: Infinity, maintainMs: DEFAULT_MAINTAIN_MS, maxDbBytes: DEFAULT_MAX_DB_MB * 1_000_000, noNewpairs: false, db: null, verbose: false, retentionBytes: null, retentionDays: null, focusTicks: false, focusModule: null, exitProbes: false };
282
332
  for (let i = 0; i < argv.length; i++) {
283
333
  const t = argv[i];
284
334
  const r = parseRetention(t, () => argv[++i], a);
@@ -329,8 +379,9 @@ export function parseRun(argv) {
329
379
  export const RECORD_USAGE = `dexterm record: keep the history recorder fed with no TUI open (task-1414).
330
380
 
331
381
  dexterm record [run] [--interval S] [--ticks N | --once] [--maintain S] [--max-db-mb N] [--retention-mb N] [--retention-days D]
332
- [--focus-ticks] [--no-newpairs] [--db PATH] [--verbose]
333
- dexterm record install [--interval S] [--max-db-mb N] [--retention-mb N] [--retention-days D] [--focus-ticks] [--load] [--dry-run]
382
+ [--focus-ticks] [--focus-module PATH] [--exit-probes] [--no-newpairs] [--db PATH] [--verbose]
383
+ dexterm record install [--interval S] [--max-db-mb N] [--retention-mb N] [--retention-days D] [--focus-ticks]
384
+ [--focus-module PATH] [--exit-probes] [--load] [--dry-run]
334
385
  dexterm record uninstall [--unload]
335
386
  dexterm record status [--db PATH]
336
387
 
@@ -341,7 +392,10 @@ run the collector loop: one keyless feed refresh per --interval (default
341
392
  to stay under N MB and observation never pauses. --retention-days D drops snapshot rows (sweep rows
342
393
  too) older than D days (default 90). --focus-ticks samples watchlist / held / candidate / open coins
343
394
  every 5-10 s (DexScreener batches of 30, a daily call ceiling) with 1m candles; autotrade backtests
344
- read them. --no-newpairs drops the PumpPortal stream. Ctrl-C stops cleanly
395
+ read them. --focus-module PATH adds the coins an ESM module's focus(now) returns
396
+ ([{ mint, role }], role held | open | candidate | watch) to that lane. --exit-probes asks Jupiter for
397
+ a keyless $50 sell quote of every held focus coin once a minute (exit_probes; quotes only).
398
+ Both imply --focus-ticks. --no-newpairs drops the PumpPortal stream. Ctrl-C stops cleanly
345
399
  (labels whatever is due). Requests honour the data layer's rate limits, health gate and sweep budgets.
346
400
  install write a user-level launchd LaunchAgent (no sudo) that runs \`record run\`; prints the launchctl
347
401
  command. --load also runs launchctl (off by default). --dry-run prints the plist, writes nothing.
@@ -404,7 +458,22 @@ async function run(argv, io, deps) {
404
458
  };
405
459
  })();
406
460
  const log = (s) => io.stderr.write(s);
407
- const engine = deps.engine ?? buildLiveEngine(io.env, { dbPath, noNewpairs: parsed.noNewpairs, verbose: parsed.verbose, onLog: log, retentionBytes: parsed.retentionBytes, retentionDays: parsed.retentionDays, focusTicks: parsed.focusTicks });
461
+ // task-1786: the focus module is loaded once, up front; a module that does not load is a startup error (exit 2).
462
+ let extraFocus;
463
+ if (parsed.focusModule) {
464
+ try {
465
+ const mod = await import(__rewriteRelativeImportExtension(pathToFileURL(parsed.focusModule).href));
466
+ if (typeof mod.focus !== 'function')
467
+ throw new Error('it exports no focus(now) function');
468
+ const fn = mod.focus;
469
+ extraFocus = () => fn(Date.now());
470
+ }
471
+ catch (e) {
472
+ io.stderr.write(`dexterm record: --focus-module ${parsed.focusModule}: ${redactSecrets(e.message)}\n`);
473
+ return 2;
474
+ }
475
+ }
476
+ const engine = deps.engine ?? buildLiveEngine(io.env, { dbPath, noNewpairs: parsed.noNewpairs, verbose: parsed.verbose, onLog: log, retentionBytes: parsed.retentionBytes, retentionDays: parsed.retentionDays, focusTicks: parsed.focusTicks, extraFocus, exitProbes: parsed.exitProbes });
408
477
  const own = !deps.engine;
409
478
  const onSig = () => { io.stderr.write('\ndexterm record: stopping…\n'); stop.abort(); };
410
479
  if (own) {
@@ -415,7 +484,7 @@ async function run(argv, io, deps) {
415
484
  // alive between passes and node would exit mid-run (task-1504 smoke). Hold it open until the loops finish.
416
485
  const hold = own ? setInterval(() => { }, 1 << 30) : null;
417
486
  if (parsed.verbose || parsed.ticks !== Infinity)
418
- io.stderr.write(`dexterm record: db ${dbPath} · interval ${Math.round(parsed.intervalMs / 1000)}s · maintain ${Math.round(parsed.maintainMs / 1000)}s · cap ${parsed.maxDbBytes === null ? 'off' : `${Math.round(parsed.maxDbBytes / 1e6)} MB`} · newpairs ${parsed.noNewpairs ? 'off' : 'on'}${parsed.retentionBytes !== null ? ` · retention ${Math.round(parsed.retentionBytes / 1e6)} MB (evict, never pause)` : ''}${parsed.retentionDays !== null ? ` · rows ${parsed.retentionDays} d` : ''} · focus ticks ${parsed.focusTicks ? 'on' : 'off'}\n`);
487
+ io.stderr.write(`dexterm record: db ${dbPath} · interval ${Math.round(parsed.intervalMs / 1000)}s · maintain ${Math.round(parsed.maintainMs / 1000)}s · cap ${parsed.maxDbBytes === null ? 'off' : `${Math.round(parsed.maxDbBytes / 1e6)} MB`} · newpairs ${parsed.noNewpairs ? 'off' : 'on'}${parsed.retentionBytes !== null ? ` · retention ${Math.round(parsed.retentionBytes / 1e6)} MB (evict, never pause)` : ''}${parsed.retentionDays !== null ? ` · rows ${parsed.retentionDays} d` : ''} · focus ticks ${parsed.focusTicks ? 'on' : 'off'}${parsed.focusModule ? ` · focus module ${parsed.focusModule}` : ''}${parsed.exitProbes ? ' · exit probes on' : ''}\n`);
419
488
  let result;
420
489
  let tickRes = { polls: 0, calls: 0, written: 0 };
421
490
  try {
@@ -446,7 +515,7 @@ async function run(argv, io, deps) {
446
515
  }
447
516
  function install(argv, io) {
448
517
  let intervalMs = DEFAULT_INTERVAL_MS, maxDbMb = DEFAULT_MAX_DB_MB, load = false, dry = false;
449
- const ret = { retentionBytes: null, retentionDays: null, focusTicks: false };
518
+ const ret = { retentionBytes: null, retentionDays: null, focusTicks: false, focusModule: null, exitProbes: false };
450
519
  for (let i = 0; i < argv.length; i++) {
451
520
  const t = argv[i];
452
521
  const r = parseRetention(t, () => argv[++i], ret);
@@ -488,6 +557,8 @@ function install(argv, io) {
488
557
  ...(ret.retentionBytes !== null ? ['--retention-mb', String(Math.round(ret.retentionBytes / 1e6))] : []),
489
558
  ...(ret.retentionDays !== null ? ['--retention-days', String(ret.retentionDays)] : []),
490
559
  ...(ret.focusTicks ? ['--focus-ticks'] : []),
560
+ ...(ret.focusModule ? ['--focus-module', ret.focusModule] : []),
561
+ ...(ret.exitProbes ? ['--exit-probes'] : []),
491
562
  ];
492
563
  const plist = renderCollectorPlist({ label: COLLECTOR_LABEL, nodePath: process.execPath, binPath: bin, intervalMs, maxDbMb, extraArgs, logPath, workingDir: resolve(dirname(bin), '..', '..', '..') });
493
564
  if (dry) {
@@ -119,8 +119,9 @@ export async function rulesCommand(argv, io) {
119
119
  case 'fields': {
120
120
  io.stdout.write(`fields: ${FIELD_NAMES.join(' ')}\n`);
121
121
  io.stdout.write(`aliases: vol volume liquidity cap mc holder organic top locked cluster% clusterpct\n`);
122
+ io.stdout.write(`narrative (EXPERIMENTAL, measured heat, not advice): narrative_rank narrative_score narrative:spark|ignition|peak|fading|dead|saturated (null until N → n builds the tracker)\n`);
122
123
  io.stdout.write(`signals: <field>.velocity (current ÷ mean of earlier samples in the window) · <field>.delta (current − oldest in the window)\n`);
123
- io.stdout.write(`tags: ${TAG_NAMES.map((t) => t + ':').join(' ')} (e.g. launchpad:pump flag:mint flag:clean verified:yes sus:no; prefix ! to negate)\n`);
124
+ io.stdout.write(`tags: ${TAG_NAMES.map((t) => t + ':').join(' ')} (e.g. launchpad:pump flag:mint flag:clean verified:yes sus:no narrative:ignition narrative:saturated; prefix ! to negate)\n`);
124
125
  io.stdout.write(`values: $1.5M 25K 2B 20% 2h 30m 3d · ops > >= < <= = != · and or not ( )\n`);
125
126
  io.stdout.write(`actions: ${ACTIONS.join(' ')} alert(level, "text") ticket(buy|sell, size ≤ ${TICKET_SIZE_CAP}, sol|usd|pct) ask(skill) popup(${RULE_POPUP_KINDS.join('|')}) tag("label") sound(name) watch\n`);
126
127
  io.stdout.write(`limits: cooldown (default 10m) · max n/h (default 12, cap ${MAX_PER_HOUR_CAP}) · scope ${SCOPES.join('|')}|mint:<addr> · window (default 10m)\n`);
@@ -1,4 +1,16 @@
1
+ // dexterm — the rules engine's app adapter (task-1358 Tier 3). Pure helpers over @dexterm/custom's RulesEngine
2
+ // that the key handler, the interactive loop and the Rules screen call: load `config.rules` into an engine on
3
+ // `state.rulesUi`, feed it the screener rows every few seconds (`rulesTick`), turn its outputs into things the
4
+ // app can show (toasts, the screen's log, PROPOSAL cards, pending UI actions the loop performs), and persist edits
5
+ // through the config journal (so `dexterm config undo` / the journal cover rules too).
6
+ //
7
+ // SAFETY: a rule's outputs are alerts, annotations (watch/tag on the user's own watchlist), UI actions (popup /
8
+ // terminal bell) and PROPOSALS. Acting on a ticket proposal opens the PAPER ticket in its editing phase (the user
9
+ // sizes and confirms there, signer caps apply); acting on an ask proposal sends a question to the analyst. Nothing
10
+ // in this module can arm, sign, confirm, open a live ticket or touch a mandate — there is no code path for it.
11
+ import { classifierLiveTick } from "./classifier-live.js";
1
12
  import { devBookOf } from "./live/dev-history.js";
13
+ import { narrativeRuleFields } from "./narratives.js";
2
14
  import { RulesEngine, ruleToJson, ruleToText, slug, applyAndJournal, describeActions, } from '../../custom/src/index.js';
3
15
  import { alertToDelivery, proposalToDelivery } from "./alerts/format.js";
4
16
  export const RULE_INPUT_MAX = 400;
@@ -56,6 +68,11 @@ export function larpGrade(verdict) {
56
68
  * `mintable > 0` fires on the real streaming values, not the on-open snapshot. A token opened by `--mint` that is
57
69
  * not in the screener feed is appended as its own row so rules still see it.
58
70
  */
71
+ /** task-1822: the classifier's drain estimate for the `rugp` field (null when unscored or the scoring failed). */
72
+ function rugpOf(state, mint) {
73
+ const p = state.classifierByMint?.get(mint)?.p;
74
+ return typeof p === 'number' && Number.isFinite(p) ? p : null;
75
+ }
59
76
  export function ruleRowsOf(state) {
60
77
  const out = [];
61
78
  const seen = new Set();
@@ -70,11 +87,13 @@ export function ruleRowsOf(state) {
70
87
  // 0.1.5 dev history: the creator's recorded rugs as the `devrugs` field (local recorder only; null = unknown).
71
88
  const book = devBookOf(state);
72
89
  const devRugsOf = (mint) => book ? book.rugsFor(mint) : null;
90
+ // task-1818: narrative_rank / narrative_score / narrative:<state> from the ALREADY-built narratives tracker (never builds one)
91
+ const narr = (mint) => narrativeRuleFields(state, mint);
73
92
  for (const r of state.screener.rows) {
74
93
  if (!r.mint || r.skeleton || seen.has(r.mint))
75
94
  continue;
76
95
  seen.add(r.mint);
77
- const row = Object.assign(Object.create(null), r, { cluster: clusterOf(r.mint), larp: larpGrade(state.larpByMint?.get(r.mint)?.verdict), devRugs: devRugsOf(r.mint) });
96
+ const row = Object.assign(Object.create(null), r, { cluster: clusterOf(r.mint), larp: larpGrade(state.larpByMint?.get(r.mint)?.verdict), devRugs: devRugsOf(r.mint), rugP: rugpOf(state, r.mint) }, narr(r.mint));
78
97
  if (r.mint === openMint)
79
98
  overlaySignalFields(row, state, r.mint);
80
99
  out.push(row);
@@ -85,9 +104,9 @@ export function ruleRowsOf(state) {
85
104
  const row = Object.assign(Object.create(null), {
86
105
  mint: v.mint, symbol: v.symbol, price: v.price, mcap: v.mcap, fdv: v.fdv, liq: v.liq, vol24: v.vol24,
87
106
  holders: v.holders, org: v.org, top10: v.top10, dev: v.dev, lpLocked: v.lpLocked,
88
- cluster: clusterOf(v.mint), larp: larpGrade(state.larpByMint?.get(v.mint)?.verdict), devRugs: devRugsOf(v.mint),
107
+ cluster: clusterOf(v.mint), larp: larpGrade(state.larpByMint?.get(v.mint)?.verdict), devRugs: devRugsOf(v.mint), rugP: rugpOf(state, v.mint),
89
108
  ageMin: v.ageMin, chg: v.chg, launchpad: v.launchpad, flags: v.flags, verified: v.verified, sus: v.sus,
90
- });
109
+ }, narr(v.mint));
91
110
  overlaySignalFields(row, state, v.mint);
92
111
  out.push(row);
93
112
  }
@@ -107,7 +126,7 @@ export function ruleRowsOf(state) {
107
126
  out.push(Object.assign(Object.create(null), {
108
127
  mint: p.mint, symbol: p.symbol, mcap: p.mcap, liq: p.liq, holders: p.holders, launchpad: p.launchpad,
109
128
  ageMin: p.createdAt ? Math.max(0, (now - p.createdAt) / 60_000) : null, bonding: p.bonding, devRugs,
110
- }));
129
+ }, narr(p.mint)));
111
130
  }
112
131
  }
113
132
  return out;
@@ -153,6 +172,7 @@ export function rulesTick(state, now) {
153
172
  const rows = ruleRowsOf(state);
154
173
  if (!rows.length)
155
174
  return null;
175
+ classifierLiveTick(state, rows, now); // task-1822: fills classifierByMint for `rugp` rules (a no-op without one)
156
176
  ui.engine.observe(rows, now);
157
177
  const watch = state.live?.watch.store ?? null;
158
178
  const positions = state.live?.positions ?? null;