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.
- package/dist/packages/agents/src/spectator.js +31 -3
- package/dist/packages/agents/src/tiers.js +12 -0
- package/dist/packages/agents/src/tools.js +13 -5
- package/dist/packages/autotrade/assets/limits.schema.json +569 -0
- package/dist/packages/autotrade/examples/executor/README.md +70 -0
- package/dist/packages/autotrade/examples/executor/jsonl-tail-executor.mjs +57 -0
- package/dist/packages/autotrade/examples/executor/kit.mjs +144 -0
- package/dist/packages/autotrade/examples/executor/signal-cmd-executor.mjs +25 -0
- package/dist/packages/autotrade/examples/executor/telegram-bot-recipe.md +47 -0
- package/dist/packages/autotrade/examples/executor/webhook-executor.mjs +47 -0
- package/dist/packages/autotrade/src/backtest.js +7 -2
- package/dist/packages/autotrade/src/chat/config.js +4 -2
- package/dist/packages/autotrade/src/chat/control.js +1 -0
- package/dist/packages/autotrade/src/chat/notify.js +16 -6
- package/dist/packages/autotrade/src/chat/secrets.js +25 -18
- package/dist/packages/autotrade/src/chat/service.js +3 -0
- package/dist/packages/autotrade/src/engine.js +16 -2
- package/dist/packages/autotrade/src/executor-health.js +217 -0
- package/dist/packages/autotrade/src/fleet.js +5 -2
- package/dist/packages/autotrade/src/forward.js +355 -0
- package/dist/packages/autotrade/src/index.js +21 -0
- package/dist/packages/autotrade/src/journey.js +477 -0
- package/dist/packages/autotrade/src/limits.js +682 -0
- package/dist/packages/autotrade/src/outbox.js +394 -0
- package/dist/packages/autotrade/src/paper-limits.js +87 -0
- package/dist/packages/autotrade/src/real-mode.js +688 -0
- package/dist/packages/autotrade/src/receipts.js +330 -0
- package/dist/packages/autotrade/src/report.js +168 -0
- package/dist/packages/autotrade/src/rugproof-probe.js +415 -0
- package/dist/packages/autotrade/src/rugproof.js +231 -0
- package/dist/packages/autotrade/src/runner.js +194 -28
- package/dist/packages/autotrade/src/service-guard.js +125 -0
- package/dist/packages/autotrade/src/service-install.js +215 -0
- package/dist/packages/autotrade/src/service-recover.js +232 -0
- package/dist/packages/autotrade/src/service.js +289 -0
- package/dist/packages/autotrade/src/sessions.js +16 -7
- package/dist/packages/autotrade/src/signal-channels.js +312 -0
- package/dist/packages/autotrade/src/signal.js +196 -0
- package/dist/packages/autotrade/src/strategy-stats.js +344 -0
- package/dist/packages/autotrade/src/strategy.js +15 -2
- package/dist/packages/autotrade/src/trade-ledger.js +2 -2
- package/dist/packages/classifier/src/calibrate.js +138 -0
- package/dist/packages/classifier/src/classify.js +107 -0
- package/dist/packages/classifier/src/config.js +220 -0
- package/dist/packages/classifier/src/dossier.js +95 -0
- package/dist/packages/classifier/src/hardware.js +63 -0
- package/dist/packages/classifier/src/index.js +18 -0
- package/dist/packages/classifier/src/models.js +111 -0
- package/dist/packages/classifier/src/providers/heuristic.js +77 -0
- package/dist/packages/classifier/src/providers/http.js +63 -0
- package/dist/packages/classifier/src/providers/jev-open.js +86 -0
- package/dist/packages/classifier/src/providers/llm.js +89 -0
- package/dist/packages/classifier/src/providers/systemone-http.js +95 -0
- package/dist/packages/classifier/src/questions.js +97 -0
- package/dist/packages/classifier/src/registry.js +42 -0
- package/dist/packages/classifier/src/snapshot.js +31 -0
- package/dist/packages/classifier/src/systemone.js +88 -0
- package/dist/packages/classifier/src/types.js +18 -0
- package/dist/packages/cli/assets/dash/dash.css +139 -0
- package/dist/packages/cli/assets/dash/dash.js +200 -0
- package/dist/packages/cli/assets/dash/icon.svg +1 -0
- package/dist/packages/cli/assets/dash/index.html +36 -0
- package/dist/packages/cli/src/analyst/backend.js +3 -2
- package/dist/packages/cli/src/analyst/codex.js +3 -2
- package/dist/packages/cli/src/app-help-gen.js +26 -2
- package/dist/packages/cli/src/autotrade-cmd/audit.js +19 -2
- package/dist/packages/cli/src/autotrade-cmd/backtest.js +21 -4
- package/dist/packages/cli/src/autotrade-cmd/executor.js +91 -0
- package/dist/packages/cli/src/autotrade-cmd/fleet.js +16 -5
- package/dist/packages/cli/src/autotrade-cmd/forward.js +283 -0
- package/dist/packages/cli/src/autotrade-cmd/journey.js +208 -0
- package/dist/packages/cli/src/autotrade-cmd/limits.js +393 -0
- package/dist/packages/cli/src/autotrade-cmd/receipt.js +63 -0
- package/dist/packages/cli/src/autotrade-cmd/report.js +79 -0
- package/dist/packages/cli/src/autotrade-cmd/run.js +268 -14
- package/dist/packages/cli/src/autotrade-cmd/service.js +873 -0
- package/dist/packages/cli/src/autotrade-cmd/signal.js +824 -0
- package/dist/packages/cli/src/autotrade-cmd.js +50 -4
- package/dist/packages/cli/src/classifier-cmd.js +353 -0
- package/dist/packages/cli/src/classifier-live.js +81 -0
- package/dist/packages/cli/src/dash/data.js +217 -0
- package/dist/packages/cli/src/dash/server.js +92 -0
- package/dist/packages/cli/src/dash-cmd.js +113 -0
- package/dist/packages/cli/src/doctor.js +34 -0
- package/dist/packages/cli/src/ledger-cmd.js +4 -2
- package/dist/packages/cli/src/live/screens/token-live.js +4 -0
- package/dist/packages/cli/src/live/signals.js +8 -0
- package/dist/packages/cli/src/main.js +98 -6
- package/dist/packages/cli/src/mcp-follow.js +21 -1
- package/dist/packages/cli/src/narratives-cmd.js +147 -0
- package/dist/packages/cli/src/narratives.js +114 -0
- package/dist/packages/cli/src/overlay/model.js +147 -0
- package/dist/packages/cli/src/overlay/page.js +146 -0
- package/dist/packages/cli/src/overlay/pnl.js +75 -0
- package/dist/packages/cli/src/overlay/server.js +170 -0
- package/dist/packages/cli/src/overlay-cmd.js +367 -0
- package/dist/packages/cli/src/pnl-card.js +4 -0
- package/dist/packages/cli/src/record-cmd.js +82 -11
- package/dist/packages/cli/src/rules-cmd.js +2 -1
- package/dist/packages/cli/src/rules.js +24 -4
- package/dist/packages/cli/src/screens/autotrade-journey.js +134 -0
- package/dist/packages/cli/src/screens/autotrade-keys.js +13 -0
- package/dist/packages/cli/src/screens/autotrade-screen.js +57 -6
- package/dist/packages/cli/src/screens/autotrade-ui.js +17 -7
- package/dist/packages/cli/src/screens/autotrade-worker.js +4 -2
- package/dist/packages/cli/src/screens/narrative.js +154 -2
- package/dist/packages/cli/src/state.js +1 -0
- package/dist/packages/cli/src/telegram-cmd.js +4 -1
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/custom/src/rules.js +5 -1
- package/dist/packages/data/src/trade/ledger/schema.js +1 -1
- package/dist/packages/mcp/assets/kb/app-help/autotrade-executor-kit.md +44 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-journey.md +46 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-limits.md +48 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-real-signal.md +45 -0
- package/dist/packages/mcp/assets/kb/app-help/classification-models.md +41 -0
- package/dist/packages/mcp/assets/kb/app-help/live-trading.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/narratives-tracker.md +45 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-autotrade.md +12 -0
- package/dist/packages/mcp/assets/kb/app-help/strategy-dashboard.md +38 -0
- package/dist/packages/mcp/assets/kb/app-help/streamer-overlay.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/trade-ledger.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-change-a-strategy-by-chat.md +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-on-the-q-screen.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-rug-proof-entries-experimental.md +52 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-running-it-all-day.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-stopping-it.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-always-on-service.md +59 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-strategy-dashboard-dexterm-dash.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-is-paper-self-directed.md +9 -2
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-will-not-do.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-copy-and-paste-without-surprises.md +0 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-dexterm-says-command-not-found.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-reopen-dexterm-later.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-terminal-shell-prompt-command-output-and-path.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-1-ask-for-a-strategy.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-2-backtest-it-held-out-split.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-3-forward-test-it-on-live-data.md +44 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-4-promote-it-to-always-on-paper.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-5-real-only-if-you-choose.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-ladder.md +41 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-report.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-what-the-analyst-knows.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-hosted-jev-with-your-own-key.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-how-they-measured-on-dexterm-s-own-history.md +52 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-set-up-kev-on-your-own-machine.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-settings.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-the-choices.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-where-it-plugs-in.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-which-model-for-which-machine.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-cli-invocations.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -0
- package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +2 -11
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-executor-health.md +38 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-paper-vs-real.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-digest.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-executor-kit.md +48 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-whole-path.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-what-dexterm-does-and-never-does.md +44 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-your-limits.md +77 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real.md +24 -0
- package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-larp-paste-a-coin-on-the-web.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-changed-in-0-1-7.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-real-mode-your-strategy-s-trades-to-your.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-the-short-version.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-using-a-trading-bot-with-dexterm.md +0 -2
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does-not-do.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-how-coins-are-grouped.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-limits.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-is-measured.md +44 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-testing-showed.md +43 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-where-to-find-it.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-before-you-start.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-limits.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-options.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-security-model.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-set-it-up-in-obs-step-by-step.md +49 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-pnl-scene-paper-and-live.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-three-scenes.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-what-is-redacted.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay.md +25 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-receipts-what-your-executor-did.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-safety.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-set-it-up.md +47 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-channels.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-signal.md +48 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-is.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-will-not-do.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-where-you-see-it.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-bubble-map.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-streaming-it.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +2 -2
- package/dist/packages/mcp/assets/kb/entries/autotrade.md +4 -2
- package/dist/packages/mcp/assets/kb/entries/narratives-tracker.md +45 -0
- package/dist/packages/mcp/src/autotrade-tools.js +168 -5
- package/dist/packages/mcp/src/engine/restrictions.js +3 -3
- package/dist/packages/mcp/src/index.js +1 -1
- package/dist/packages/mcp/src/ledger-tools.js +155 -7
- package/dist/packages/mcp/src/server.js +125 -10
- package/dist/packages/mcp/src/spectator-sink.js +27 -0
- package/dist/packages/narratives/src/cluster.js +193 -0
- package/dist/packages/narratives/src/embed.js +184 -0
- package/dist/packages/narratives/src/export.js +60 -0
- package/dist/packages/narratives/src/index.js +10 -0
- package/dist/packages/narratives/src/label.js +48 -0
- package/dist/packages/narratives/src/normalize.js +179 -0
- package/dist/packages/narratives/src/recorder.js +93 -0
- package/dist/packages/narratives/src/tracker.js +328 -0
- package/dist/packages/recorder/src/exit-probes.js +215 -0
- package/dist/packages/recorder/src/index.js +1 -0
- package/dist/packages/recorder/src/recorder.js +11 -1
- package/dist/packages/recorder/src/retention.js +4 -0
- package/dist/packages/recorder/src/schema.js +20 -1
- package/dist/packages/tui/src/keymap.js +12 -0
- package/package.json +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-powershell-experimental.md +0 -39
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// dexterm — STREAMER OVERLAY model (task-1427). A pure fold of spectator events into the three OBS scenes:
|
|
2
|
+
// activity the agent's recent calls (redacted), newest first
|
|
3
|
+
// coin the coin the agent is on: name, LARP evidence + verdict, a top-10 holder bar, live signals with ages
|
|
4
|
+
// pnl the agent's PAPER fills on that coin (drawn by the task-1407 card renderer, see overlay/pnl.ts)
|
|
5
|
+
//
|
|
6
|
+
// Honesty by construction:
|
|
7
|
+
// - every event is passed through redactSpectatorEvent HERE, whatever its source already did, so a full address
|
|
8
|
+
// or key can never reach a scene (addresses become head…tail; key material becomes [key]);
|
|
9
|
+
// - every figure is copied from an event; nothing is estimated. Each scene carries the age of its data;
|
|
10
|
+
// - a replayed recording is SAMPLE DATA: `sample` is set once by the caller and every scene view carries it.
|
|
11
|
+
// No I/O, no clock (callers pass `now`), no control surface: the overlay can only describe what already happened.
|
|
12
|
+
import { redactSpectatorEvent } from '../../../agents/src/index.js';
|
|
13
|
+
/** How many activity rows a scene keeps (OBS lower thirds are short). */
|
|
14
|
+
export const OVERLAY_ACTIVITY_MAX = 8;
|
|
15
|
+
/** Live signals kept on the coin card. */
|
|
16
|
+
export const OVERLAY_FLAGS_MAX = 4;
|
|
17
|
+
export const OVERLAY_SCENES = ['activity', 'coin', 'pnl'];
|
|
18
|
+
export function emptyOverlayState(sample) {
|
|
19
|
+
return { sample, lastEventAt: null, events: 0, activity: [], coins: new Map(), current: null, paper: new Map(), paperCurrent: null };
|
|
20
|
+
}
|
|
21
|
+
const MASKED_RE = /^[1-9A-HJ-NP-Za-km-z]{4}…[1-9A-HJ-NP-Za-km-z]{4}$/;
|
|
22
|
+
// eslint-disable-next-line no-control-regex
|
|
23
|
+
const UNSAFE_RE = /[\u0000-\u001f\u007f-\u009f---]/g;
|
|
24
|
+
const clean = (s, max) => (typeof s === 'string' ? s.replace(UNSAFE_RE, '').slice(0, max) : '');
|
|
25
|
+
const finite = (x) => (typeof x === 'number' && Number.isFinite(x) ? x : null);
|
|
26
|
+
/** Fold one event (any source) into the state. Returns the redacted copy that was folded. */
|
|
27
|
+
export function foldOverlay(state, raw) {
|
|
28
|
+
const ev = redactSpectatorEvent(raw);
|
|
29
|
+
const t = finite(ev.t) ?? 0;
|
|
30
|
+
state.events++;
|
|
31
|
+
state.lastEventAt = state.lastEventAt === null ? t : Math.max(state.lastEventAt, t);
|
|
32
|
+
if (ev.phase !== 'note' || ev.tool !== 'live_signal') {
|
|
33
|
+
state.activity.unshift({
|
|
34
|
+
t, agent: clean(ev.agent, 24) || 'agent', phase: clean(ev.phase, 12), tool: clean(ev.tool ?? '', 28),
|
|
35
|
+
summary: clean(ev.summary ?? '', 120), ok: ev.ok !== false, dataAgeSec: finite(ev.dataAgeSec),
|
|
36
|
+
});
|
|
37
|
+
if (state.activity.length > OVERLAY_ACTIVITY_MAX)
|
|
38
|
+
state.activity.length = OVERLAY_ACTIVITY_MAX;
|
|
39
|
+
}
|
|
40
|
+
const d = ev.detail ?? {};
|
|
41
|
+
const age = finite(ev.dataAgeSec) ?? 0;
|
|
42
|
+
const mint = [d.coin?.mint, d.fill?.mint, d.ticket?.mint, ev.nav?.mint].find((m) => typeof m === 'string' && MASKED_RE.test(m)) ?? null;
|
|
43
|
+
if (mint) {
|
|
44
|
+
const c = coin(state, mint, t);
|
|
45
|
+
if (ev.nav?.mint === mint || d.coin || d.verdict || d.larp || d.holders)
|
|
46
|
+
state.current = mint;
|
|
47
|
+
const sym = clean(d.coin?.symbol ?? d.fill?.symbol ?? d.ticket?.symbol ?? '', 16);
|
|
48
|
+
if (sym && sym !== '?')
|
|
49
|
+
c.symbol = sym;
|
|
50
|
+
const px = finite(d.coin?.priceUsd ?? null) ?? finite(d.fill?.priceUsd ?? null);
|
|
51
|
+
if (px !== null && px > 0)
|
|
52
|
+
c.priceUsd = { value: px, at: t, ageSec: age };
|
|
53
|
+
if (d.verdict)
|
|
54
|
+
c.verdict = { label: clean(d.verdict.label, 18), rug: finite(d.verdict.rug ?? null), at: t, ageSec: age };
|
|
55
|
+
if (d.larp)
|
|
56
|
+
c.larp = { flags: Math.max(0, Math.round(d.larp.flags)), items: d.larp.items.slice(0, 4).map((x) => clean(x, 60)), at: t, ageSec: age };
|
|
57
|
+
if (d.holders && finite(d.holders.top10Pct) !== null) {
|
|
58
|
+
c.holders = { top10Pct: Math.max(0, Math.min(100, d.holders.top10Pct)), count: finite(d.holders.count ?? null), basis: d.holders.basis === 'raw' ? 'raw' : 'ex-pools', at: t, ageSec: finite(d.holders.ageSec ?? null) ?? age };
|
|
59
|
+
}
|
|
60
|
+
for (const f of d.liveFlags ?? []) {
|
|
61
|
+
// dated on the event's clock: at the event the reading was its own age plus the time since the signal fired
|
|
62
|
+
// (so a replayed recording, re-timed to the playback clock, keeps its signals' ages consistent)
|
|
63
|
+
const fat = finite(f.at) ?? t;
|
|
64
|
+
c.flags.unshift({ kind: clean(f.kind, 16), level: f.level, title: clean(f.title, 24), text: clean(f.text, 120), source: clean(f.source, 24), stale: f.stale === true, at: t, ageSec: Math.max(0, finite(f.ageSec) ?? 0) + Math.max(0, t - fat) / 1000 });
|
|
65
|
+
}
|
|
66
|
+
if (c.flags.length > OVERLAY_FLAGS_MAX)
|
|
67
|
+
c.flags.length = OVERLAY_FLAGS_MAX;
|
|
68
|
+
if (d.fill && ev.ok !== false) {
|
|
69
|
+
const f = d.fill;
|
|
70
|
+
const sol = finite(f.sol), qty = finite(f.qty);
|
|
71
|
+
if (sol !== null && qty !== null && qty > 0 && (f.side === 'buy' || f.side === 'sell')) {
|
|
72
|
+
const p = state.paper.get(mint) ?? { mint, symbol: c.symbol ?? '?', fills: [] };
|
|
73
|
+
p.symbol = c.symbol ?? p.symbol;
|
|
74
|
+
p.fills.push({ t, side: f.side, sol, qty, priceUsd: finite(f.priceUsd) });
|
|
75
|
+
if (p.fills.length > 200)
|
|
76
|
+
p.fills.splice(0, p.fills.length - 200);
|
|
77
|
+
state.paper.set(mint, p);
|
|
78
|
+
state.paperCurrent = mint;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return ev;
|
|
83
|
+
}
|
|
84
|
+
function coin(state, mint, t) {
|
|
85
|
+
let c = state.coins.get(mint);
|
|
86
|
+
if (!c) {
|
|
87
|
+
c = { mint, symbol: null, priceUsd: null, verdict: null, larp: null, holders: null, flags: [], lastAt: t };
|
|
88
|
+
state.coins.set(mint, c);
|
|
89
|
+
// bounded: a long session touches many coins; keep the 50 most recent
|
|
90
|
+
if (state.coins.size > 50) {
|
|
91
|
+
const oldest = [...state.coins.values()].filter((x) => x.mint !== state.current).sort((a, b) => a.lastAt - b.lastAt)[0];
|
|
92
|
+
if (oldest)
|
|
93
|
+
state.coins.delete(oldest.mint);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
c.lastAt = Math.max(c.lastAt, t);
|
|
97
|
+
return c;
|
|
98
|
+
}
|
|
99
|
+
// ------------------------------------------------------------------------------------------------ scene views
|
|
100
|
+
/** Seconds → "12s" / "4m" / "2h" / "3d" (the scenes' age chips). */
|
|
101
|
+
export function fmtAgeShort(sec) {
|
|
102
|
+
if (sec === null || !Number.isFinite(sec))
|
|
103
|
+
return '—';
|
|
104
|
+
const s = Math.max(0, Math.round(sec));
|
|
105
|
+
return s < 60 ? `${s}s` : s < 3600 ? `${Math.floor(s / 60)}m` : s < 86_400 ? `${Math.floor(s / 3600)}h` : `${Math.floor(s / 86_400)}d`;
|
|
106
|
+
}
|
|
107
|
+
/** The age of a dated value at `now`: how old it was when it arrived plus the time since. */
|
|
108
|
+
export const ageAt = (d, now) => d.ageSec + Math.max(0, now - d.at) / 1000;
|
|
109
|
+
function header(state, newestAgeSec) {
|
|
110
|
+
return { sample: state.sample, ageSec: newestAgeSec, age: newestAgeSec === null ? 'no data yet' : `data age ${fmtAgeShort(newestAgeSec)}` };
|
|
111
|
+
}
|
|
112
|
+
export function activityView(state, now) {
|
|
113
|
+
const newest = state.activity[0];
|
|
114
|
+
return {
|
|
115
|
+
scene: 'activity',
|
|
116
|
+
...header(state, newest ? Math.max(0, now - newest.t) / 1000 + (newest.dataAgeSec ?? 0) : null),
|
|
117
|
+
rows: state.activity.map((r) => ({
|
|
118
|
+
when: fmtAgeShort(Math.max(0, now - r.t) / 1000), agent: r.agent, phase: r.phase, tool: r.tool, summary: r.summary, ok: r.ok,
|
|
119
|
+
dataAge: r.dataAgeSec === null ? null : fmtAgeShort(r.dataAgeSec + Math.max(0, now - r.t) / 1000),
|
|
120
|
+
})),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const fmtUsdPrice = (v) => (v >= 1 ? `$${v.toFixed(4)}` : v >= 0.0001 ? `$${v.toPrecision(4)}` : `$${v.toExponential(2)}`);
|
|
124
|
+
export function coinView(state, now) {
|
|
125
|
+
const c = state.current ? state.coins.get(state.current) ?? null : null;
|
|
126
|
+
if (!c)
|
|
127
|
+
return { scene: 'coin', ...header(state, null), coin: null };
|
|
128
|
+
const ages = [c.priceUsd, c.verdict, c.larp, c.holders, ...c.flags].filter((x) => !!x).map((x) => ageAt(x, now));
|
|
129
|
+
return {
|
|
130
|
+
scene: 'coin',
|
|
131
|
+
...header(state, ages.length ? Math.min(...ages) : null),
|
|
132
|
+
coin: {
|
|
133
|
+
mint: c.mint,
|
|
134
|
+
symbol: c.symbol ?? '?',
|
|
135
|
+
price: c.priceUsd ? { text: fmtUsdPrice(c.priceUsd.value), age: fmtAgeShort(ageAt(c.priceUsd, now)) } : null,
|
|
136
|
+
verdict: c.verdict ? { label: c.verdict.label, rug: c.verdict.rug === null ? null : `${Math.round(c.verdict.rug * 100)}%`, age: fmtAgeShort(ageAt(c.verdict, now)) } : null,
|
|
137
|
+
larp: c.larp ? { flags: c.larp.flags, items: c.larp.items, age: fmtAgeShort(ageAt(c.larp, now)) } : null,
|
|
138
|
+
holders: c.holders ? {
|
|
139
|
+
top10Pct: +c.holders.top10Pct.toFixed(1),
|
|
140
|
+
text: `top 10 hold ${c.holders.top10Pct.toFixed(1)}%${c.holders.count !== null ? ` · ${c.holders.count.toLocaleString('en-US')} holders` : ''}`,
|
|
141
|
+
basis: c.holders.basis === 'ex-pools' ? 'pools, curves, locks and burns left out' : 'every account counted (raw)',
|
|
142
|
+
age: fmtAgeShort(ageAt(c.holders, now)),
|
|
143
|
+
} : null,
|
|
144
|
+
flags: c.flags.map((f) => ({ level: f.level, title: f.title, text: f.text, source: f.source, age: fmtAgeShort(ageAt(f, now)), stale: f.stale })),
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// dexterm — STREAMER OVERLAY page (task-1427): one self-contained HTML document per scene, built for an OBS
|
|
2
|
+
// browser source. The body is transparent (OBS composites it over the stream); the panels carry the cyberspace
|
|
3
|
+
// look in three presets. Everything is inlined under the server's per-page nonce; the script builds the DOM with
|
|
4
|
+
// textContent only (never innerHTML), because every string it shows came from an agent or a token and is untrusted.
|
|
5
|
+
// The page has no controls and sends nothing back: the socket is read-only (see server.ts).
|
|
6
|
+
/** Three presets of the cyberspace look (the app's `cyberspace` theme tokens and two neon variants). */
|
|
7
|
+
export const OVERLAY_THEMES = {
|
|
8
|
+
cyberspace: { panel: 'rgba(3,9,12,0.86)', edge: '#0c3a24', glow: '#39ff14', text: '#eafff0', muted: '#62c98a', dim: '#3aa866', accent: '#00f0ff', up: '#5dff6e', down: '#ff3b6b', warn: '#f5ff2d' },
|
|
9
|
+
ice: { panel: 'rgba(2,8,16,0.86)', edge: '#0b3348', glow: '#00f0ff', text: '#e8fbff', muted: '#6cc7dd', dim: '#3f98b0', accent: '#39ff14', up: '#5dff6e', down: '#ff3b6b', warn: '#f5ff2d' },
|
|
10
|
+
neon: { panel: 'rgba(10,3,16,0.86)', edge: '#3b0f4a', glow: '#ff2bd6', text: '#fff0fc', muted: '#d07ac0', dim: '#a04f93', accent: '#00f0ff', up: '#5dff6e', down: '#ff3b6b', warn: '#f5ff2d' },
|
|
11
|
+
};
|
|
12
|
+
export const OVERLAY_THEME_NAMES = Object.keys(OVERLAY_THEMES);
|
|
13
|
+
const CSS = `
|
|
14
|
+
:root{--panel:__PANEL__;--edge:__EDGE__;--glow:__GLOW__;--text:__TEXT__;--muted:__MUTED__;--dim:__DIM__;--accent:__ACCENT__;--up:__UP__;--down:__DOWN__;--warn:__WARN__}
|
|
15
|
+
html,body{margin:0;padding:0;background:transparent;color:var(--text);font:600 18px/1.35 ui-monospace,SFMono-Regular,Menlo,Consolas,"DejaVu Sans Mono",monospace;overflow:hidden}
|
|
16
|
+
body{padding:12px}
|
|
17
|
+
.grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:12px}
|
|
18
|
+
.grid .pnl{grid-column:1 / span 2}
|
|
19
|
+
.scene{background:var(--panel);border:1px solid var(--edge);box-shadow:0 0 0 1px rgba(0,0,0,.4),0 0 18px -6px var(--glow);border-radius:6px;padding:10px 14px;max-width:1200px}
|
|
20
|
+
.head{display:flex;gap:10px;align-items:center;border-bottom:1px solid var(--edge);padding-bottom:6px;margin-bottom:8px;white-space:nowrap}
|
|
21
|
+
.brand{color:var(--glow)}.title{color:var(--muted);letter-spacing:.08em}
|
|
22
|
+
.sp{flex:1}
|
|
23
|
+
.chip{border:1px solid var(--edge);border-radius:3px;padding:0 6px;color:var(--dim);font-size:14px}
|
|
24
|
+
.chip.sample{color:#000;background:var(--warn);border-color:var(--warn)}
|
|
25
|
+
.chip.old{color:var(--warn);border-color:var(--warn)}
|
|
26
|
+
.chip.paper{color:#000;background:var(--warn);border-color:var(--warn)}
|
|
27
|
+
.chip.live{color:#000;background:var(--up);border-color:var(--up)}
|
|
28
|
+
.chip.off{color:var(--down);border-color:var(--down)}
|
|
29
|
+
.row{display:grid;grid-template-columns:3.2em 9em 11em minmax(0,1fr) 4.5em;gap:8px;white-space:nowrap;overflow:hidden}
|
|
30
|
+
.row>*{overflow:hidden;text-overflow:ellipsis}
|
|
31
|
+
.when,.age{color:var(--dim)}.agent{color:var(--accent)}.tool{color:var(--muted)}.bad{color:var(--down)}
|
|
32
|
+
.sym{font-size:34px;color:var(--text)}.mint{color:var(--dim);font-size:14px}
|
|
33
|
+
.kv{display:flex;gap:10px;margin:4px 0;white-space:nowrap}.k{color:var(--dim);min-width:7.5em}.v{overflow:hidden;text-overflow:ellipsis}
|
|
34
|
+
.bar{height:14px;border:1px solid var(--edge);border-radius:2px;background:rgba(0,0,0,.35);margin:3px 0 2px}
|
|
35
|
+
.fill{height:100%;background:linear-gradient(90deg,var(--up),var(--warn) 55%,var(--down))}
|
|
36
|
+
.small{font-size:14px;color:var(--dim)}
|
|
37
|
+
.flag{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
38
|
+
.flag.warn,.flag.down{color:var(--down)}.flag.up{color:var(--up)}.flag.info{color:var(--muted)}
|
|
39
|
+
.card{position:relative}.card img{display:block;width:100%;height:auto;image-rendering:pixelated;border-radius:3px}
|
|
40
|
+
.banner{position:absolute;left:0;right:0;top:0;text-align:center;font-size:22px;line-height:30px;color:#000;background:var(--warn);letter-spacing:.12em;border-radius:3px 3px 0 0}
|
|
41
|
+
.empty{color:var(--dim)}
|
|
42
|
+
.foot{margin-top:6px;color:var(--dim);font-size:13px}
|
|
43
|
+
`;
|
|
44
|
+
const JS = `
|
|
45
|
+
(function(){
|
|
46
|
+
'use strict';
|
|
47
|
+
var PAGE = document.querySelector('meta[name="dexterm-page"]').content;
|
|
48
|
+
var WS_URL = document.querySelector('meta[name="dexterm-ws"]').content;
|
|
49
|
+
var SCENES = PAGE === 'all' ? ['coin','activity','pnl'] : [PAGE];
|
|
50
|
+
var TITLES = { activity: 'AGENT ACTIVITY', coin: 'COIN', pnl: 'PnL' };
|
|
51
|
+
var root = document.getElementById('root');
|
|
52
|
+
if (PAGE === 'all') root.className = 'grid';
|
|
53
|
+
var boxes = {};
|
|
54
|
+
function el(tag, cls, text){ var e = document.createElement(tag); if (cls) e.className = cls; if (text !== undefined && text !== null) e.textContent = String(text); return e; }
|
|
55
|
+
SCENES.forEach(function(s){
|
|
56
|
+
var box = el('section', 'scene ' + s);
|
|
57
|
+
var head = el('div', 'head');
|
|
58
|
+
head.appendChild(el('span', 'brand', '\\u25c6 dexterm'));
|
|
59
|
+
head.appendChild(el('span', 'title', TITLES[s]));
|
|
60
|
+
head.appendChild(el('span', 'sp'));
|
|
61
|
+
var stamp = el('span', 'chip'); stamp.hidden = true; head.appendChild(stamp);
|
|
62
|
+
var sample = el('span', 'chip sample', 'SAMPLE DATA \\u00b7 REPLAY'); sample.hidden = true; head.appendChild(sample);
|
|
63
|
+
var age = el('span', 'chip age', 'waiting for data'); head.appendChild(age);
|
|
64
|
+
var conn = el('span', 'chip off', 'disconnected'); conn.hidden = true; head.appendChild(conn);
|
|
65
|
+
var body = el('div', 'body'); body.appendChild(el('div', 'empty', 'waiting for the agent\\u2026'));
|
|
66
|
+
var foot = el('div', 'foot', 'read-only \\u00b7 addresses redacted \\u00b7 not financial advice \\u00b7 dexterm.xyz');
|
|
67
|
+
box.appendChild(head); box.appendChild(body); box.appendChild(foot);
|
|
68
|
+
root.appendChild(box);
|
|
69
|
+
boxes[s] = { body: body, age: age, sample: sample, stamp: stamp, conn: conn };
|
|
70
|
+
});
|
|
71
|
+
function header(b, v){
|
|
72
|
+
b.sample.hidden = !v.sample;
|
|
73
|
+
b.age.textContent = v.age;
|
|
74
|
+
b.age.className = 'chip age' + (v.ageSec !== null && v.ageSec > 120 ? ' old' : '');
|
|
75
|
+
}
|
|
76
|
+
function clear(n){ while (n.firstChild) n.removeChild(n.firstChild); }
|
|
77
|
+
function kv(k, v, extra){ var r = el('div', 'kv'); r.appendChild(el('span', 'k', k)); r.appendChild(el('span', 'v', v)); if (extra) r.appendChild(el('span', 'age', extra)); return r; }
|
|
78
|
+
var render = {
|
|
79
|
+
activity: function(b, v){
|
|
80
|
+
clear(b.body);
|
|
81
|
+
if (!v.rows.length) { b.body.appendChild(el('div', 'empty', 'no agent calls yet')); return; }
|
|
82
|
+
v.rows.forEach(function(r){
|
|
83
|
+
var row = el('div', 'row' + (r.ok ? '' : ' bad'));
|
|
84
|
+
row.appendChild(el('span', 'when', r.when));
|
|
85
|
+
row.appendChild(el('span', 'agent', r.agent));
|
|
86
|
+
row.appendChild(el('span', 'tool', r.tool || r.phase));
|
|
87
|
+
row.appendChild(el('span', 'sum', r.summary));
|
|
88
|
+
row.appendChild(el('span', 'age', r.dataAge ? r.dataAge + ' old' : ''));
|
|
89
|
+
b.body.appendChild(row);
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
coin: function(b, v){
|
|
93
|
+
clear(b.body);
|
|
94
|
+
var c = v.coin;
|
|
95
|
+
if (!c) { b.body.appendChild(el('div', 'empty', 'the agent has not opened a coin yet')); return; }
|
|
96
|
+
var top = el('div', 'kv'); top.appendChild(el('span', 'sym', '$' + c.symbol)); top.appendChild(el('span', 'mint', c.mint)); b.body.appendChild(top);
|
|
97
|
+
if (c.price) b.body.appendChild(kv('PRICE', c.price.text, c.price.age + ' old'));
|
|
98
|
+
b.body.appendChild(c.verdict ? kv('VERDICT', c.verdict.label + (c.verdict.rug ? ' \\u00b7 rug heuristic ' + c.verdict.rug : ''), c.verdict.age + ' old') : kv('VERDICT', 'not run yet'));
|
|
99
|
+
if (c.larp) {
|
|
100
|
+
b.body.appendChild(kv('LARP', c.larp.flags + ' evidence flag' + (c.larp.flags === 1 ? '' : 's') + ' (evidence, not a verdict)', c.larp.age + ' old'));
|
|
101
|
+
c.larp.items.slice(0, 3).forEach(function(i){ b.body.appendChild(el('div', 'small', ' \\u00b7 ' + i)); });
|
|
102
|
+
} else b.body.appendChild(kv('LARP', 'not checked yet'));
|
|
103
|
+
if (c.holders) {
|
|
104
|
+
b.body.appendChild(kv('HOLDERS', c.holders.text, c.holders.age + ' old'));
|
|
105
|
+
var bar = el('div', 'bar'); var f = el('div', 'fill'); f.style.width = Math.max(0, Math.min(100, c.holders.top10Pct)) + '%'; bar.appendChild(f); b.body.appendChild(bar);
|
|
106
|
+
b.body.appendChild(el('div', 'small', c.holders.basis));
|
|
107
|
+
} else b.body.appendChild(kv('HOLDERS', 'not read yet'));
|
|
108
|
+
if (c.flags.length) {
|
|
109
|
+
b.body.appendChild(el('div', 'k', 'LIVE SIGNALS'));
|
|
110
|
+
c.flags.forEach(function(s){ b.body.appendChild(el('div', 'flag ' + s.level, '\\u25cf ' + s.title + ' \\u00b7 ' + s.text + ' \\u00b7 ' + s.source + ' \\u00b7 ' + s.age + ' old' + (s.stale ? ' (stale)' : ''))); });
|
|
111
|
+
} else b.body.appendChild(kv('SIGNALS', 'none fired yet'));
|
|
112
|
+
},
|
|
113
|
+
pnl: function(b, v){
|
|
114
|
+
clear(b.body);
|
|
115
|
+
b.stamp.hidden = !v.stamp;
|
|
116
|
+
if (v.stamp) { b.stamp.textContent = v.strip || (v.stamp === 'LIVE' ? 'LIVE' : 'PAPER \\u00b7 SIMULATED'); b.stamp.className = 'chip ' + (v.stamp === 'LIVE' ? 'live' : 'paper'); }
|
|
117
|
+
if (!v.png) { b.body.appendChild(el('div', 'empty', v.note)); return; }
|
|
118
|
+
var card = el('div', 'card');
|
|
119
|
+
var img = new Image(v.w, v.h); img.alt = (v.stamp || '') + ' PnL card'; img.src = 'data:image/png;base64,' + v.png;
|
|
120
|
+
card.appendChild(img);
|
|
121
|
+
if (v.sample) card.appendChild(el('div', 'banner', 'SAMPLE DATA \\u00b7 REPLAY OF A SAVED SESSION'));
|
|
122
|
+
b.body.appendChild(card);
|
|
123
|
+
b.body.appendChild(el('div', 'small', v.note));
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
var ws = new WebSocket(WS_URL);
|
|
127
|
+
ws.onmessage = function(m){
|
|
128
|
+
var msg; try { msg = JSON.parse(m.data); } catch (e) { return; }
|
|
129
|
+
if (!msg || msg.type !== 'view' || !boxes[msg.scene] || !render[msg.scene]) return;
|
|
130
|
+
var b = boxes[msg.scene];
|
|
131
|
+
header(b, msg.view);
|
|
132
|
+
render[msg.scene](b, msg.view);
|
|
133
|
+
};
|
|
134
|
+
ws.onclose = function(){ SCENES.forEach(function(s){ boxes[s].conn.hidden = false; boxes[s].conn.textContent = 'disconnected \\u00b7 run dexterm overlay for a fresh URL'; }); };
|
|
135
|
+
})();
|
|
136
|
+
`;
|
|
137
|
+
const htmlEscape = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
138
|
+
/** The full document for one scene page (or 'all'). Values substituted here are ours (theme tokens, ws url). */
|
|
139
|
+
export function renderOverlayPage(o) {
|
|
140
|
+
const t = OVERLAY_THEMES[o.theme];
|
|
141
|
+
const css = CSS.replace(/__([A-Z]+)__/g, (_m, k) => t[k.toLowerCase()] ?? '');
|
|
142
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8">`
|
|
143
|
+
+ `<meta name="dexterm-page" content="${htmlEscape(o.page)}"><meta name="dexterm-ws" content="${htmlEscape(o.wsUrl)}">`
|
|
144
|
+
+ `<title>dexterm overlay · ${htmlEscape(o.page)}</title><style nonce="${o.nonce}">${css}</style></head>`
|
|
145
|
+
+ `<body><main id="root"></main><script nonce="${o.nonce}">${JS}</script></body></html>`;
|
|
146
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// dexterm — STREAMER OVERLAY PnL scene (task-1427). Draws the coin's result with the task-1407 card renderer, so
|
|
2
|
+
// the stamp (PAPER / LIVE), the provenance block, the data ages and the "not financial advice" footer are the
|
|
3
|
+
// renderer's own and cannot be turned off here.
|
|
4
|
+
//
|
|
5
|
+
// Two sources, never mixed:
|
|
6
|
+
// - the spectator stream (default): the agent's PAPER fills on the coin, exactly as the paper desk booked them.
|
|
7
|
+
// The stream only ever carries paper fills (an agent can never trade for real), so this card is always PAPER.
|
|
8
|
+
// - the trade ledger (`--ledger <tradeId>`): one round trip of the user's own unified ledger, read from the local
|
|
9
|
+
// file (no network). Its wallet mode picks the stamp: paper → PAPER, devnet / live → LIVE. A real-mode row is
|
|
10
|
+
// refused by the adapter (dexterm only modelled that fill).
|
|
11
|
+
// Addresses are masked before they reach the card (the stream copy is already redacted; a ledger mint is masked
|
|
12
|
+
// here), the size is a band, and the wallet is never drawn. A ledger row's tx signatures stay in the card input so
|
|
13
|
+
// the LIVE note counts them truthfully; the renderer prints a signature only with showWallet, which the overlay
|
|
14
|
+
// never sets, and only the PNG leaves this module.
|
|
15
|
+
import { computeCard, cardPng, stampOf, CardError, CARD_GEOMETRY, ledgerCardInput } from "../pnl-card.js";
|
|
16
|
+
import { maskAddress } from '../../../agents/src/index.js';
|
|
17
|
+
import { THEMES } from '../../../tui/src/index.js';
|
|
18
|
+
import { fmtAgeShort } from "./model.js";
|
|
19
|
+
/** Paper fills carry whole-token amounts; the card's integer token maths uses this many decimals for them. */
|
|
20
|
+
const STREAM_DECIMALS = 6;
|
|
21
|
+
/** Card input from the stream's PAPER fills on one coin. The mark (open position) is the newest stream price. */
|
|
22
|
+
export function streamCardInput(state, mint) {
|
|
23
|
+
const p = state.paper.get(mint);
|
|
24
|
+
if (!p || !p.fills.length)
|
|
25
|
+
return null;
|
|
26
|
+
const scale = 10 ** STREAM_DECIMALS;
|
|
27
|
+
const fills = p.fills.map((f) => ({
|
|
28
|
+
t: f.t, side: f.side, solDelta: (f.side === 'buy' ? -1 : 1) * Math.abs(f.sol),
|
|
29
|
+
tokenDelta: BigInt(Math.round(f.qty * scale)) * (f.side === 'buy' ? 1n : -1n), feeSol: null, sig: null,
|
|
30
|
+
}));
|
|
31
|
+
// Mark: the newest price the stream carried, in SOL via the SOL/USD rate the newest priced fill implies
|
|
32
|
+
// (its USD price ÷ its SOL price). Without a later price, the last fill's own SOL price marks the position.
|
|
33
|
+
const last = p.fills[p.fills.length - 1];
|
|
34
|
+
const lastSolPx = Math.abs(last.sol) / last.qty;
|
|
35
|
+
const priced = [...p.fills].reverse().find((f) => f.priceUsd !== null && f.priceUsd > 0);
|
|
36
|
+
const c = state.coins.get(mint);
|
|
37
|
+
let mark = { priceSol: lastSolPx, at: last.t, via: 'last paper fill' };
|
|
38
|
+
if (priced && c?.priceUsd && c.priceUsd.at > last.t) {
|
|
39
|
+
const solUsd = priced.priceUsd / (Math.abs(priced.sol) / priced.qty);
|
|
40
|
+
if (solUsd > 0 && Number.isFinite(solUsd))
|
|
41
|
+
mark = { priceSol: c.priceUsd.value / solUsd, at: c.priceUsd.at - c.priceUsd.ageSec * 1000, via: 'stream price, SOL/USD implied by the paper fill' };
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
source: 'paper', mint, symbol: p.symbol, name: null, decimals: STREAM_DECIMALS, fills,
|
|
45
|
+
ledger: 'agent paper desk via the spectator stream', wallet: null, mark,
|
|
46
|
+
excluded: [], fees: 'as booked by the paper desk (simulated)', image: null, supply: null,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Card input from one ledger row, the mint masked first (the overlay never shows a full address). */
|
|
50
|
+
export function ledgerOverlayInput(row, solUsd) {
|
|
51
|
+
const input = ledgerCardInput(row, { solUsd });
|
|
52
|
+
return { ...input, mint: maskAddress(row.mint) };
|
|
53
|
+
}
|
|
54
|
+
function header(state, s, now) {
|
|
55
|
+
const ageSec = s ? Math.max(0, now - (s.open && s.input.mark ? s.input.mark.at : s.lastT)) / 1000 : null;
|
|
56
|
+
return { sample: state.sample, ageSec, age: ageSec === null ? 'no data yet' : `data age ${fmtAgeShort(ageSec)}` };
|
|
57
|
+
}
|
|
58
|
+
/** Render the PnL scene. `input` null → the stream's current coin. Never throws: a refusal becomes the note. */
|
|
59
|
+
export function pnlView(state, now, input) {
|
|
60
|
+
const g = CARD_GEOMETRY.wide;
|
|
61
|
+
const inp = input ?? (state.paperCurrent ? streamCardInput(state, state.paperCurrent) : null);
|
|
62
|
+
if (!inp)
|
|
63
|
+
return { scene: 'pnl', ...header(state, null, now), stamp: null, strip: null, png: null, w: g.w, h: g.h, note: 'no PAPER trade by the agent yet' };
|
|
64
|
+
let s;
|
|
65
|
+
try {
|
|
66
|
+
s = computeCard(inp, now);
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
if (e instanceof CardError)
|
|
70
|
+
return { scene: 'pnl', ...header(state, null, now), stamp: null, strip: null, png: null, w: g.w, h: g.h, note: e.message };
|
|
71
|
+
throw e;
|
|
72
|
+
}
|
|
73
|
+
const png = cardPng(s, { aspect: 'wide' }).toString('base64');
|
|
74
|
+
return { scene: 'pnl', ...header(state, s, now), stamp: inp.source === 'paper' ? 'PAPER' : 'LIVE', strip: stampOf(s, THEMES.cyberspace).strip, png, w: g.w, h: g.h, note: inp.ledger };
|
|
75
|
+
}
|
|
@@ -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
|
+
}
|