dexterm-cli 0.2.0 → 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 (138) hide show
  1. package/dist/packages/agents/src/spectator.js +28 -2
  2. package/dist/packages/agents/src/tiers.js +3 -0
  3. package/dist/packages/agents/src/tools.js +3 -0
  4. package/dist/packages/autotrade/src/backtest.js +6 -2
  5. package/dist/packages/autotrade/src/fleet.js +2 -2
  6. package/dist/packages/autotrade/src/index.js +2 -0
  7. package/dist/packages/autotrade/src/rugproof-probe.js +415 -0
  8. package/dist/packages/autotrade/src/rugproof.js +231 -0
  9. package/dist/packages/autotrade/src/runner.js +132 -17
  10. package/dist/packages/autotrade/src/service-install.js +6 -4
  11. package/dist/packages/autotrade/src/sessions.js +16 -7
  12. package/dist/packages/autotrade/src/strategy-stats.js +344 -0
  13. package/dist/packages/autotrade/src/strategy.js +12 -1
  14. package/dist/packages/classifier/src/calibrate.js +138 -0
  15. package/dist/packages/classifier/src/classify.js +107 -0
  16. package/dist/packages/classifier/src/config.js +220 -0
  17. package/dist/packages/classifier/src/dossier.js +95 -0
  18. package/dist/packages/classifier/src/hardware.js +63 -0
  19. package/dist/packages/classifier/src/index.js +18 -0
  20. package/dist/packages/classifier/src/models.js +111 -0
  21. package/dist/packages/classifier/src/providers/heuristic.js +77 -0
  22. package/dist/packages/classifier/src/providers/http.js +63 -0
  23. package/dist/packages/classifier/src/providers/jev-open.js +86 -0
  24. package/dist/packages/classifier/src/providers/llm.js +89 -0
  25. package/dist/packages/classifier/src/providers/systemone-http.js +95 -0
  26. package/dist/packages/classifier/src/questions.js +97 -0
  27. package/dist/packages/classifier/src/registry.js +42 -0
  28. package/dist/packages/classifier/src/snapshot.js +31 -0
  29. package/dist/packages/classifier/src/systemone.js +88 -0
  30. package/dist/packages/classifier/src/types.js +18 -0
  31. package/dist/packages/cli/assets/dash/dash.css +139 -0
  32. package/dist/packages/cli/assets/dash/dash.js +200 -0
  33. package/dist/packages/cli/assets/dash/icon.svg +1 -0
  34. package/dist/packages/cli/assets/dash/index.html +36 -0
  35. package/dist/packages/cli/src/app-help-gen.js +12 -0
  36. package/dist/packages/cli/src/autotrade-cmd/fleet.js +9 -2
  37. package/dist/packages/cli/src/autotrade-cmd/run.js +11 -1
  38. package/dist/packages/cli/src/autotrade-cmd/signal.js +2 -2
  39. package/dist/packages/cli/src/classifier-cmd.js +353 -0
  40. package/dist/packages/cli/src/classifier-live.js +81 -0
  41. package/dist/packages/cli/src/dash/data.js +217 -0
  42. package/dist/packages/cli/src/dash/server.js +92 -0
  43. package/dist/packages/cli/src/dash-cmd.js +113 -0
  44. package/dist/packages/cli/src/doctor.js +34 -0
  45. package/dist/packages/cli/src/live/screens/token-live.js +4 -0
  46. package/dist/packages/cli/src/live/signals.js +8 -0
  47. package/dist/packages/cli/src/main.js +98 -6
  48. package/dist/packages/cli/src/mcp-follow.js +21 -1
  49. package/dist/packages/cli/src/narratives-cmd.js +147 -0
  50. package/dist/packages/cli/src/narratives.js +114 -0
  51. package/dist/packages/cli/src/overlay/model.js +147 -0
  52. package/dist/packages/cli/src/overlay/page.js +146 -0
  53. package/dist/packages/cli/src/overlay/pnl.js +75 -0
  54. package/dist/packages/cli/src/overlay/server.js +170 -0
  55. package/dist/packages/cli/src/overlay-cmd.js +367 -0
  56. package/dist/packages/cli/src/record-cmd.js +82 -11
  57. package/dist/packages/cli/src/rules-cmd.js +2 -1
  58. package/dist/packages/cli/src/rules.js +24 -4
  59. package/dist/packages/cli/src/screens/narrative.js +154 -2
  60. package/dist/packages/cli/src/state.js +1 -0
  61. package/dist/packages/cli/src/version.js +1 -1
  62. package/dist/packages/custom/src/rules.js +5 -1
  63. package/dist/packages/mcp/assets/kb/app-help/classification-models.md +41 -0
  64. package/dist/packages/mcp/assets/kb/app-help/narratives-tracker.md +45 -0
  65. package/dist/packages/mcp/assets/kb/app-help/strategy-dashboard.md +38 -0
  66. package/dist/packages/mcp/assets/kb/app-help/streamer-overlay.md +36 -0
  67. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-rug-proof-entries-experimental.md +52 -0
  68. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-always-on-service.md +3 -1
  69. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-strategy-dashboard-dexterm-dash.md +37 -0
  70. package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +3 -1
  71. package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
  72. package/dist/packages/mcp/assets/kb/docs/docs-beginners-copy-and-paste-without-surprises.md +0 -1
  73. package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-dexterm-says-command-not-found.md +2 -2
  74. package/dist/packages/mcp/assets/kb/docs/docs-beginners-reopen-dexterm-later.md +1 -1
  75. package/dist/packages/mcp/assets/kb/docs/docs-beginners-terminal-shell-prompt-command-output-and-path.md +2 -2
  76. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +1 -1
  77. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows.md +1 -1
  78. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-ladder.md +2 -0
  79. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat.md +2 -0
  80. package/dist/packages/mcp/assets/kb/docs/docs-classifier-hosted-jev-with-your-own-key.md +34 -0
  81. package/dist/packages/mcp/assets/kb/docs/docs-classifier-how-they-measured-on-dexterm-s-own-history.md +52 -0
  82. package/dist/packages/mcp/assets/kb/docs/docs-classifier-set-up-kev-on-your-own-machine.md +39 -0
  83. package/dist/packages/mcp/assets/kb/docs/docs-classifier-settings.md +35 -0
  84. package/dist/packages/mcp/assets/kb/docs/docs-classifier-the-choices.md +36 -0
  85. package/dist/packages/mcp/assets/kb/docs/docs-classifier-where-it-plugs-in.md +29 -0
  86. package/dist/packages/mcp/assets/kb/docs/docs-classifier-which-model-for-which-machine.md +42 -0
  87. package/dist/packages/mcp/assets/kb/docs/docs-classifier.md +32 -0
  88. package/dist/packages/mcp/assets/kb/docs/docs-cli-invocations.md +1 -1
  89. package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -0
  90. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
  91. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +1 -0
  92. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
  93. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +2 -2
  94. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +2 -11
  95. package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +2 -2
  96. package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +2 -2
  97. package/dist/packages/mcp/assets/kb/docs/docs-larp-paste-a-coin-on-the-web.md +1 -1
  98. package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +1 -1
  99. package/dist/packages/mcp/assets/kb/docs/docs-narratives-how-coins-are-grouped.md +39 -0
  100. package/dist/packages/mcp/assets/kb/docs/docs-narratives-limits.md +29 -0
  101. package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-is-measured.md +44 -0
  102. package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-testing-showed.md +43 -0
  103. package/dist/packages/mcp/assets/kb/docs/docs-narratives-where-to-find-it.md +37 -0
  104. package/dist/packages/mcp/assets/kb/docs/docs-narratives.md +29 -0
  105. package/dist/packages/mcp/assets/kb/docs/docs-overlay-before-you-start.md +33 -0
  106. package/dist/packages/mcp/assets/kb/docs/docs-overlay-limits.md +30 -0
  107. package/dist/packages/mcp/assets/kb/docs/docs-overlay-options.md +36 -0
  108. package/dist/packages/mcp/assets/kb/docs/docs-overlay-security-model.md +36 -0
  109. package/dist/packages/mcp/assets/kb/docs/docs-overlay-set-it-up-in-obs-step-by-step.md +49 -0
  110. package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-pnl-scene-paper-and-live.md +34 -0
  111. package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-three-scenes.md +35 -0
  112. package/dist/packages/mcp/assets/kb/docs/docs-overlay-what-is-redacted.md +31 -0
  113. package/dist/packages/mcp/assets/kb/docs/docs-overlay.md +25 -0
  114. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +1 -1
  115. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-is.md +2 -0
  116. package/dist/packages/mcp/assets/kb/docs/docs-screens-bubble-map.md +1 -1
  117. package/dist/packages/mcp/assets/kb/docs/docs-spectator-streaming-it.md +1 -0
  118. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +1 -1
  119. package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +2 -2
  120. package/dist/packages/mcp/assets/kb/entries/narratives-tracker.md +45 -0
  121. package/dist/packages/mcp/src/server.js +104 -0
  122. package/dist/packages/mcp/src/spectator-sink.js +27 -0
  123. package/dist/packages/narratives/src/cluster.js +193 -0
  124. package/dist/packages/narratives/src/embed.js +184 -0
  125. package/dist/packages/narratives/src/export.js +60 -0
  126. package/dist/packages/narratives/src/index.js +10 -0
  127. package/dist/packages/narratives/src/label.js +48 -0
  128. package/dist/packages/narratives/src/normalize.js +179 -0
  129. package/dist/packages/narratives/src/recorder.js +93 -0
  130. package/dist/packages/narratives/src/tracker.js +328 -0
  131. package/dist/packages/recorder/src/exit-probes.js +215 -0
  132. package/dist/packages/recorder/src/index.js +1 -0
  133. package/dist/packages/recorder/src/recorder.js +11 -1
  134. package/dist/packages/recorder/src/retention.js +4 -0
  135. package/dist/packages/recorder/src/schema.js +20 -1
  136. package/dist/packages/tui/src/keymap.js +5 -0
  137. package/package.json +3 -1
  138. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-powershell-experimental.md +0 -39
