dexterm-cli 0.1.5 → 0.1.6
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/index.js +2 -0
- package/dist/packages/agents/src/spectator.js +54 -0
- package/dist/packages/agents/src/story.js +523 -0
- package/dist/packages/agents/src/tiers.js +1 -0
- package/dist/packages/agents/src/tools.js +1 -0
- package/dist/packages/cli/assets/claude/plugin/skills/REGISTRY.json +1 -1
- package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +15 -8
- package/dist/packages/cli/src/agent-cmd.js +6 -4
- package/dist/packages/cli/src/agent-spectator.js +17 -1
- package/dist/packages/cli/src/analyst/backend.js +23 -0
- package/dist/packages/cli/src/analyst/larp-gauge.js +60 -0
- package/dist/packages/cli/src/analyst/session.js +12 -1
- package/dist/packages/cli/src/app-help-gen.js +7 -7
- package/dist/packages/cli/src/backtest-cmd.js +13 -2
- package/dist/packages/cli/src/display.js +55 -9
- package/dist/packages/cli/src/live/dev-history.js +31 -15
- package/dist/packages/cli/src/live/index.js +5 -1
- package/dist/packages/cli/src/live/screens/newpairs-live.js +1 -1
- package/dist/packages/cli/src/live/screens/token-live.js +41 -11
- package/dist/packages/cli/src/live/screens/watchlist-live.js +1 -1
- package/dist/packages/cli/src/live/trade.js +22 -0
- package/dist/packages/cli/src/main.js +146 -12
- package/dist/packages/cli/src/mcp-follow.js +103 -6
- package/dist/packages/cli/src/plan-cmd.js +15 -5
- package/dist/packages/cli/src/popup.js +17 -1
- package/dist/packages/cli/src/scan/ui.js +7 -1
- package/dist/packages/cli/src/screens/agent-hud.js +741 -0
- package/dist/packages/cli/src/screens/analyst-session.js +4 -0
- package/dist/packages/cli/src/screens/brain.js +608 -0
- package/dist/packages/cli/src/screens/newpairs.js +1 -1
- package/dist/packages/cli/src/screens/pnl-chip.js +143 -0
- package/dist/packages/cli/src/screens/screener.js +1 -0
- package/dist/packages/cli/src/screens/token.js +16 -7
- package/dist/packages/cli/src/screens/trending.js +1 -1
- package/dist/packages/cli/src/state.js +69 -5
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/custom/src/schema.js +8 -2
- package/dist/packages/data/src/index.js +2 -0
- package/dist/packages/data/src/larp/score.js +220 -0
- package/dist/packages/data/src/larp/x-account.js +316 -0
- package/dist/packages/data/src/trade/agent/paper-desk.js +77 -12
- package/dist/packages/data/src/trade/plans/engine.js +7 -0
- package/dist/packages/data/src/trade/plans/mandate.js +28 -15
- package/dist/packages/data/src/trade/plans/service.js +1 -1
- package/dist/packages/mcp/assets/kb/app-help/dev-history.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/larp-check.md +5 -1
- package/dist/packages/mcp/assets/kb/app-help/scan-mode.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/screen-token.md +3 -0
- package/dist/packages/mcp/assets/kb/app-help/spectate-an-agent.md +2 -1
- package/dist/packages/mcp/assets/kb/docs/docs-agents-agent-paper-trading.md +3 -2
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-starting-a-run.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-y-axis.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-price-display-mode.md +3 -3
- 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 +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-running-the-tests.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +60 -0
- package/dist/packages/mcp/assets/kb/docs/docs-larp-the-verdict.md +0 -2
- package/dist/packages/mcp/assets/kb/docs/docs-larp-what-it-looks-at.md +4 -1
- package/dist/packages/mcp/assets/kb/docs/docs-larp.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-order-plans-and-standing-mandates.md +1 -9
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-paper-only-mandates.md +41 -0
- package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-market-cap-axis.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-what-it-shows.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-scan-the-read.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-dev-history.md +10 -7
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-follow-over-mcp-in-the-running-terminal.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-limits.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-agent-hud-spectator-v2.md +58 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-brain-view-and-the-pnl-chip.md +46 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-three-surfaces.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-spectator.md +1 -1
- package/dist/packages/mcp/src/paper-agent.js +3 -2
- package/dist/packages/mcp/src/server.js +162 -21
- package/dist/packages/mcp/src/spectator-sink.js +85 -0
- package/dist/packages/packs/src/index.js +1 -0
- package/dist/packages/packs/src/larp-score-backtest.js +119 -0
- package/dist/packages/popup/src/pages/chart-lib.js +36 -15
- package/dist/packages/popup/src/pages/chart.css +9 -0
- package/dist/packages/popup/src/pages/chart.html +4 -1
- package/dist/packages/popup/src/pages/chart.js +34 -5
- package/dist/packages/popup/src/pages/spectator.css +45 -0
- package/dist/packages/popup/src/pages/spectator.html +1 -0
- package/dist/packages/popup/src/pages/spectator.js +69 -0
- package/dist/packages/popup/src/protocol.js +2 -0
- package/dist/packages/popup/src/server.js +5 -1
- package/dist/packages/recorder/src/dev-history.js +7 -3
- package/dist/packages/tui/src/canvas.js +7 -0
- package/dist/packages/tui/src/fmt.js +53 -0
- package/dist/packages/tui/src/keymap.js +4 -0
- package/dist/packages/tui/src/widgets.js +9 -4
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// screen and the token ANALYST panel. Cyberspace styling: the verdict and the tool-call lines are
|
|
3
3
|
// coral, the answer decrypts in through code glyphs, and a cost/latency footer sits on the caption.
|
|
4
4
|
import { Canvas, badge, spinnerGlyph, decryptText, wrap, mix, fmtUsd, fmtPrice, fmtPctDir, } from '../../../tui/src/index.js';
|
|
5
|
+
import { drawLarpGauge } from "../analyst/larp-gauge.js";
|
|
5
6
|
const verdictColor = (c, v) => {
|
|
6
7
|
const t = c.theme;
|
|
7
8
|
if (v === 'WATCH')
|
|
@@ -126,6 +127,9 @@ export function renderAnalystSession(c, x, y, w, h, s, opts) {
|
|
|
126
127
|
c.textR(box.x + 1, cy, inner - 1, right, { fg: t.muted, bg: mix(bg, vc, 0.18) });
|
|
127
128
|
cy += 1;
|
|
128
129
|
}
|
|
130
|
+
// task-1471: the LARP score gauge (from the MCP larp_check result, not the model), under the verdict.
|
|
131
|
+
if (s.larpGauge && cy < inputY - 1)
|
|
132
|
+
cy = drawLarpGauge(c, box.x + 2, cy, inner - 2, inputY - 1, s.larpGauge, bg);
|
|
129
133
|
// task-1313 trading view: the directional bias + setup quality, drawn under the risk verdict.
|
|
130
134
|
if (s.bias && cy < inputY - 1) {
|
|
131
135
|
const bc2 = s.bias === 'long' ? t.up : s.bias === 'short' || s.bias === 'avoid' ? t.down : t.warn;
|
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
// dexterm — the ANALYST panel's BRAIN VIEW (task-1473). While the app runs in an auto mode, i.e. it follows an agent
|
|
2
|
+
// driving dexterm (`--spectate-mcp`, the task-1466 HUD) or AI scan mode is on, the analyst panel on the token screen
|
|
3
|
+
// shows what the auto mode is thinking instead of the "press a for a dossier" help. Top to bottom:
|
|
4
|
+
// 1. REASONING: the agent's thoughts (thoughts feed) typing in with a clock and the phase tag, newest at the bottom,
|
|
5
|
+
// older lines dimmed. Without a thoughts feed, dexterm's own narration of each call, labelled as such. In scan
|
|
6
|
+
// mode: the scan's own log plus the scan analyst's answer.
|
|
7
|
+
// 2. VETTING $SYM: the checklist for the coin in focus, each box with the number the app computed and pass / warn /
|
|
8
|
+
// fail / pending (pending = the number is not in yet). A ◆ marks the checks the agent itself ran.
|
|
9
|
+
// 3. DATA: mcap, price, liquidity, 5m / 1h, buy/sell flow, holders, and a candle trace.
|
|
10
|
+
// 4. PLAN · POSITION (only while a PAPER ticket / position exists): entry, size, TP / stop, mark PnL, peak, time held,
|
|
11
|
+
// what it is waiting for.
|
|
12
|
+
// 5. DECISIONS: the session log (passed X because…, rejected Y, entered Z) and the scoreboard (PAPER, simulated).
|
|
13
|
+
// Every number comes from data the app already holds (the story folded from spectator records, the token view on
|
|
14
|
+
// screen, the scan engine). Nothing here fetches, and nothing here can arm, sign, size, confirm or raise a cap: the
|
|
15
|
+
// panel has no input. Agent text arrives scrubbed and clipped (story.ribbonText) with addresses shortened.
|
|
16
|
+
// Smaller panels drop blocks in priority order (decisions, then data, then the vetting list folds to two columns);
|
|
17
|
+
// calm mode shows the same information with no typing; NO_COLOR keeps meaning in glyphs (✓ ! ✗ · ◆ ❯ ›).
|
|
18
|
+
import { mix, clock, decryptText, fmtUsd, fmtPrice, fmtNum, wrap } from '../../../tui/src/index.js';
|
|
19
|
+
import { ribbonText, ribbonLines, fmtPx } from '../../../agents/src/index.js';
|
|
20
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
21
|
+
// formatting (deterministic, no locale)
|
|
22
|
+
const sgnPct = (p, d = 1) => `${p >= 0 ? '+' : '−'}${Math.abs(p).toFixed(d)}%`;
|
|
23
|
+
const sgnRatio = (r, d = 1) => sgnPct(r * 100, d);
|
|
24
|
+
const sgnSol = (s) => `${s >= 0 ? '+' : '−'}${Math.abs(s).toFixed(4)} SOL`;
|
|
25
|
+
const sgnUsd = (n) => `${n >= 0 ? '+' : '−'}$${Math.abs(n).toFixed(2)}`;
|
|
26
|
+
export function fmtDur(ms) {
|
|
27
|
+
const s = Math.max(0, Math.round(ms / 1000));
|
|
28
|
+
if (s < 60)
|
|
29
|
+
return `${s}s`;
|
|
30
|
+
const m = Math.floor(s / 60);
|
|
31
|
+
return m < 60 ? `${m}m${String(s % 60).padStart(2, '0')}s` : `${Math.floor(m / 60)}h${String(m % 60).padStart(2, '0')}m`;
|
|
32
|
+
}
|
|
33
|
+
const clip = (s, n) => (s.length > n ? s.slice(0, Math.max(1, n - 1)) + '…' : s);
|
|
34
|
+
export const PHASE_TAG = {
|
|
35
|
+
idle: 'IDLE', scanning: 'SCANNING', vetting: 'VETTING', sizing: 'SIZING', executing: 'EXECUTING', holding: 'HOLDING', exiting: 'EXITING',
|
|
36
|
+
};
|
|
37
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
38
|
+
// auto-mode detection
|
|
39
|
+
/** How long after the agent's last call the brain view stays up (a holding agent polls far more often). */
|
|
40
|
+
export const AGENT_QUIET_MS = 180_000;
|
|
41
|
+
/**
|
|
42
|
+
* Whether the agent-follow brain view applies: following, not killed, and the agent has acted recently (or holds a
|
|
43
|
+
* position). Before the first call, after a kill, or once the agent has gone quiet, the normal analyst panel returns.
|
|
44
|
+
*/
|
|
45
|
+
export function agentBrainActive(v, now) {
|
|
46
|
+
if (!v || v.control === 'killed')
|
|
47
|
+
return false;
|
|
48
|
+
if (v.arc && (v.arc.stage === 'open' || v.arc.stage === 'ticket' || v.arc.stage === 'sized'))
|
|
49
|
+
return true;
|
|
50
|
+
return v.lastCallAt !== null && now - v.lastCallAt < AGENT_QUIET_MS;
|
|
51
|
+
}
|
|
52
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
53
|
+
// facts adapters (the numbers the token screen already holds)
|
|
54
|
+
const distinct = (xs) => new Set(xs).size;
|
|
55
|
+
export function factsFromFixture(tk, larpVerdict = null) {
|
|
56
|
+
const buys = tk.trades.filter((x) => x.side === 'buy').length;
|
|
57
|
+
return {
|
|
58
|
+
mint: tk.mint, symbol: tk.symbol, price: tk.price, mcap: tk.mcap, liq: tk.liq, chg5m: tk.chg.m5, chg1h: tk.chg.h1,
|
|
59
|
+
buys: tk.trades.length ? buys : null, sells: tk.trades.length ? tk.trades.length - buys : null,
|
|
60
|
+
wallets: tk.trades.length ? distinct(tk.trades.map((x) => x.addr)) : null, holders: tk.holders, top10: tk.top10,
|
|
61
|
+
mintAuth: tk.mintAuth, freezeAuth: tk.freezeAuth, lpLocked: tk.lpLocked,
|
|
62
|
+
dev: [`${tk.devHistory.launches} launches · ${tk.devHistory.rugged} rugs`, tk.devHistory.rugged ? 'warn' : 'ok'],
|
|
63
|
+
rugScore: tk.rug.score, larpVerdict, closes: tk.candles.map((k) => k.c),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** From the live token view. `extra` carries what the screen derived this frame (top-10 ex-pool, the dev row). */
|
|
67
|
+
export function factsFromView(v, extra = {}) {
|
|
68
|
+
const tr = v.trades;
|
|
69
|
+
const buys = tr.filter((x) => x.side === 'buy').length;
|
|
70
|
+
return {
|
|
71
|
+
mint: v.mint, symbol: v.symbol, price: extra.price ?? v.price, mcap: v.mcap, liq: v.liq, chg5m: v.chg.m5, chg1h: v.chg.h1,
|
|
72
|
+
buys: tr.length ? buys : null, sells: tr.length ? tr.length - buys : null, wallets: tr.length ? distinct(tr.map((x) => x.wallet)) : null,
|
|
73
|
+
holders: v.holders, top10: extra.top10 ?? v.top10, mintAuth: v.mintAuth, freezeAuth: v.freezeAuth, lpLocked: v.lpLocked,
|
|
74
|
+
dev: extra.dev ?? (v.devMints === null ? null : [`${v.devMints} mints by dev (jup)`, v.devMints > 3 ? 'warn' : 'ok']),
|
|
75
|
+
rugScore: v.rug?.score ?? null, larpVerdict: extra.larpVerdict ?? null, closes: v.candles.map((k) => k.c),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
79
|
+
// 1) reasoning stream
|
|
80
|
+
/** Agent thoughts and dexterm narration, newest last (≤ max). */
|
|
81
|
+
export function agentStream(v, tz, max = 24) {
|
|
82
|
+
const hasAgent = v.thoughts.some((x) => x.source === 'agent');
|
|
83
|
+
const lines = v.thoughts.slice(-max).map((x) => ({
|
|
84
|
+
t: x.t, clock: clock(new Date(x.t), tz), tag: PHASE_TAG[x.phase ?? 'idle'], text: x.text,
|
|
85
|
+
kind: x.source === 'agent' ? 'agent' : 'narration', demo: !!x.demo,
|
|
86
|
+
}));
|
|
87
|
+
const demo = lines.some((l) => l.demo);
|
|
88
|
+
return { lines, label: hasAgent ? 'agent thoughts + dexterm narration' : 'dexterm narration · no thoughts feed', demo };
|
|
89
|
+
}
|
|
90
|
+
const SCAN_TAG = { start: 'SCANNING', lock: 'LOCKED', refresh: 'READING', release: 'SCANNING', stop: 'OFF', cancel: 'PAUSED', blocked: 'BLOCKED' };
|
|
91
|
+
/** The scan's log lines plus the analyst's current answer (tool lines, then its sentences), newest last. */
|
|
92
|
+
export function scanStream(log, answer, tz, max = 24) {
|
|
93
|
+
const out = log.map((e) => ({ t: e.t, clock: clock(new Date(e.t), tz), tag: SCAN_TAG[e.kind], text: ribbonText(`${e.symbol ? `$${e.symbol} · ` : ''}${e.text}`, 140), kind: 'scan', demo: false }));
|
|
94
|
+
if (answer) {
|
|
95
|
+
const t0 = answer.startedAt;
|
|
96
|
+
answer.toolLines.slice(-4).forEach((tl, i) => out.push({ t: t0 + i, clock: clock(new Date(t0), tz), tag: 'READING', text: ribbonText(`↳ ${tl}`, 120), kind: 'narration', demo: false }));
|
|
97
|
+
const tEnd = answer.doneAt ?? t0 + 5;
|
|
98
|
+
for (const s of ribbonLines(answer.text, 140, 6))
|
|
99
|
+
out.push({ t: tEnd, clock: clock(new Date(tEnd), tz), tag: answer.status === 'done' ? 'READ' : 'READING', text: s, kind: 'agent', demo: false });
|
|
100
|
+
if (answer.verdict)
|
|
101
|
+
out.push({ t: tEnd + 1, clock: clock(new Date(tEnd), tz), tag: 'VERDICT', text: ribbonText(`verdict ${answer.verdict}`, 60), kind: 'agent', demo: false });
|
|
102
|
+
}
|
|
103
|
+
out.sort((a, b) => a.t - b.t);
|
|
104
|
+
return out.slice(-max);
|
|
105
|
+
}
|
|
106
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
107
|
+
// 2) vetting checklist
|
|
108
|
+
/** The app's liquidity floor (the AI scan's minimum liquidity, DEFAULT_SCAN_CONFIG.minLiqUsd). */
|
|
109
|
+
export const LIQ_FLOOR_USD = 10_000;
|
|
110
|
+
const st = (l) => (l === 'ok' ? 'pass' : l === 'warn' ? 'warn' : l === 'bad' ? 'fail' : 'pending');
|
|
111
|
+
/**
|
|
112
|
+
* The checklist for the coin in focus. Values and thresholds are the token screen's (ON-CHAIN panel); a box is
|
|
113
|
+
* `pending` until its number exists. `focus` adds what the agent's own checks returned (verdict, rug heuristic,
|
|
114
|
+
* LARP flags) and which checks it ran (◆).
|
|
115
|
+
*/
|
|
116
|
+
export function vetRows(f, focus, floorUsd = LIQ_FLOOR_USD) {
|
|
117
|
+
const ran = new Set(focus && focus.mint === f.mint ? focus.checks : []);
|
|
118
|
+
const fc = focus && focus.mint === f.mint ? focus : null;
|
|
119
|
+
const rows = [];
|
|
120
|
+
// momentum
|
|
121
|
+
{
|
|
122
|
+
const a = f.chg5m, b = f.chg1h;
|
|
123
|
+
const lvl = a === null && b === null ? 'na' : (a ?? 0) > 0 && (b ?? 0) > 0 ? 'ok' : (a ?? 0) < 0 && (b ?? 0) < 0 ? 'bad' : 'warn';
|
|
124
|
+
rows.push({ key: 'momentum', label: 'momentum', value: a === null && b === null ? '—' : `5m ${a === null ? '—' : sgnPct(a)} · 1h ${b === null ? '—' : sgnPct(b)}`, state: st(lvl), byAgent: ran.has('chart') || ran.has('price') });
|
|
125
|
+
}
|
|
126
|
+
// liquidity vs floor
|
|
127
|
+
rows.push({ key: 'liquidity', label: 'liquidity', value: f.liq === null ? '—' : `${fmtUsd(f.liq, { digits: 1 })} ${f.liq >= floorUsd ? '≥' : '<'} ${fmtUsd(floorUsd, { digits: 0 })} floor`, state: f.liq === null ? 'pending' : f.liq >= floorUsd ? 'pass' : 'fail', byAgent: ran.has('price') });
|
|
128
|
+
// tape
|
|
129
|
+
{
|
|
130
|
+
const b = f.buys, s = f.sells;
|
|
131
|
+
const lvl = b === null || s === null ? 'na' : b >= s ? 'ok' : s > b * 2 ? 'bad' : 'warn';
|
|
132
|
+
rows.push({ key: 'tape', label: 'tape b/s', value: b === null || s === null ? '—' : `${b} buys / ${s} sells${f.wallets !== null ? ` · ${f.wallets} wallets` : ''}`, state: st(lvl), byAgent: ran.has('tape') });
|
|
133
|
+
}
|
|
134
|
+
// holders
|
|
135
|
+
rows.push({ key: 'holders', label: 'top-10 hold', value: f.top10 === null ? '—' : `${f.top10.toFixed(1)}%`, state: f.top10 === null ? 'pending' : f.top10 < 25 ? 'pass' : f.top10 < 35 ? 'warn' : 'fail', byAgent: ran.has('holders') });
|
|
136
|
+
// authorities
|
|
137
|
+
{
|
|
138
|
+
const m = f.mintAuth, z = f.freezeAuth;
|
|
139
|
+
const lvl = m === null && z === null ? 'na' : m || z ? 'bad' : m === null || z === null ? 'warn' : 'ok';
|
|
140
|
+
rows.push({ key: 'auth', label: 'mint/freeze', value: m === null && z === null ? '—' : `mint ${m === null ? '—' : m ? 'ENABLED' : 'revoked'} · freeze ${z === null ? '—' : z ? 'ENABLED' : 'revoked'}`, state: st(lvl), byAgent: ran.has('rug') });
|
|
141
|
+
}
|
|
142
|
+
// LP lock
|
|
143
|
+
rows.push({ key: 'lp', label: 'LP lock', value: f.lpLocked === null ? '—' : `${Math.round(f.lpLocked)}% locked/burned`, state: f.lpLocked === null ? 'pending' : f.lpLocked >= 90 ? 'pass' : 'fail', byAgent: ran.has('rug') });
|
|
144
|
+
// dev history
|
|
145
|
+
rows.push({ key: 'dev', label: 'dev history', value: f.dev ? f.dev[0] : '—', state: f.dev ? st(f.dev[1]) : 'pending', byAgent: ran.has('dev') });
|
|
146
|
+
// LARP flags: the agent's larp_check evidence first, else an existing detector verdict
|
|
147
|
+
if (fc?.larp) {
|
|
148
|
+
const n = fc.larp.flags;
|
|
149
|
+
rows.push({ key: 'larp', label: 'LARP flags', value: `${n} flag${n === 1 ? '' : 's'}${fc.larp.items[0] ? ` · ${fc.larp.items[0]}` : ''}`, state: n === 0 ? 'pass' : n <= 2 ? 'warn' : 'fail', byAgent: true });
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const v = f.larpVerdict;
|
|
153
|
+
const lvl = !v ? 'na' : v === 'LARP' ? 'bad' : v === 'REAL_PRODUCT' || v === 'EARLY_BUT_REAL' ? 'ok' : 'warn';
|
|
154
|
+
rows.push({ key: 'larp', label: 'LARP flags', value: v ? `detector: ${v.replace(/_/g, ' ').toLowerCase()}` : '—', state: st(lvl), byAgent: ran.has('larp') });
|
|
155
|
+
}
|
|
156
|
+
// rug heuristic (the analyze_token pre-registered heuristic) or the RugCheck score
|
|
157
|
+
if (fc?.verdict?.rug != null) {
|
|
158
|
+
const r = fc.verdict.rug;
|
|
159
|
+
rows.push({ key: 'rug', label: 'rug heuristic', value: `${(r * 100).toFixed(0)}%`, state: r < 0.3 ? 'pass' : r < 0.6 ? 'warn' : 'fail', byAgent: true });
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const r = f.rugScore;
|
|
163
|
+
rows.push({ key: 'rug', label: 'rug score', value: r === null ? '—' : `RugCheck ${Math.round(r)}/100`, state: r === null ? 'pending' : r < 30 ? 'pass' : r < 60 ? 'warn' : 'fail', byAgent: ran.has('rug') });
|
|
164
|
+
}
|
|
165
|
+
// verdict
|
|
166
|
+
{
|
|
167
|
+
const v = fc?.verdict?.label ?? null;
|
|
168
|
+
rows.push({ key: 'verdict', label: 'verdict', value: v ?? (fc ? 'not yet' : '—'), state: !v ? 'pending' : /WATCH|BUY|ENTER/i.test(v) ? 'pass' : /AVOID|RUG|SKIP/i.test(v) ? 'fail' : 'warn', byAgent: !!v });
|
|
169
|
+
}
|
|
170
|
+
return rows;
|
|
171
|
+
}
|
|
172
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
173
|
+
// 3) data strip
|
|
174
|
+
export function dataCells(f) {
|
|
175
|
+
const dir = (n) => (n === null || n === 0 ? 0 : n > 0 ? 1 : -1);
|
|
176
|
+
const flow = f.buys !== null && f.sells !== null ? `${f.buys}/${f.sells}` : '—';
|
|
177
|
+
return [
|
|
178
|
+
{ k: 'mcap', v: f.mcap === null ? '—' : fmtUsd(f.mcap, { digits: 1 }) },
|
|
179
|
+
{ k: 'price', v: f.price === null ? '—' : fmtPrice(f.price) },
|
|
180
|
+
{ k: 'liq', v: f.liq === null ? '—' : fmtUsd(f.liq, { digits: 1 }) },
|
|
181
|
+
{ k: '5m', v: f.chg5m === null ? '—' : sgnPct(f.chg5m), dir: dir(f.chg5m) },
|
|
182
|
+
{ k: '1h', v: f.chg1h === null ? '—' : sgnPct(f.chg1h), dir: dir(f.chg1h) },
|
|
183
|
+
{ k: 'b/s', v: flow, dir: f.buys !== null && f.sells !== null ? dir(f.buys - f.sells) : 0 },
|
|
184
|
+
{ k: 'holders', v: f.holders === null ? '—' : fmtNum(f.holders) },
|
|
185
|
+
];
|
|
186
|
+
}
|
|
187
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
188
|
+
// 4) plan + position
|
|
189
|
+
const TP_RE = /\b(?:TP ?1|TP|take[- ]profit|target)\b[^.;!?]{0,24}?([+]?\d{1,4}(?:\.\d+)?%|\$\d+(?:[.,]\d+)*|\d+(?:\.\d+)?x)/i;
|
|
190
|
+
const STOP_RE = /\b(?:stop(?:[- ]loss)?|SL)\b[^.;!?]{0,20}?([−-]\d{1,3}(?:\.\d+)?%|\$\d+(?:[.,]\d+)*)/i;
|
|
191
|
+
const NO_STOP_RE = /\bno (?:hard )?stop\b/i;
|
|
192
|
+
/** The take-profit / stop the agent SAID (its words, newest first): display text only, never an order. */
|
|
193
|
+
export function planFromThoughts(thoughts, since) {
|
|
194
|
+
let tp = null, stop = null;
|
|
195
|
+
for (let i = thoughts.length - 1; i >= 0; i--) {
|
|
196
|
+
const x = thoughts[i];
|
|
197
|
+
if (x.source !== 'agent' || x.t < since - 60_000)
|
|
198
|
+
continue;
|
|
199
|
+
if (!tp) {
|
|
200
|
+
const m = TP_RE.exec(x.text);
|
|
201
|
+
if (m)
|
|
202
|
+
tp = m[1].replace('-', '−');
|
|
203
|
+
}
|
|
204
|
+
if (!stop) {
|
|
205
|
+
if (NO_STOP_RE.test(x.text))
|
|
206
|
+
stop = 'no stop';
|
|
207
|
+
else {
|
|
208
|
+
const m = STOP_RE.exec(x.text);
|
|
209
|
+
if (m)
|
|
210
|
+
stop = m[1].replace('-', '−');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (tp && stop)
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
return { tp, stop };
|
|
217
|
+
}
|
|
218
|
+
/** The PLAN / POSITION block for the agent's PAPER arc (null when there is no ticket or position). */
|
|
219
|
+
export function planVM(v, now) {
|
|
220
|
+
const a = v.arc;
|
|
221
|
+
if (!a || a.stage === 'closed' || a.stage === 'refused')
|
|
222
|
+
return null;
|
|
223
|
+
const said = planFromThoughts(v.thoughts, a.openedAt);
|
|
224
|
+
const open = a.stage === 'open';
|
|
225
|
+
const pnlUsd = open && a.pnlPct !== null && a.sizeUsd !== null ? a.sizeUsd * a.pnlPct : null;
|
|
226
|
+
const dd = open && a.pnlPct !== null && a.peakPct !== null ? (1 + a.pnlPct) / (1 + a.peakPct) - 1 : null;
|
|
227
|
+
const tp = said.tp ? `TP ${said.tp} (agent's plan)` : 'TP agent-managed';
|
|
228
|
+
const stop = said.stop === 'no stop' || !said.stop ? 'no stop' : `stop ${said.stop} (agent's plan)`;
|
|
229
|
+
const waiting = !open
|
|
230
|
+
? (a.sizeUsd !== null ? 'the agent to confirm the sized ticket' : 'the agent to size the ticket')
|
|
231
|
+
: said.tp ? `TP ${said.tp} · agent-managed exit, no resting order on PAPER` : 'the agent\'s exit call · no resting orders on PAPER';
|
|
232
|
+
return {
|
|
233
|
+
symbol: a.symbol, stage: a.stage,
|
|
234
|
+
entry: a.entryUsd !== null ? `$${fmtPx(a.entryUsd)}` : '—',
|
|
235
|
+
size: a.sizeUsd !== null ? `$${a.sizeUsd.toFixed(2)}${a.spentSol > 0 ? ` (${a.spentSol.toFixed(4)} SOL)` : ''}` : a.spentSol > 0 ? `${a.spentSol.toFixed(4)} SOL` : 'sizing…',
|
|
236
|
+
tp, stop,
|
|
237
|
+
pnlPct: open ? a.pnlPct : null, pnlUsd, peakPct: open ? a.peakPct : null, ddPct: dd,
|
|
238
|
+
heldMs: open && a.filledAt !== null ? now - a.filledAt : null,
|
|
239
|
+
mark: a.mark ? `$${fmtPx(a.mark.priceUsd)}` : '—',
|
|
240
|
+
waiting, trace: a.trace.map((s) => s.pct),
|
|
241
|
+
refused: open && a.refusal && v.beats.refusedAt !== null && now - v.beats.refusedAt < 3500 ? a.refusal : null,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
245
|
+
// 5) decision log + scoreboard
|
|
246
|
+
const DEC_GLYPH = { pass: ['·', 'na'], reject: ['✗', 'bad'], enter: ['▲', 'ok'], exit: ['▼', 'warn'], refused: ['⊘', 'bad'] };
|
|
247
|
+
const DEC_VERB = { pass: 'passed', reject: 'rejected', enter: '', exit: '', refused: '' };
|
|
248
|
+
export function decisionLog(v, tz) {
|
|
249
|
+
return v.decisions.map((d) => {
|
|
250
|
+
const [glyph, level] = DEC_GLYPH[d.kind];
|
|
251
|
+
const verb = DEC_VERB[d.kind];
|
|
252
|
+
return { t: d.t, clock: clock(new Date(d.t), tz).slice(0, 5), glyph, level, symbol: d.symbol, text: verb ? `${verb} · ${d.text}` : d.text, said: d.said };
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
export function scoreLine(s) {
|
|
256
|
+
return `PAPER simulated · vetted ${s.attempts} · entries ${s.entries} · closed ${s.closed}${s.closed ? ` (${s.wins}W)` : ''} · ${sgnSol(s.realizedSol)}`;
|
|
257
|
+
}
|
|
258
|
+
export function scanLogLines(log, tz) {
|
|
259
|
+
const lv = { start: ['◉', 'na'], lock: ['▲', 'ok'], refresh: ['↻', 'na'], release: ['·', 'na'], stop: ['■', 'warn'], cancel: ['❚', 'warn'], blocked: ['⊘', 'bad'] };
|
|
260
|
+
return log.map((e) => ({ t: e.t, clock: clock(new Date(e.t), tz).slice(0, 5), glyph: lv[e.kind][0], level: lv[e.kind][1], symbol: e.symbol, text: e.text, said: null }));
|
|
261
|
+
}
|
|
262
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
263
|
+
// the whole model
|
|
264
|
+
export function agentBrainModel(v, facts, now, tz) {
|
|
265
|
+
const s = agentStream(v, tz);
|
|
266
|
+
const status = v.control === 'paused' ? '❚❚ paused by you · its next call is held' : v.control === 'takeover' ? 'you are driving · the agent is frozen' : null;
|
|
267
|
+
return {
|
|
268
|
+
mode: 'agent', title: `✦ BRAIN · ${v.agent}`, streamLabel: s.label, demo: s.demo, stream: s.lines, status,
|
|
269
|
+
coin: facts ? { symbol: facts.symbol, mint: facts.mint } : v.focus ? { symbol: v.focus.symbol, mint: v.focus.mint } : null,
|
|
270
|
+
vet: facts ? vetRows(facts, v.focus) : [],
|
|
271
|
+
data: facts ? dataCells(facts) : [], closes: facts?.closes ?? [],
|
|
272
|
+
plan: planVM(v, now), log: decisionLog(v, tz), score: scoreLine(v.score),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
export function scanBrainModel(s, facts, tz) {
|
|
276
|
+
const locks = s.log.filter((e) => e.kind === 'lock').length;
|
|
277
|
+
const reads = s.log.filter((e) => e.kind === 'lock' || e.kind === 'refresh').length;
|
|
278
|
+
return {
|
|
279
|
+
mode: 'scan', title: '✦ BRAIN · AI scan', streamLabel: 'scan log + the scan analyst (observe-only)', demo: false,
|
|
280
|
+
stream: scanStream(s.log, s.answer, tz), status: `◉ ${s.mode}${s.lock ? ` $${s.lock.symbol} · score ${s.lock.score} · read ${s.lock.reads}` : ''} · ${s.scanned} coins${s.note ? ` · ${s.note}` : ''}`,
|
|
281
|
+
coin: facts ? { symbol: facts.symbol, mint: facts.mint } : null,
|
|
282
|
+
vet: facts ? vetRows(facts, null, s.floorUsd) : [], data: facts ? dataCells(facts) : [], closes: facts?.closes ?? [],
|
|
283
|
+
plan: null, log: scanLogLines(s.log, tz), score: `locks ${locks} · reads ${reads} · ${s.usage || 'ai cap'} · observe-only · never trades`,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/** Rows each block wants. */
|
|
287
|
+
const VET_FULL = (n) => 1 + n;
|
|
288
|
+
const VET_COMPACT = (n) => 1 + Math.ceil(n / 2);
|
|
289
|
+
const DATA_ROWS = 3;
|
|
290
|
+
const PLAN_ROWS = 6;
|
|
291
|
+
const LOG_ROWS = (n) => 2 + Math.min(6, Math.max(1, n));
|
|
292
|
+
const STREAM_MIN = 4;
|
|
293
|
+
/**
|
|
294
|
+
* Fit the blocks into `h` rows (inside the border) of a panel `w` wide. Priority to keep: plan > stream > vetting >
|
|
295
|
+
* data > decisions; the vetting list folds to two columns before it goes. The stream takes what is left. Returned in
|
|
296
|
+
* draw order (top to bottom: stream, vetting, data, plan, decisions).
|
|
297
|
+
*/
|
|
298
|
+
export function layoutBlocks(m, w, h) {
|
|
299
|
+
const wants = [];
|
|
300
|
+
const nVet = m.vet.length;
|
|
301
|
+
let vet = nVet ? { id: 'vet', rows: VET_FULL(nVet) } : null;
|
|
302
|
+
let data = m.data.length ? { id: 'data', rows: DATA_ROWS } : null;
|
|
303
|
+
let log = { id: 'log', rows: LOG_ROWS(m.log.length) };
|
|
304
|
+
const plan = m.plan ? { id: 'plan', rows: PLAN_ROWS } : null;
|
|
305
|
+
const used = () => (vet?.rows ?? 0) + (data?.rows ?? 0) + (log?.rows ?? 0) + (plan?.rows ?? 0);
|
|
306
|
+
const room = () => h - used();
|
|
307
|
+
// the stream wants a third of the panel when it can have it
|
|
308
|
+
const want = Math.max(STREAM_MIN, Math.floor(h / 3));
|
|
309
|
+
if (room() < want && log)
|
|
310
|
+
log = log.rows > 4 && room() + (log.rows - 4) >= want ? { id: 'log', rows: 4 } : null;
|
|
311
|
+
if (room() < want && data)
|
|
312
|
+
data = null;
|
|
313
|
+
if (room() < want && vet && w >= 44 && nVet > 2)
|
|
314
|
+
vet = { id: 'vet', rows: VET_COMPACT(nVet), compact: true };
|
|
315
|
+
if (room() < STREAM_MIN && vet)
|
|
316
|
+
vet = null;
|
|
317
|
+
if (room() < STREAM_MIN && log)
|
|
318
|
+
log = null;
|
|
319
|
+
const streamRows = Math.max(0, room());
|
|
320
|
+
wants.push({ id: 'stream', rows: streamRows });
|
|
321
|
+
if (vet)
|
|
322
|
+
wants.push(vet);
|
|
323
|
+
if (data)
|
|
324
|
+
wants.push(data);
|
|
325
|
+
if (plan)
|
|
326
|
+
wants.push(plan);
|
|
327
|
+
if (log)
|
|
328
|
+
wants.push(log);
|
|
329
|
+
return wants.filter((b) => b.rows > 0);
|
|
330
|
+
}
|
|
331
|
+
const STATE_GLYPH = { pass: '✓', warn: '!', fail: '✗', pending: '·' };
|
|
332
|
+
function levelColor(c, l) {
|
|
333
|
+
const t = c.theme;
|
|
334
|
+
return l === 'ok' || l === 'pass' ? t.up : l === 'warn' ? t.warn : l === 'bad' || l === 'fail' ? t.down : t.dim;
|
|
335
|
+
}
|
|
336
|
+
function section(c, x, y, w, title, right, bg, col) {
|
|
337
|
+
const t = c.theme;
|
|
338
|
+
c.hline(x, y, w, { fg: mix(bg, t.border, 0.9), bg, soft: true });
|
|
339
|
+
const tx = c.text(x + 1, y, ` ${title} `, { fg: col, bg, b: true });
|
|
340
|
+
if (right) {
|
|
341
|
+
const r = clip(right, Math.max(0, x + w - tx - 3));
|
|
342
|
+
if (r)
|
|
343
|
+
c.text(x + w - r.length - 2, y, ` ${r}`, { fg: t.dim, bg });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/** Draw the brain view into the analyst panel rectangle. */
|
|
347
|
+
export function renderBrainPanel(c, x, y, w, h, m, o) {
|
|
348
|
+
const t = c.theme;
|
|
349
|
+
const bc = mix(t.border, t.ai, 0.55);
|
|
350
|
+
const cap = m.mode === 'agent' ? 'display only · cannot trade' : 'observe-only · never trades';
|
|
351
|
+
const box = c.box(x, y, w, h, { title: m.title, titleFg: t.ai, caption: cap, captionFg: t.dim, border: bc });
|
|
352
|
+
const bg = box.bg;
|
|
353
|
+
const iw = box.w;
|
|
354
|
+
c.noRain(box.x, box.y, box.w, box.h);
|
|
355
|
+
const blocks = layoutBlocks(m, iw, box.h);
|
|
356
|
+
let cy = box.y;
|
|
357
|
+
for (const b of blocks) {
|
|
358
|
+
const y0 = cy;
|
|
359
|
+
if (b.id === 'stream')
|
|
360
|
+
drawStream(c, box.x, y0, iw, b.rows, m, o, bg);
|
|
361
|
+
else if (b.id === 'vet')
|
|
362
|
+
drawVet(c, box.x, y0, iw, b.rows, m, b.compact === true, bg);
|
|
363
|
+
else if (b.id === 'data')
|
|
364
|
+
drawData(c, box.x, y0, iw, m, bg);
|
|
365
|
+
else if (b.id === 'plan' && m.plan)
|
|
366
|
+
drawPlan(c, box.x, y0, iw, m.plan, o, bg);
|
|
367
|
+
else if (b.id === 'log')
|
|
368
|
+
drawLog(c, box.x, y0, iw, b.rows, m, bg);
|
|
369
|
+
cy += b.rows;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function drawStream(c, x, y, w, rows, m, o, bg) {
|
|
373
|
+
const t = c.theme;
|
|
374
|
+
if (rows <= 0)
|
|
375
|
+
return;
|
|
376
|
+
// header: what is talking, plus the coral demo-text tag when a stand-in feed wrote the lines
|
|
377
|
+
let hx = c.text(x + 1, y, 'REASONING', { fg: t.ai, bg, b: true });
|
|
378
|
+
hx = c.text(hx + 1, y, `· ${m.streamLabel}`, { fg: t.dim, bg }, x + w - hx - 2);
|
|
379
|
+
if (m.demo) {
|
|
380
|
+
const tag = ' ✦ claude lines: demo text ';
|
|
381
|
+
c.text(x + w - tag.length - 1, y, tag, { fg: t.bg, bg: t.ai, b: true });
|
|
382
|
+
}
|
|
383
|
+
const statusRows = m.status ? 1 : 0;
|
|
384
|
+
const area = rows - 1 - statusRows;
|
|
385
|
+
const bottom = y + 1 + area; // exclusive
|
|
386
|
+
if (m.status)
|
|
387
|
+
c.text(x + 1, bottom, clip(m.status, w - 2), { fg: t.warn, bg, b: true });
|
|
388
|
+
if (area <= 0)
|
|
389
|
+
return;
|
|
390
|
+
if (!m.stream.length) {
|
|
391
|
+
c.text(x + 2, y + 1, m.mode === 'agent' ? '› waiting for the agent\'s first move' : '› watching… nothing has locked yet', { fg: t.dim, bg, i: true }, w - 3);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
// "HH:MM:SS " + "EXECUTING " + a space; a narrow panel drops the clock and keeps the phase tag
|
|
395
|
+
const narrow = w < 56;
|
|
396
|
+
const gutter = narrow ? 11 : 9 + 11;
|
|
397
|
+
const tw = Math.max(8, w - gutter - 3);
|
|
398
|
+
const vrows = [];
|
|
399
|
+
for (let i = m.stream.length - 1, rank = 0; i >= 0 && vrows.length < area; i--, rank++) {
|
|
400
|
+
const ln = m.stream[i];
|
|
401
|
+
const lead = ln.kind === 'agent' ? '❯ ' : ln.kind === 'scan' ? '◉ ' : '› ';
|
|
402
|
+
const body = lead + ln.text;
|
|
403
|
+
const parts = rank === 0 ? wrap(body, tw).slice(0, 3) : [clip(body, tw)];
|
|
404
|
+
if (rank === 0 && wrap(body, tw).length > 3)
|
|
405
|
+
parts[2] = clip(parts[2] + ' …', tw);
|
|
406
|
+
for (let k = parts.length - 1; k >= 0 && vrows.length < area; k--)
|
|
407
|
+
vrows.push({ line: ln, text: parts[k], first: k === 0, newest: rank === 0, rank });
|
|
408
|
+
}
|
|
409
|
+
vrows.reverse();
|
|
410
|
+
let yy = bottom - vrows.length;
|
|
411
|
+
// typing budget for the newest line (calm mode: shown at once)
|
|
412
|
+
const newest = m.stream[m.stream.length - 1];
|
|
413
|
+
let budget = o.calm ? Infinity : Math.max(0, Math.floor((o.now - newest.t) / 14));
|
|
414
|
+
for (const r of vrows) {
|
|
415
|
+
const fade = r.newest ? 0 : Math.min(0.62, 0.12 + r.rank * 0.07);
|
|
416
|
+
const base = r.line.kind === 'agent' ? mix(t.text, t.ai, 0.3) : r.line.kind === 'scan' ? t.text : t.muted;
|
|
417
|
+
const fg = mix(base, bg, fade);
|
|
418
|
+
if (r.first) {
|
|
419
|
+
if (!narrow)
|
|
420
|
+
c.text(x + 1, yy, r.line.clock, { fg: mix(t.dim, bg, fade * 0.6), bg });
|
|
421
|
+
c.text(x + (narrow ? 1 : 10), yy, r.line.tag.slice(0, 9), { fg: mix(tagColor(c, r.line.tag), bg, fade), bg, b: r.newest });
|
|
422
|
+
}
|
|
423
|
+
const st = { fg, bg, i: r.line.kind === 'narration' };
|
|
424
|
+
if (r.newest && budget < r.text.length) {
|
|
425
|
+
const end = decryptText(c, x + gutter, yy, r.text, Math.max(0, budget), Math.floor(o.now / 33), st, tw);
|
|
426
|
+
if (budget >= 0)
|
|
427
|
+
c.put(Math.min(end, x + w - 2), yy, '▌', { fg: t.ai, bg, soft: true });
|
|
428
|
+
budget = -1;
|
|
429
|
+
}
|
|
430
|
+
else if (r.newest && budget < 0) {
|
|
431
|
+
// the rest of a line still typing: nothing yet
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
c.text(x + gutter, yy, r.text, st, tw);
|
|
435
|
+
if (r.newest)
|
|
436
|
+
budget -= r.text.length + 1;
|
|
437
|
+
}
|
|
438
|
+
yy += 1;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function tagColor(c, tag) {
|
|
442
|
+
const t = c.theme;
|
|
443
|
+
switch (tag) {
|
|
444
|
+
case 'SCANNING': return t.accent;
|
|
445
|
+
case 'VETTING':
|
|
446
|
+
case 'READING':
|
|
447
|
+
case 'READ': return t.borderFocus;
|
|
448
|
+
case 'SIZING': return t.meteora;
|
|
449
|
+
case 'EXECUTING':
|
|
450
|
+
case 'LOCKED': return t.up;
|
|
451
|
+
case 'HOLDING': return t.magenta;
|
|
452
|
+
case 'EXITING':
|
|
453
|
+
case 'VERDICT': return t.ai;
|
|
454
|
+
case 'BLOCKED':
|
|
455
|
+
case 'OFF': return t.down;
|
|
456
|
+
case 'PAUSED': return t.warn;
|
|
457
|
+
default: return t.dim;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function drawVet(c, x, y, w, rows, m, compact, bg) {
|
|
461
|
+
const t = c.theme;
|
|
462
|
+
const done = m.vet.filter((r) => r.state !== 'pending').length;
|
|
463
|
+
section(c, x, y, w, `VETTING${m.coin ? ` $${m.coin.symbol}` : ''}`, `${done}/${m.vet.length} evaluated${m.mode === 'agent' ? ' · ◆ agent ran it' : ''}`, bg, t.borderFocus);
|
|
464
|
+
const rowsAvail = rows - 1;
|
|
465
|
+
if (!compact) {
|
|
466
|
+
m.vet.slice(0, rowsAvail).forEach((r, i) => {
|
|
467
|
+
const yy = y + 1 + i;
|
|
468
|
+
const col = levelColor(c, r.state);
|
|
469
|
+
c.text(x + 1, yy, STATE_GLYPH[r.state], { fg: col, bg, b: r.state !== 'pending' });
|
|
470
|
+
c.text(x + 3, yy, r.byAgent ? '◆' : ' ', { fg: t.ai, bg });
|
|
471
|
+
c.text(x + 5, yy, r.label, { fg: t.muted, bg }, 13);
|
|
472
|
+
const val = clip(r.value, Math.max(4, w - 26));
|
|
473
|
+
c.text(x + 19, yy, val, { fg: r.state === 'pending' ? t.dim : r.state === 'pass' ? t.text : col, bg, b: r.state === 'fail', i: r.state === 'pending' });
|
|
474
|
+
const word = r.state === 'pending' ? 'pending' : r.state;
|
|
475
|
+
c.textR(x + 1, yy, w - 2, word, { fg: col, bg, b: r.state === 'fail' });
|
|
476
|
+
});
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
// two columns: glyph + label + short value
|
|
480
|
+
const colW = Math.floor((w - 2) / 2);
|
|
481
|
+
m.vet.forEach((r, i) => {
|
|
482
|
+
const row = Math.floor(i / 2);
|
|
483
|
+
if (row >= rowsAvail)
|
|
484
|
+
return;
|
|
485
|
+
const cx = x + 1 + (i % 2) * colW;
|
|
486
|
+
const yy = y + 1 + row;
|
|
487
|
+
const col = levelColor(c, r.state);
|
|
488
|
+
c.text(cx, yy, STATE_GLYPH[r.state], { fg: col, bg, b: true });
|
|
489
|
+
c.text(cx + 2, yy, clip(`${r.label} ${r.value === '—' ? '' : r.value.split(' · ')[0]}`, colW - 3), { fg: r.state === 'pending' ? t.dim : t.text, bg });
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
function drawData(c, x, y, w, m, bg) {
|
|
493
|
+
const t = c.theme;
|
|
494
|
+
section(c, x, y, w, 'DATA', 'app numbers · no extra fetch', bg, t.accent);
|
|
495
|
+
let cx = x + 1;
|
|
496
|
+
for (const d of m.data) {
|
|
497
|
+
const need = d.k.length + 1 + d.v.length + 2;
|
|
498
|
+
if (cx + need > x + w - 1)
|
|
499
|
+
break;
|
|
500
|
+
cx = c.text(cx, y + 1, d.k, { fg: t.dim, bg });
|
|
501
|
+
const fg = d.dir === 1 ? t.up : d.dir === -1 ? t.down : t.text;
|
|
502
|
+
cx = c.text(cx + 1, y + 1, d.v, { fg, bg, b: d.dir !== undefined && d.dir !== 0 }) + 2;
|
|
503
|
+
}
|
|
504
|
+
// the candle trace: one cell per close (most recent at the right)
|
|
505
|
+
const LV = '▁▂▃▄▅▆▇█';
|
|
506
|
+
const tw = w - 4;
|
|
507
|
+
const cl = m.closes.slice(-tw);
|
|
508
|
+
if (cl.length > 1) {
|
|
509
|
+
const lo = Math.min(...cl), hi = Math.max(...cl), span = hi - lo || 1;
|
|
510
|
+
const up = cl[cl.length - 1] >= cl[0];
|
|
511
|
+
c.text(x + 1, y + 2, up ? '▲' : '▼', { fg: up ? t.up : t.down, bg, b: true });
|
|
512
|
+
cl.forEach((v, i) => c.put(x + 3 + i, y + 2, LV[Math.round(((v - lo) / span) * 7)], { fg: mix(bg, up ? t.up : t.down, 0.45 + 0.55 * (i / Math.max(1, cl.length - 1))), bg, soft: true }));
|
|
513
|
+
}
|
|
514
|
+
else
|
|
515
|
+
c.text(x + 1, y + 2, '· no candles yet', { fg: t.dim, bg, i: true });
|
|
516
|
+
}
|
|
517
|
+
function drawPlan(c, x, y, w, p, o, bg) {
|
|
518
|
+
const t = c.theme;
|
|
519
|
+
const open = p.stage === 'open';
|
|
520
|
+
section(c, x, y, w, open ? `POSITION · PAPER $${p.symbol}` : `PLAN · PAPER $${p.symbol}`, open ? `held ${p.heldMs !== null ? fmtDur(p.heldMs) : '—'}` : p.stage === 'sized' ? 'sized · not filled' : 'ticket open', bg, t.magenta);
|
|
521
|
+
const pc = p.pnlPct === null ? t.muted : p.pnlPct >= 0 ? t.up : t.down;
|
|
522
|
+
let yy = y + 1;
|
|
523
|
+
if (open) {
|
|
524
|
+
let cx = c.text(x + 1, yy, 'MARK PnL ', { fg: t.dim, bg });
|
|
525
|
+
cx = c.text(cx, yy, p.pnlPct === null ? '—' : sgnRatio(p.pnlPct), { fg: pc, bg, b: true });
|
|
526
|
+
if (p.pnlUsd !== null)
|
|
527
|
+
cx = c.text(cx + 1, yy, `(${sgnUsd(p.pnlUsd)} paper)`, { fg: pc, bg });
|
|
528
|
+
if (p.peakPct !== null)
|
|
529
|
+
cx = c.text(cx + 2, yy, `peak ${sgnRatio(p.peakPct)}`, { fg: p.peakPct >= 0 ? t.up : t.down, bg });
|
|
530
|
+
if (p.ddPct !== null && p.ddPct < -0.0005)
|
|
531
|
+
c.text(cx + 2, yy, `dd ${sgnRatio(p.ddPct)}`, { fg: t.warn, bg }, x + w - cx - 3);
|
|
532
|
+
// trace at the right of the next row
|
|
533
|
+
}
|
|
534
|
+
else
|
|
535
|
+
c.text(x + 1, yy, `size ${p.size}`, { fg: t.text, bg, b: true }, w - 2);
|
|
536
|
+
yy++;
|
|
537
|
+
c.text(x + 1, yy, clip(`entry ${p.entry} · mark ${p.mark} · size ${p.size}`, w - 2), { fg: t.muted, bg });
|
|
538
|
+
yy++;
|
|
539
|
+
c.text(x + 1, yy, clip(`${p.tp} · ${p.stop}`, w - 2), { fg: t.text, bg });
|
|
540
|
+
yy++;
|
|
541
|
+
if (p.refused)
|
|
542
|
+
c.text(x + 1, yy, clip(` ✗ REFUSED BY THE DESK · ${p.refused} `, w - 2), { fg: t.bg, bg: t.down, b: true });
|
|
543
|
+
else
|
|
544
|
+
c.text(x + 1, yy, clip(`waiting for ${p.waiting}`, w - 2), { fg: t.ai, bg, i: true });
|
|
545
|
+
yy++;
|
|
546
|
+
const tr = p.trace.slice(-(w - 4));
|
|
547
|
+
if (tr.length > 1) {
|
|
548
|
+
const lo = Math.min(0, ...tr), hi = Math.max(0, ...tr), span = hi - lo || 1;
|
|
549
|
+
const LV = '▁▂▃▄▅▆▇█';
|
|
550
|
+
tr.forEach((v, i) => c.put(x + 2 + i, yy, LV[Math.round(((v - lo) / span) * 7)], { fg: v >= 0 ? t.up : t.down, bg, soft: true }));
|
|
551
|
+
}
|
|
552
|
+
else
|
|
553
|
+
c.text(x + 1, yy, open ? '· waiting for the first mark tick' : '· PAPER · simulated ledger', { fg: t.dim, bg, i: true });
|
|
554
|
+
void o;
|
|
555
|
+
}
|
|
556
|
+
function drawLog(c, x, y, w, rows, m, bg) {
|
|
557
|
+
const t = c.theme;
|
|
558
|
+
section(c, x, y, w, m.mode === 'agent' ? 'DECISIONS' : 'SCAN LOG', 'this session', bg, t.ai);
|
|
559
|
+
const area = rows - 2;
|
|
560
|
+
const lines = m.log.slice(-Math.max(0, area));
|
|
561
|
+
if (!lines.length)
|
|
562
|
+
c.text(x + 2, y + 1, m.mode === 'agent' ? '· no decisions yet' : '· nothing locked yet', { fg: t.dim, bg, i: true });
|
|
563
|
+
lines.forEach((l, i) => {
|
|
564
|
+
const yy = y + 1 + i;
|
|
565
|
+
const col = levelColor(c, l.level);
|
|
566
|
+
let cx = c.text(x + 1, yy, l.clock, { fg: t.dim, bg });
|
|
567
|
+
cx = c.text(cx + 1, yy, l.glyph, { fg: col, bg, b: true });
|
|
568
|
+
if (l.symbol)
|
|
569
|
+
cx = c.text(cx + 1, yy, `$${l.symbol}`, { fg: t.text, bg, b: true });
|
|
570
|
+
cx = c.text(cx + 1, yy, l.text, { fg: l.level === 'na' ? t.muted : col, bg }, x + w - cx - 2);
|
|
571
|
+
if (l.said && cx + 8 < x + w - 1)
|
|
572
|
+
c.text(cx + 1, yy, `❯ ${l.said}`, { fg: mix(t.ai, bg, 0.35), bg, i: true }, x + w - cx - 2);
|
|
573
|
+
});
|
|
574
|
+
if (m.score)
|
|
575
|
+
c.text(x + 1, y + rows - 1, clip(m.score, w - 2), { fg: t.warn, bg, b: true });
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* The brain view for the coin on screen, or null when the normal analyst panel should draw: no auto mode, the agent
|
|
579
|
+
* was killed / went quiet / has not acted yet, the scan is off, or the user is typing into the analyst.
|
|
580
|
+
*/
|
|
581
|
+
export function brainFor(h, facts, calm, scanUsage = '') {
|
|
582
|
+
if (h.aiFocused)
|
|
583
|
+
return null;
|
|
584
|
+
const hud = h.agentHud;
|
|
585
|
+
if (hud) {
|
|
586
|
+
const now = hud.clock();
|
|
587
|
+
const v = hud.story.view(now);
|
|
588
|
+
if (agentBrainActive(v, now))
|
|
589
|
+
return { model: agentBrainModel(v, facts, now, h.timeZone), opts: { now, calm: calm || !!hud.reduced, depth: h.colorDepth, frame: h.vfx.frame } };
|
|
590
|
+
}
|
|
591
|
+
const scan = h.scan;
|
|
592
|
+
if (scan?.engine.on) {
|
|
593
|
+
const e = scan.engine;
|
|
594
|
+
const now = h.now.getTime();
|
|
595
|
+
// the scan's brain shows on the locked coin (or any coin while it is still scanning)
|
|
596
|
+
if (e.lock && facts && e.lock.mint !== facts.mint)
|
|
597
|
+
return null;
|
|
598
|
+
const sess = e.lock ? h.analystByMint?.get(e.lock.mint) : null;
|
|
599
|
+
const answer = isScanAnswer(sess) ? sess : null;
|
|
600
|
+
const model = scanBrainModel({ mode: e.mode.toUpperCase(), lock: e.lock, note: e.note, scanned: e.scanned, floorUsd: e.cfg.minLiqUsd, log: scan.log ?? [], answer, usage: scanUsage }, facts, h.timeZone);
|
|
601
|
+
return { model, opts: { now, calm, depth: h.colorDepth, frame: h.vfx.frame } };
|
|
602
|
+
}
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
function isScanAnswer(x) {
|
|
606
|
+
const o = x;
|
|
607
|
+
return !!o && typeof o === 'object' && typeof o.text === 'string' && Array.isArray(o.toolLines) && typeof o.startedAt === 'number';
|
|
608
|
+
}
|
|
@@ -75,7 +75,7 @@ export function renderNewPairs(c, state) {
|
|
|
75
75
|
cv.text(x, y, '✓ clean', { fg: mix(bg, t.up, 0.7), bg });
|
|
76
76
|
} },
|
|
77
77
|
];
|
|
78
|
-
table(c, box.x + 1, box.y, box.w - 2, box.h, { columns: cols, rows: pairs, selected: sel, scroll: 0, bg: box.bg });
|
|
78
|
+
table(c, box.x + 1, box.y, box.w - 2, box.h, { columns: cols, rows: pairs, selected: sel, scroll: 0, bg: box.bg, rowKey: (r) => r.mint });
|
|
79
79
|
if (sideW) {
|
|
80
80
|
c.rainZone(tableW, top, 1, bottom - top, 1);
|
|
81
81
|
pairSpotlight(c, c.w - sideW, top, sideW, bottom - top, pairs[sel], priceModeOf(state));
|