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
@@ -47,6 +47,7 @@ import { recordLarpVerdict } from "./larp-record.js";
47
47
  import { newAutotradeUi, defaultDeps as autotradeDeps, autotradeCapturing, autotradeTick, reloadList as autotradeReload, refreshRunner as autotradeRefreshRunner } from "./screens/autotrade-ui.js";
48
48
  import { autotradeAction, autotradeKey } from "./screens/autotrade-keys.js";
49
49
  import { workerJobs as autotradeWorkerJobs } from "./screens/autotrade-worker.js";
50
+ import { narrativesUiOf, kickNarratives, heatRows, drillDetail } from "./narratives.js";
50
51
  import { rulesUiOf, rulesTick, rulesMove, beginRuleInput, saveRuleInput, dryRunNow, removeSelectedRule, toggleSelectedRule, dismissSelectedProposal, actOnSelectedProposal, RULE_INPUT_MAX } from "./rules.js";
51
52
  import { buildNotifier, hasPushChannel } from "./alerts/notifier.js";
52
53
  import { copyAlertToDelivery } from "./alerts/format.js";
@@ -306,12 +307,16 @@ export const HELP = `dexterm ${VERSION} (live: screener, token, new pairs, watch
306
307
  dexterm kb list|show|new|edit|check|reset|path your knowledge-base entries (~/.dexterm/kb/entries) over the bundled 18
307
308
  dexterm skill list|show|new|edit|check|reset|path your analyst skills (~/.dexterm/skills) over the bundled ones (read tools only)
308
309
  dexterm narrative [--window 1h|24h] [--summary] the narrative board over recorded history (no network, no model)
310
+ dexterm narratives [top|detail ID|of MINT|export] EXPERIMENTAL memes + narratives tracker over recorded history: themes, heat, lifecycle (measured heat, not advice)
309
311
  dexterm backtest [--subject heuristic|pack_verdicts] [--horizon SEC] honest rug-model metrics over recorded outcomes (no network, no model)
310
312
  dexterm ledger trades|positions|summary|equity|compare|export|card|sync|verify the unified trade ledger: paper, agent desk, autotrade runs (no network; 'dexterm ledger help')
313
+ dexterm dash [--port N] [--home DIR] [--baseline ID] [--export DIR] your strategies' performance in a browser on 127.0.0.1 (read-only, no network; 'dexterm dash --help')
311
314
  dexterm wallet link|unlink|status|panic|resume link a Phantom wallet via a local bridge (pubkey only); panic halts autotrade + the paper desk, resume needs a TTY keypress
312
315
  dexterm plan propose|list|arm|cancel|journal conditional order plans (entry/ladder/take-profit/moon bag/stop); arming needs a physical keypress
313
316
  dexterm mandate draft|list|arm|revoke standing capped mandates (paper fills); arming needs a physical keypress — voice/agents can never arm
314
317
  dexterm follow add|remove|list|stats|trades|mirror|purge copy trading tier 1: follow a wallet (your list, local labels), its decoded swaps + FIFO PnL + win rate/early-entry; 'mirror' previews a one-key MIRROR TICKET from the wallet's latest buy (inert; arm in the app)
318
+ dexterm classifier [list|pick|pull|serve|doctor|bench|try|key] classification models (EXPERIMENTAL): Kev on your own machine
319
+ (llama.cpp), hosted Jev with your own key, or the built-in heuristic; model estimates, never advice
315
320
  dexterm ai [status|engine|model|host|restrictions|probe|ask|agent] the AI engine: your claude / codex CLI or a free LOCAL model on
316
321
  your own Ollama (localhost); restriction profiles observe|propose|paper|full ('dexterm ai help')
317
322
  dexterm mcp [install <client>|doctor] read-only MCP server for your own agent; 'install' writes the config for claude-code|claude-desktop|cursor|codex|generic (asks first, idempotent, backs up); 'doctor' verifies the connection
@@ -849,6 +854,12 @@ export async function main(argv, io = process, proc = process) {
849
854
  const { runSpectate } = await import("./spectate-cmd.js");
850
855
  return runSpectate(argv.slice(1), io);
851
856
  }
857
+ // `dexterm overlay` → the streamer overlay (task-1427): OBS browser-source scenes (agent activity, the coin card,
858
+ // the PAPER / LIVE PnL card) fed by the read-only spectator stream or a replay; loopback, single-use ticket → token.
859
+ if (argv[0] === 'overlay') {
860
+ const { runOverlay } = await import("./overlay-cmd.js");
861
+ return runOverlay(argv.slice(1), io);
862
+ }
852
863
  // `dexterm recorder <cmd>` → the history recorder's tables: stats, label, prune, export (task-1329). No network.
853
864
  if (argv[0] === 'recorder') {
854
865
  const { recorderCli } = await import('../../recorder/src/index.js');
@@ -859,11 +870,21 @@ export async function main(argv, io = process, proc = process) {
859
870
  const { narrativeCommand } = await import("./narrative-cmd.js");
860
871
  return narrativeCommand(argv.slice(1), { stdout: io.stdout, stderr: io.stderr, env: io.env });
861
872
  }
873
+ // `dexterm narratives [top|detail ID|of MINT|export]` → the MEMES + NARRATIVES tracker (task-1818, EXPERIMENTAL). No network, no model.
874
+ if (argv[0] === 'narratives') {
875
+ const { narrativesCommand } = await import("./narratives-cmd.js");
876
+ return narrativesCommand(argv.slice(1), { stdout: io.stdout, stderr: io.stderr, env: io.env });
877
+ }
862
878
  // `dexterm ledger …` → the unified trade ledger (task-1530): reads fold the file + the sources; only `sync` writes. No network, no model, no keys.
863
879
  if (argv[0] === 'ledger') {
864
880
  const { ledgerCommand } = await import("./ledger-cmd.js");
865
881
  return ledgerCommand(argv.slice(1), { stdout: io.stdout, stderr: io.stderr, env: io.env });
866
882
  }
883
+ // `dexterm dash` → the strategy performance dashboard (task-1749): run ledgers → a page on 127.0.0.1 only. Read-only, no network, no model, no keys.
884
+ if (argv[0] === 'dash') {
885
+ const { dashCommand } = await import("./dash-cmd.js");
886
+ return dashCommand(argv.slice(1), { stdout: io.stdout, stderr: io.stderr, env: io.env });
887
+ }
867
888
  // `dexterm autotrade validate|list|backtest|sweep …` and `dexterm backtest --subject strategy` → the paper autotrade engine (task-1503). No network, no model, no keys.
868
889
  if (argv[0] === 'autotrade' || argv[0] === 'backtest') {
869
890
  const at = await import("./autotrade-cmd.js");
@@ -1001,6 +1022,11 @@ export async function main(argv, io = process, proc = process) {
1001
1022
  const { aiCommand } = await import("./ai-cmd.js");
1002
1023
  return aiCommand(argv[0] === 'ask' ? ['ask', ...argv.slice(1)] : argv.slice(1), { stdin: io.stdin, stdout: io.stdout, stderr: io.stderr, env: io.env });
1003
1024
  }
1025
+ // task-1822: `dexterm classifier …` (pick / pull / serve the classification model; hardware tiers; hosted key).
1026
+ if (argv[0] === 'classifier') {
1027
+ const { classifierCommand } = await import("./classifier-cmd.js");
1028
+ return classifierCommand(argv.slice(1), { stdin: io.stdin, stdout: io.stdout, stderr: io.stderr, env: io.env });
1029
+ }
1004
1030
  // `dexterm doctor` → a pre-flight that says whether dexterm will run here and how to fix what won't (task-1305).
1005
1031
  if (argv[0] === 'doctor') {
1006
1032
  const { defaultDeps, runDoctor, renderDoctor } = await import("./doctor.js");
@@ -1433,6 +1459,20 @@ function rulesScreenMove(state, delta) {
1433
1459
  rulesMove(state.rulesUi, delta);
1434
1460
  return true;
1435
1461
  }
1462
+ // task-1818: j/k on the N screen's EXPERIMENTAL heat table move its cursor, or the member cursor while drilled in. The theme
1463
+ // board (the default mode) keeps the shared state.selected cursor.
1464
+ function narrativesMove(state, delta) {
1465
+ const ui = state.narrativesUi;
1466
+ if (state.screen !== 'narrative' || ui?.mode !== 'heat')
1467
+ return false;
1468
+ if (ui.drill) {
1469
+ const n = drillDetail(state)?.memberList.length ?? 0;
1470
+ ui.drill.selected = Math.max(0, Math.min(n - 1, ui.drill.selected + delta));
1471
+ return true;
1472
+ }
1473
+ ui.selected = Math.max(0, Math.min(heatRows(state).length - 1, ui.selected + delta));
1474
+ return true;
1475
+ }
1436
1476
  function plansMove(state, delta) {
1437
1477
  if (state.screen !== 'plans' || !state.plansUi)
1438
1478
  return false;
@@ -1519,7 +1559,7 @@ export function applyAction(state, action, stack) {
1519
1559
  // updates the context injected next turn); PgUp/PgDn scroll the chat transcript. On the token
1520
1560
  // screen (QA M12) the movement keys scroll the answer. Keys reach here only when input is NOT focused.
1521
1561
  case 'move.down':
1522
- if (plansMove(state, 1) || followsMove(state, 1) || rulesScreenMove(state, 1))
1562
+ if (narrativesMove(state, 1) || plansMove(state, 1) || followsMove(state, 1) || rulesScreenMove(state, 1))
1523
1563
  break;
1524
1564
  if (bubbleMove(state, 1))
1525
1565
  break;
@@ -1528,7 +1568,7 @@ export function applyAction(state, action, stack) {
1528
1568
  state.selected = Math.min(rows().length - 1, (state.selected || 0) + 1);
1529
1569
  break;
1530
1570
  case 'move.up':
1531
- if (plansMove(state, -1) || followsMove(state, -1) || rulesScreenMove(state, -1))
1571
+ if (narrativesMove(state, -1) || plansMove(state, -1) || followsMove(state, -1) || rulesScreenMove(state, -1))
1532
1572
  break;
1533
1573
  if (bubbleMove(state, -1))
1534
1574
  break;
@@ -1537,7 +1577,7 @@ export function applyAction(state, action, stack) {
1537
1577
  state.selected = Math.max(0, (state.selected || 0) - 1);
1538
1578
  break;
1539
1579
  case 'move.pagedown':
1540
- if (plansMove(state, PAGE_ROWS) || followsMove(state, PAGE_ROWS) || rulesScreenMove(state, PAGE_ROWS))
1580
+ if (narrativesMove(state, PAGE_ROWS) || plansMove(state, PAGE_ROWS) || followsMove(state, PAGE_ROWS) || rulesScreenMove(state, PAGE_ROWS))
1541
1581
  break;
1542
1582
  if (analystChatScroll(state, PAGE_ROWS))
1543
1583
  break;
@@ -1546,7 +1586,7 @@ export function applyAction(state, action, stack) {
1546
1586
  state.selected = Math.min(rows().length - 1, (state.selected || 0) + PAGE_ROWS);
1547
1587
  break;
1548
1588
  case 'move.pageup':
1549
- if (plansMove(state, -PAGE_ROWS) || followsMove(state, -PAGE_ROWS) || rulesScreenMove(state, -PAGE_ROWS))
1589
+ if (narrativesMove(state, -PAGE_ROWS) || plansMove(state, -PAGE_ROWS) || followsMove(state, -PAGE_ROWS) || rulesScreenMove(state, -PAGE_ROWS))
1550
1590
  break;
1551
1591
  if (analystChatScroll(state, -PAGE_ROWS))
1552
1592
  break;
@@ -1555,14 +1595,14 @@ export function applyAction(state, action, stack) {
1555
1595
  state.selected = Math.max(0, (state.selected || 0) - PAGE_ROWS);
1556
1596
  break;
1557
1597
  case 'move.top':
1558
- if (plansMove(state, -1e9) || followsMove(state, -1e9))
1598
+ if (narrativesMove(state, -1e9) || plansMove(state, -1e9) || followsMove(state, -1e9))
1559
1599
  break;
1560
1600
  if (chatScroll(state, -1e9))
1561
1601
  break;
1562
1602
  state.selected = 0;
1563
1603
  break;
1564
1604
  case 'move.bottom':
1565
- if (plansMove(state, 1e9) || followsMove(state, 1e9))
1605
+ if (narrativesMove(state, 1e9) || plansMove(state, 1e9) || followsMove(state, 1e9))
1566
1606
  break;
1567
1607
  if (chatScroll(state, 1e9))
1568
1608
  break;
@@ -1715,6 +1755,10 @@ export function applyAction(state, action, stack) {
1715
1755
  state.copyUi.detail = null;
1716
1756
  break;
1717
1757
  } // task-1399: esc closes the wallet detail view before leaving the screen
1758
+ if (state.screen === 'narrative' && state.narrativesUi?.drill) {
1759
+ state.narrativesUi.drill = null;
1760
+ break;
1761
+ } // task-1818: esc in a drill-down returns to the heat table
1718
1762
  const prev = stack.pop();
1719
1763
  if (prev)
1720
1764
  state.screen = prev;
@@ -1982,6 +2026,46 @@ export function applyAction(state, action, stack) {
1982
2026
  stack.push(state.screen);
1983
2027
  state.screen = 'narrative';
1984
2028
  state.narrative = computeNarrativeView(state, state.narrative?.window ?? '1h');
2029
+ if (state.narrativesUi?.mode === 'heat')
2030
+ kickNarratives(state); // task-1818: back on the heat table → refresh if it is older than 60 s
2031
+ break;
2032
+ }
2033
+ // task-1818: n flips the theme board ⇄ the EXPERIMENTAL heat table. The first flip starts the tracker build (the recorder's
2034
+ // last 26 h, ~30 s; never at app start); the table shows "building…" until it lands, later refreshes come from the tick.
2035
+ case 'narrative.view': {
2036
+ if (state.screen !== 'narrative')
2037
+ break;
2038
+ const ui = narrativesUiOf(state);
2039
+ ui.mode = ui.mode === 'heat' ? 'board' : 'heat';
2040
+ ui.drill = null;
2041
+ if (ui.mode === 'heat')
2042
+ kickNarratives(state);
2043
+ break;
2044
+ }
2045
+ // task-1818: ⏎ on the heat table drills into the selected narrative; ⏎ on a member opens that coin's token screen (live
2046
+ // mode; fixtures say so). On the theme board ⏎ keeps its old meaning (the shared `open`).
2047
+ case 'narrative.drill': {
2048
+ if (state.screen !== 'narrative')
2049
+ break;
2050
+ const ui = state.narrativesUi;
2051
+ if (ui?.mode !== 'heat')
2052
+ return applyAction(state, 'open', stack);
2053
+ if (!ui.drill) {
2054
+ const r = heatRows(state)[ui.selected];
2055
+ if (r)
2056
+ ui.drill = { id: r.id, selected: 0 };
2057
+ break;
2058
+ }
2059
+ const m = drillDetail(state)?.memberList[ui.drill.selected];
2060
+ if (!m)
2061
+ break;
2062
+ if (!live) {
2063
+ state.pendingToast = `token detail needs live mode (${m.symbol} ${m.mint.slice(0, 8)}…)`;
2064
+ break;
2065
+ }
2066
+ live.openToken(seedForMint(state, live, m.mint, m.symbol));
2067
+ stack.push(state.screen);
2068
+ state.screen = 'token';
1985
2069
  break;
1986
2070
  }
1987
2071
  case 'narrative.window': {
@@ -4283,6 +4367,11 @@ export function interactive(state, io, depth, args, proc = process) {
4283
4367
  // execution moment, pause / kill) draws over this terminal while it follows. Calm mode = reduced motion.
4284
4368
  state.agentHud = { story: followSession.story, clock: () => Date.now(), dataLabel: args.replay ? 'REPLAY DATA' : null };
4285
4369
  void followSession.start();
4370
+ // task-1427: the open coin's live holder/rug signals ride the spectator stream too (the streamer overlay's coin card).
4371
+ {
4372
+ const fs = followSession;
4373
+ state.live?.signals.listeners.add((sig) => fs.liveFlag(sig));
4374
+ }
4286
4375
  if (size()[1] < HUD_MIN_H)
4287
4376
  say('following agents over MCP · ctrl+p pause · ctrl+k stop · any key takes over', 'ai', 6);
4288
4377
  if (agentPaper)
@@ -6169,6 +6258,9 @@ export function interactive(state, io, depth, args, proc = process) {
6169
6258
  say(state.updateNotice, 'info', 8);
6170
6259
  state.updateNotice = null;
6171
6260
  }
6261
+ // task-1818: refresh the EXPERIMENTAL narratives tracker at most every 60 s while its heat table is on screen.
6262
+ if (state.screen === 'narrative' && state.narrativesUi?.mode === 'heat' && !state.paused)
6263
+ kickNarratives(state);
6172
6264
  // task-1508: the autotrade screen re-reads the paper runner's status file every 2 s while it is open.
6173
6265
  if (state.screen === 'autotrade' && state.autotradeUi)
6174
6266
  autotradeTick(state.autotradeUi);
@@ -116,7 +116,10 @@ export class McpFollowSession {
116
116
  };
117
117
  this.follow = new FollowController({ controls, activityCap: Math.max(1, opts.activityCap ?? 200) });
118
118
  this.story = new AgentStory(this.agentLabel);
119
- this.hub.on((ev) => { this.follow.apply(ev); this.story.apply(ev); });
119
+ // task-1427: a live signal the TERMINAL fired (liveFlag) rides the hub for the stream only; it is not an agent call
120
+ // or an agent thought, so the follow view and the story never fold it (the story would show a note as a thought).
121
+ this.hub.on((ev) => { if (ev.phase === 'note' && ev.tool === 'live_signal')
122
+ return; this.follow.apply(ev); this.story.apply(ev); });
120
123
  }
121
124
  /** Arm the rendezvous (so a `dexterm mcp` server starts emitting) and begin tailing. Idempotent. */
122
125
  async start() {
@@ -304,6 +307,23 @@ export class McpFollowSession {
304
307
  }
305
308
  catch { /* ignore */ }
306
309
  }
310
+ /**
311
+ * task-1427: a live holder / rug signal the owner's terminal fired for a coin (the task-1404 engine) → one `note`
312
+ * event in the hub, so the streamer overlay's coin card can show it with its age. Only while armed (an agent is
313
+ * being followed); it describes data the terminal already read, and carries nothing anyone can act on.
314
+ */
315
+ liveFlag(s) {
316
+ if (!this.armed)
317
+ return;
318
+ const detail = sanitizeDetail({ coin: { mint: s.mint }, liveFlags: [{ kind: s.kind, level: s.level, title: s.title, text: s.text, source: s.source, at: s.at, ageSec: Math.max(0, s.dataAgeMs / 1000), stale: s.stale }] });
319
+ if (!detail?.liveFlags)
320
+ return;
321
+ this.hub.emit({ phase: 'note', agent: this.currentAgent, transport: 'mcp', tool: 'live_signal', summary: `${s.title}: ${s.text}`.slice(0, 140), detail, ok: true, t: s.at });
322
+ try {
323
+ this.opts.onEvent?.();
324
+ }
325
+ catch { /* ignore */ }
326
+ }
307
327
  /** True when the terminal should mirror the agent: the agent still has control and is active. */
308
328
  canFollow() {
309
329
  if (this.follow.controlState !== 'agent')
@@ -0,0 +1,147 @@
1
+ // `dexterm narratives [top|detail ID|of MINT|export] [--db PATH] [--json] [--at ISO] [--lookback-h N] [--out FILE]`
2
+ // (task-1818, EXPERIMENTAL). The MEMES + NARRATIVES tracker over the history recorder: coins clustered by the theme
3
+ // their name/ticker carries, with per-window heat metrics and lifecycle states. Keyless, local, read-only on the DB.
4
+ // Measured heat, never advice.
5
+ import { DatabaseSync } from 'node:sqlite';
6
+ import { writeFileSync } from 'node:fs';
7
+ import { defaultDbPath } from '../../data/src/index.js';
8
+ import { trackerFromRecorder, toExport, renderTable, coinField, embedderFromEnv, HEAT_NOTE, LIFECYCLE_RULES } from '../../narratives/src/index.js';
9
+ export const NARRATIVES_HELP = `dexterm narratives: the MEMES + NARRATIVES tracker (task-1818, EXPERIMENTAL). No network, no model, no key.
10
+
11
+ dexterm narratives [top] the hottest narratives (rank = memetic traction score)
12
+ dexterm narratives detail ID one narrative: windows, lifecycle rule, member coins in spawn order
13
+ dexterm narratives of MINT which narrative a coin belongs to
14
+ dexterm narratives export [--out FILE] the dexterm-narratives/1 JSON the strategy tracker page reads
15
+
16
+ --db PATH defaults to DEXTERM_DB_PATH, else ~/.local/share/dexterm/dexterm.db (opened read-only)
17
+ --at ISO replay up to this time instead of now (point-in-time: only rows recorded before it)
18
+ --lookback-h N hours of history to replay (default 26)
19
+ --limit N rows (default 20)
20
+ --json machine-readable output
21
+
22
+ Coins are grouped by what their name/ticker says (ticker puns, animals, celebrities/politics, AI agents,
23
+ seasonal, copy-cats of a runner); a coin needs no real project to belong. Lifecycle states:
24
+ ${LIFECYCLE_RULES.map(([s, r]) => ` ${s.padEnd(9)} ${r}`).join('\n')}
25
+ ${HEAT_NOTE}
26
+ Embedder: DEXTERM_NARRATIVE_EMBEDDER=lexical (default, built in) | minilm (needs DEXTERM_EMBED_MODULE_DIR) | ollama[:model].`;
27
+ function parse(argv) {
28
+ const opts = {};
29
+ const pos = [];
30
+ for (let i = 0; i < argv.length; i++) {
31
+ const a = argv[i];
32
+ if (!a.startsWith('--')) {
33
+ pos.push(a);
34
+ continue;
35
+ }
36
+ const k = a.slice(2);
37
+ const v = argv[i + 1];
38
+ if (v !== undefined && !v.startsWith('--') && k !== 'json' && k !== 'help') {
39
+ opts[k] = v;
40
+ i++;
41
+ }
42
+ else
43
+ opts[k] = true;
44
+ }
45
+ return { pos, opts };
46
+ }
47
+ export async function narrativesCommand(argv, io) {
48
+ const { pos, opts } = parse(argv);
49
+ if (opts.help || pos[0] === 'help') {
50
+ io.stdout.write(NARRATIVES_HELP + '\n');
51
+ return 0;
52
+ }
53
+ const cmd = pos[0] ?? 'top';
54
+ if (!['top', 'detail', 'of', 'export'].includes(cmd)) {
55
+ io.stderr.write(`dexterm narratives: unknown command "${cmd}" (top | detail ID | of MINT | export)\n`);
56
+ return 2;
57
+ }
58
+ const path = typeof opts.db === 'string' ? opts.db : (io.env.DEXTERM_DB_PATH || defaultDbPath(io.env));
59
+ if (path === ':memory:') {
60
+ io.stderr.write('dexterm narratives: an in-memory DB has no history\n');
61
+ return 2;
62
+ }
63
+ const at = typeof opts.at === 'string' ? Date.parse(opts.at) : Date.now();
64
+ if (!Number.isFinite(at)) {
65
+ io.stderr.write('dexterm narratives: --at needs an ISO time\n');
66
+ return 2;
67
+ }
68
+ const lookbackH = typeof opts['lookback-h'] === 'string' ? Number(opts['lookback-h']) : 26;
69
+ if (!(Number.isFinite(lookbackH) && lookbackH > 0)) {
70
+ io.stderr.write('dexterm narratives: --lookback-h needs a number of hours above 0\n');
71
+ return 2;
72
+ }
73
+ const limitN = typeof opts.limit === 'string' ? Math.floor(Number(opts.limit)) : 20;
74
+ if (!Number.isFinite(limitN) || limitN < 1) {
75
+ io.stderr.write('dexterm narratives: --limit needs a whole number of 1 or more\n');
76
+ return 2;
77
+ }
78
+ const limit = Math.min(200, limitN);
79
+ let db = null;
80
+ try {
81
+ db = new DatabaseSync(path, { readOnly: true });
82
+ if (!db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'token_snapshots'").get()) {
83
+ io.stderr.write(`dexterm narratives: no recorded history in ${path} (run the app with the recorder on first)\n`);
84
+ return 2;
85
+ }
86
+ const t0 = performance.now();
87
+ const tr = await trackerFromRecorder(db, { now: at, lookbackMs: lookbackH * 3_600_000, embedder: embedderFromEnv(io.env) });
88
+ const ms = Math.round(performance.now() - t0);
89
+ if (cmd === 'export') {
90
+ const ex = toExport(tr, { limit: Math.max(limit, 50), now: at });
91
+ const json = JSON.stringify(ex, null, 1) + '\n';
92
+ if (typeof opts.out === 'string') {
93
+ writeFileSync(opts.out, json);
94
+ io.stdout.write(`wrote ${opts.out}: ${ex.narratives.length} narratives over ${ex.coins} coins (${ms} ms)\n`);
95
+ }
96
+ else
97
+ io.stdout.write(json);
98
+ return 0;
99
+ }
100
+ if (cmd === 'detail' || cmd === 'of') {
101
+ const key = pos[1];
102
+ if (!key) {
103
+ io.stderr.write(`dexterm narratives ${cmd}: needs ${cmd === 'detail' ? 'a narrative id' : 'a mint'}\n`);
104
+ return 2;
105
+ }
106
+ const n = cmd === 'of' ? tr.narrativeOf(key) : null;
107
+ if (cmd === 'of' && !n) {
108
+ io.stdout.write(opts.json ? JSON.stringify({ mint: key, narrative: null, note: 'not recorded in the lookback window' }) + '\n' : `${coinField(null)} (no recorded sighting in the last ${lookbackH} h)\n`);
109
+ return 0;
110
+ }
111
+ const d = tr.detail(cmd === 'of' ? n.narrative : key);
112
+ if (!d) {
113
+ io.stderr.write(`dexterm narratives: unknown narrative id ${key}\n`);
114
+ return 2;
115
+ }
116
+ if (opts.json) {
117
+ io.stdout.write(JSON.stringify(cmd === 'of' ? { ...n, detail: d } : d, null, 1) + '\n');
118
+ return 0;
119
+ }
120
+ if (n)
121
+ io.stdout.write(coinField(n) + '\n');
122
+ io.stdout.write(renderTable([d], 150) + '\n\n');
123
+ io.stdout.write(`lifecycle rule: ${d.lifecycleRule}\nmembers (spawn order, newest ${Math.min(limit, d.memberList.length)} of ${d.memberList.length}):\n`);
124
+ for (const m of d.memberList.slice(-limit))
125
+ io.stdout.write(` #${String(m.spawn).padEnd(4)} ${m.symbol.padEnd(12)} ${m.name.slice(0, 28).padEnd(28)} ${m.x == null ? ' —' : (m.x.toFixed(2) + 'x').padStart(7)} ${m.mint}\n`);
126
+ return 0;
127
+ }
128
+ const rows = tr.top(limit);
129
+ if (opts.json) {
130
+ io.stdout.write(JSON.stringify(toExport(tr, { limit, now: at }), null, 1) + '\n');
131
+ return 0;
132
+ }
133
+ io.stdout.write(`NARRATIVES (EXPERIMENTAL) · as of ${tr.lastTickAt ? new Date(tr.lastTickAt).toISOString() : '—'} · ${tr.coins.size} coins over ${lookbackH} h · embedder ${tr.embedder.id} · ${ms} ms\n`);
134
+ io.stdout.write(renderTable(rows, 150) + '\n');
135
+ return 0;
136
+ }
137
+ catch (e) {
138
+ io.stderr.write(`dexterm narratives: ${e instanceof Error ? e.message : String(e)}\n`);
139
+ return 1;
140
+ }
141
+ finally {
142
+ try {
143
+ db?.close();
144
+ }
145
+ catch { /* ignore */ }
146
+ }
147
+ }
@@ -0,0 +1,114 @@
1
+ import { NarrativeTracker, RecorderNarratives, coinField, embedderFromEnv, sanitize } from '../../narratives/src/index.js';
2
+ /** Later refreshes run at most this often (the first one runs on open). */
3
+ export const NARRATIVES_REFRESH_MS = 60_000;
4
+ /** Rows in the heat table. */
5
+ export const HEAT_LIMIT = 50;
6
+ export const UNTRUSTED_NOTE = 'names are untrusted, shown control-stripped';
7
+ const services = new WeakMap();
8
+ const screenTrackers = new WeakMap();
9
+ export function narrativesUiOf(state) {
10
+ if (!state.narrativesUi)
11
+ state.narrativesUi = { mode: 'board', selected: 0, drill: null, busy: false, error: null, source: null, refreshedAt: 0, buildMs: null };
12
+ return state.narrativesUi;
13
+ }
14
+ const dbOf = (state) => state.live?.recorder?.database ?? null;
15
+ export const narrativesSource = (state) => (dbOf(state) ? 'recorded' : 'screen');
16
+ /** The tracker ALREADY built for this state, or null. Never builds (the coin pane / rules / renders call this). */
17
+ export function currentTracker(state) {
18
+ const db = dbOf(state);
19
+ return (db ? services.get(db)?.tracker ?? null : null) ?? screenTrackers.get(state) ?? null;
20
+ }
21
+ /** Install a tracker for a state with no recorder DB (tests; the screen-rows fallback uses the same slot). */
22
+ export function setNarrativeTracker(state, tr) { if (tr)
23
+ screenTrackers.set(state, tr);
24
+ else
25
+ screenTrackers.delete(state); }
26
+ /**
27
+ * Build or refresh the tracker. Recorder DB: the per-process RecorderNarratives (first call = the 26 h replay, later
28
+ * calls replay only new rows; it self-throttles to NARRATIVES_REFRESH_MS). No DB: the visible screener rows are fed
29
+ * to a per-state tracker (first sightings once, a sample per row each call) and ticked now.
30
+ */
31
+ export async function ensureNarratives(state, now = Date.now()) {
32
+ const db = dbOf(state);
33
+ if (db) {
34
+ let svc = services.get(db);
35
+ if (!svc)
36
+ services.set(db, (svc = new RecorderNarratives(db, { embedder: embedderFromEnv(process.env), minRefreshMs: NARRATIVES_REFRESH_MS })));
37
+ return svc.refresh(now);
38
+ }
39
+ const rows = state.screener.rows.filter((r) => r.mint && !r.skeleton);
40
+ let tr = screenTrackers.get(state) ?? null;
41
+ if (!rows.length)
42
+ return tr;
43
+ if (!tr)
44
+ screenTrackers.set(state, (tr = new NarrativeTracker()));
45
+ const at = (r) => Math.min(now, r.fetchedAt ?? now);
46
+ await tr.addCoins(rows.map((r) => ({ mint: r.mint, t: at(r), symbol: r.symbol, name: r.name })));
47
+ for (const r of rows)
48
+ tr.addSample({ mint: r.mint, t: at(r), price: r.price, liq: r.liq, mcap: r.mcap, vol5m: r.vol5m, vol1h: r.vol1h, vol6h: r.vol6h, vol24h: r.vol24 });
49
+ tr.tick(now, true);
50
+ return tr;
51
+ }
52
+ /**
53
+ * Start a build/refresh in the background unless one is running or the last finished < NARRATIVES_REFRESH_MS ago
54
+ * (`force` skips the age check). The work starts on a short timer so the "building…" frame paints first; the recorder
55
+ * replay then yields to the event loop every 40 ms (RecorderNarratives), so frames keep drawing. Returns whether it started.
56
+ */
57
+ export function kickNarratives(state, opts = {}) {
58
+ const ui = narrativesUiOf(state);
59
+ const now = opts.now ?? Date.now();
60
+ if (ui.busy)
61
+ return false;
62
+ if (!opts.force && ui.refreshedAt && now - ui.refreshedAt < NARRATIVES_REFRESH_MS)
63
+ return false;
64
+ ui.busy = true;
65
+ ui.error = null;
66
+ const run = () => {
67
+ const t0 = Date.now();
68
+ void ensureNarratives(state, Date.now())
69
+ .then(() => { ui.source = narrativesSource(state); ui.buildMs = Date.now() - t0; })
70
+ .catch((e) => { ui.error = e instanceof Error ? e.message : String(e); })
71
+ .finally(() => { ui.busy = false; ui.refreshedAt = Date.now(); opts.onDone?.(); });
72
+ };
73
+ const tm = setTimeout(run, opts.delayMs ?? 40);
74
+ tm.unref?.();
75
+ return true;
76
+ }
77
+ /** The heat table's rows (control-stripped labels). The screen-rows fallback has no history, so solo coins show too. */
78
+ export function heatRows(state) {
79
+ const tr = currentTracker(state);
80
+ if (!tr)
81
+ return [];
82
+ return tr.top(HEAT_LIMIT, { minMembers: narrativesSource(state) === 'screen' ? 1 : 2 }).map((s) => sanitize(s));
83
+ }
84
+ /** The drilled narrative (with its member list in spawn order), or null. */
85
+ export function drillDetail(state) {
86
+ const id = state.narrativesUi?.drill?.id;
87
+ const tr = currentTracker(state);
88
+ if (!id || !tr)
89
+ return null;
90
+ const d = tr.detail(id);
91
+ if (d)
92
+ d.memberList.sort((a, b) => a.spawn - b.spawn);
93
+ return d;
94
+ }
95
+ /** One coin's narrative from the already-built tracker (null = not built or not tracked). */
96
+ export function narrativeFor(state, mint) {
97
+ return currentTracker(state)?.narrativeOf(mint) ?? null;
98
+ }
99
+ /** The coin pane's Narrative row value + level: warn when copy-cat saturated or fading/dead, else the neutral na (heat is
100
+ * never shown as a green "ok": it is not a quality mark of the coin). */
101
+ export function narrativeCell(state, mint) {
102
+ if (!currentTracker(state))
103
+ return ['open N → n to build', 'na'];
104
+ const n = narrativeFor(state, mint);
105
+ const text = coinField(n).replace(/^narrative: /, '');
106
+ if (!n)
107
+ return [text, 'na'];
108
+ return [text, n.saturated || n.lifecycle === 'fading' || n.lifecycle === 'dead' ? 'warn' : 'na'];
109
+ }
110
+ /** The rules' narrative fields for one coin (all null when not built / not tracked; a rule clause on them is then false). */
111
+ export function narrativeRuleFields(state, mint) {
112
+ const n = narrativeFor(state, mint);
113
+ return { narrativeRank: n?.rank ?? null, narrativeScore: n?.score ?? null, narrativeLifecycle: n?.lifecycle ?? null, narrativeSaturated: n ? n.saturated : null };
114
+ }