@@ -0,0 +1,170 @@
1
+ // dexterm — STREAMER OVERLAY page server (task-1427). Serves the OBS browser-source scenes on loopback with the
2
+ // popup host's security model (packages/popup/src/server.ts):
3
+ // * listens on 127.0.0.1 only (a random port unless --port), never 0.0.0.0 / ::; the remote must be loopback;
4
+ // * a scene URL carries a single-use TICKET (32 random bytes). OBS needs time to be pasted into, so its TTL is
5
+ // longer than a popup's (10 min by default) — but it still buys exactly one page load;
6
+ // * the page embeds a single-use WS TOKEN (30 s) accepted once for the WebSocket upgrade;
7
+ // * strict Host check on every request (DNS-rebind guard), strict Origin check on the upgrade; no CORS header is
8
+ // ever set; anything unknown or unauthorised gets an empty 404 and is reported as a probe;
9
+ // * the page's JS/CSS are inlined under a per-page nonce CSP: no remote content, no eval, no inline handlers;
10
+ // the only images are the PnL card PNG the socket delivers as a data: URL;
11
+ // * the socket is OUTBOUND ONLY: the server never parses what a page sends. Every inbound text frame is counted
12
+ // as dropped and ignored (pings are answered by the framing layer). There is no inbound control path: the
13
+ // server is constructed with nothing it could control, it only pushes view models it is handed.
14
+ import http from 'node:http';
15
+ import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
16
+ import { WsConn } from '../../../agents/src/index.js';
17
+ import { OVERLAY_SCENES } from "./model.js";
18
+ import { renderOverlayPage, OVERLAY_THEMES } from "./page.js";
19
+ export const OVERLAY_PAGES = [...OVERLAY_SCENES, 'all'];
20
+ export const OVERLAY_TICKET_TTL_MS = 10 * 60_000;
21
+ const WS_TOKEN_TTL_MS = 30_000;
22
+ const LOOPBACK = new Set(['127.0.0.1', '::ffff:127.0.0.1', '::1']);
23
+ const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
24
+ function safeEq(a, b) {
25
+ const ba = Buffer.from(a), bb = Buffer.from(b);
26
+ return ba.length === bb.length && timingSafeEqual(ba, bb);
27
+ }
28
+ const sanitize = (s, n = 80) => String(s ?? '').replace(/[^\x20-\x7e]/g, '?').slice(0, n);
29
+ export async function createOverlayServer(opts = {}) {
30
+ const onEvent = opts.onEvent ?? (() => { });
31
+ const now = opts.now ?? (() => Date.now());
32
+ const ttl = Math.max(1000, opts.ticketTtlMs ?? OVERLAY_TICKET_TTL_MS);
33
+ const tickets = new Map();
34
+ const wsTokens = new Map();
35
+ const viewers = new Set();
36
+ const latest = new Map();
37
+ let dropped = 0;
38
+ let port = 0;
39
+ const probe = (req, reason) => onEvent({ ev: 'probe.rejected', reason, method: req.method ?? '', path: sanitize((req.url ?? '').replace(/([?&](p|t)=)[^&]*/g, '$1…')) });
40
+ const reject = (req, res, reason) => {
41
+ probe(req, reason);
42
+ res.writeHead(404, { 'content-length': '0', 'cache-control': 'no-store' });
43
+ res.end();
44
+ };
45
+ const baseChecks = (req) => {
46
+ if (!LOOPBACK.has(req.socket.remoteAddress ?? ''))
47
+ return 'remote-not-loopback';
48
+ if (req.headers.host !== `127.0.0.1:${port}`)
49
+ return 'bad-host';
50
+ return null;
51
+ };
52
+ const sweep = () => {
53
+ const t = now();
54
+ for (const [k, v] of tickets)
55
+ if (v.exp < t)
56
+ tickets.delete(k);
57
+ for (const [k, v] of wsTokens)
58
+ if (v.exp < t)
59
+ wsTokens.delete(k);
60
+ };
61
+ const server = http.createServer((req, res) => {
62
+ const bad = baseChecks(req);
63
+ if (bad)
64
+ return reject(req, res, bad);
65
+ const url = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);
66
+ if (req.method === 'GET' && url.pathname.startsWith('/overlay/')) {
67
+ sweep();
68
+ const page = url.pathname.slice('/overlay/'.length);
69
+ const p = url.searchParams.get('p') ?? '';
70
+ const key = [...tickets.keys()].find((k) => safeEq(k, p));
71
+ const t = key ? tickets.get(key) : undefined;
72
+ if (!key || !t || t.exp < now() || t.page !== page || !OVERLAY_PAGES.includes(page))
73
+ return reject(req, res, 'bad-ticket');
74
+ tickets.delete(key); // single use: a reload or a copied URL is refused
75
+ const wsToken = randomBytes(32).toString('base64url');
76
+ wsTokens.set(wsToken, { page, exp: now() + WS_TOKEN_TTL_MS });
77
+ const nonce = randomBytes(16).toString('base64url');
78
+ const html = renderOverlayPage({ page, theme: t.theme, nonce, wsUrl: `ws://127.0.0.1:${port}/ws?t=${wsToken}` });
79
+ res.writeHead(200, {
80
+ 'content-type': 'text/html; charset=utf-8',
81
+ 'cache-control': 'no-store',
82
+ 'referrer-policy': 'no-referrer',
83
+ 'x-content-type-options': 'nosniff',
84
+ 'cross-origin-opener-policy': 'same-origin',
85
+ 'cross-origin-resource-policy': 'same-origin',
86
+ 'permissions-policy': 'camera=(), microphone=(), geolocation=(), payment=(), usb=()',
87
+ 'content-security-policy': `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src ws://127.0.0.1:${port}; img-src data:; font-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; object-src 'none'`,
88
+ });
89
+ onEvent({ ev: 'page.served', page });
90
+ return res.end(html);
91
+ }
92
+ return reject(req, res, 'unknown-route');
93
+ });
94
+ server.on('upgrade', (req, socket) => {
95
+ const fail = (reason) => {
96
+ probe(req, reason);
97
+ try {
98
+ socket.write('HTTP/1.1 404 Not Found\r\ncontent-length: 0\r\nconnection: close\r\n\r\n');
99
+ }
100
+ catch { /* gone */ }
101
+ socket.destroy();
102
+ };
103
+ const bad = baseChecks(req);
104
+ if (bad)
105
+ return fail(bad);
106
+ if (req.headers.origin !== `http://127.0.0.1:${port}`)
107
+ return fail('bad-origin');
108
+ const url = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);
109
+ if (url.pathname !== '/ws')
110
+ return fail('unknown-route');
111
+ sweep();
112
+ const tok = url.searchParams.get('t') ?? '';
113
+ const key = [...wsTokens.keys()].find((k) => safeEq(k, tok));
114
+ const entry = key ? wsTokens.get(key) : undefined;
115
+ if (!key || !entry || entry.exp < now())
116
+ return fail('bad-ws-token');
117
+ wsTokens.delete(key);
118
+ if (String(req.headers['sec-websocket-version']) !== '13' || !req.headers['sec-websocket-key'])
119
+ return fail('bad-ws-handshake');
120
+ const accept = createHash('sha1').update(req.headers['sec-websocket-key'] + WS_GUID).digest('base64');
121
+ socket.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\n\r\n`);
122
+ const viewer = { page: entry.page, conn: new WsConn(socket, req.socket.remoteAddress ?? '') };
123
+ // Outbound only: whatever a page sends is never parsed, never forwarded, never acted on.
124
+ viewer.conn.onMessage = () => { dropped++; onEvent({ ev: 'viewer.dropped', page: viewer.page }); };
125
+ viewer.conn.onClose = () => { viewers.delete(viewer); onEvent({ ev: 'viewer.closed', page: viewer.page }); };
126
+ viewers.add(viewer);
127
+ onEvent({ ev: 'viewer.connected', page: viewer.page });
128
+ viewer.conn.send({ type: 'hello', protocol: 'dexterm-overlay/1', page: viewer.page });
129
+ for (const [scene, view] of latest)
130
+ if (viewer.page === 'all' || viewer.page === scene)
131
+ viewer.conn.send({ type: 'view', scene, view });
132
+ });
133
+ server.headersTimeout = 5000;
134
+ server.requestTimeout = 10_000;
135
+ server.keepAliveTimeout = 5000;
136
+ server.maxHeadersCount = 40;
137
+ await new Promise((resolve, rejectListen) => {
138
+ server.once('error', rejectListen);
139
+ server.listen(opts.port ?? 0, '127.0.0.1', () => { server.off('error', rejectListen); resolve(); });
140
+ });
141
+ port = server.address().port;
142
+ return {
143
+ port,
144
+ origin: `http://127.0.0.1:${port}`,
145
+ issueTicket(page, theme = 'cyberspace') {
146
+ if (!OVERLAY_PAGES.includes(page))
147
+ throw new Error(`unknown overlay scene ${JSON.stringify(page)} (one of ${OVERLAY_PAGES.join(', ')})`);
148
+ if (!(theme in OVERLAY_THEMES))
149
+ throw new Error(`unknown overlay theme ${JSON.stringify(theme)} (one of ${Object.keys(OVERLAY_THEMES).join(', ')})`);
150
+ sweep();
151
+ const ticket = randomBytes(32).toString('base64url');
152
+ tickets.set(ticket, { page, theme, exp: now() + ttl });
153
+ return `http://127.0.0.1:${port}/overlay/${page}?p=${ticket}`;
154
+ },
155
+ push(scene, view) {
156
+ latest.set(scene, view);
157
+ for (const v of viewers)
158
+ if (v.page === 'all' || v.page === scene)
159
+ v.conn.send({ type: 'view', scene, view });
160
+ },
161
+ get viewers() { return viewers.size; },
162
+ get dropped() { return dropped; },
163
+ close() {
164
+ for (const v of [...viewers])
165
+ v.conn.close(1001);
166
+ viewers.clear();
167
+ return new Promise((r) => server.close(() => r()));
168
+ },
169
+ };
170
+ }
@@ -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
+ }