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.
Files changed (93) hide show
  1. package/dist/packages/agents/src/index.js +2 -0
  2. package/dist/packages/agents/src/spectator.js +54 -0
  3. package/dist/packages/agents/src/story.js +523 -0
  4. package/dist/packages/agents/src/tiers.js +1 -0
  5. package/dist/packages/agents/src/tools.js +1 -0
  6. package/dist/packages/cli/assets/claude/plugin/skills/REGISTRY.json +1 -1
  7. package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +15 -8
  8. package/dist/packages/cli/src/agent-cmd.js +6 -4
  9. package/dist/packages/cli/src/agent-spectator.js +17 -1
  10. package/dist/packages/cli/src/analyst/backend.js +23 -0
  11. package/dist/packages/cli/src/analyst/larp-gauge.js +60 -0
  12. package/dist/packages/cli/src/analyst/session.js +12 -1
  13. package/dist/packages/cli/src/app-help-gen.js +7 -7
  14. package/dist/packages/cli/src/backtest-cmd.js +13 -2
  15. package/dist/packages/cli/src/display.js +55 -9
  16. package/dist/packages/cli/src/live/dev-history.js +31 -15
  17. package/dist/packages/cli/src/live/index.js +5 -1
  18. package/dist/packages/cli/src/live/screens/newpairs-live.js +1 -1
  19. package/dist/packages/cli/src/live/screens/token-live.js +41 -11
  20. package/dist/packages/cli/src/live/screens/watchlist-live.js +1 -1
  21. package/dist/packages/cli/src/live/trade.js +22 -0
  22. package/dist/packages/cli/src/main.js +146 -12
  23. package/dist/packages/cli/src/mcp-follow.js +103 -6
  24. package/dist/packages/cli/src/plan-cmd.js +15 -5
  25. package/dist/packages/cli/src/popup.js +17 -1
  26. package/dist/packages/cli/src/scan/ui.js +7 -1
  27. package/dist/packages/cli/src/screens/agent-hud.js +741 -0
  28. package/dist/packages/cli/src/screens/analyst-session.js +4 -0
  29. package/dist/packages/cli/src/screens/brain.js +608 -0
  30. package/dist/packages/cli/src/screens/newpairs.js +1 -1
  31. package/dist/packages/cli/src/screens/pnl-chip.js +143 -0
  32. package/dist/packages/cli/src/screens/screener.js +1 -0
  33. package/dist/packages/cli/src/screens/token.js +16 -7
  34. package/dist/packages/cli/src/screens/trending.js +1 -1
  35. package/dist/packages/cli/src/state.js +69 -5
  36. package/dist/packages/cli/src/version.js +1 -1
  37. package/dist/packages/custom/src/schema.js +8 -2
  38. package/dist/packages/data/src/index.js +2 -0
  39. package/dist/packages/data/src/larp/score.js +220 -0
  40. package/dist/packages/data/src/larp/x-account.js +316 -0
  41. package/dist/packages/data/src/trade/agent/paper-desk.js +77 -12
  42. package/dist/packages/data/src/trade/plans/engine.js +7 -0
  43. package/dist/packages/data/src/trade/plans/mandate.js +28 -15
  44. package/dist/packages/data/src/trade/plans/service.js +1 -1
  45. package/dist/packages/mcp/assets/kb/app-help/dev-history.md +1 -1
  46. package/dist/packages/mcp/assets/kb/app-help/larp-check.md +5 -1
  47. package/dist/packages/mcp/assets/kb/app-help/scan-mode.md +1 -1
  48. package/dist/packages/mcp/assets/kb/app-help/screen-token.md +3 -0
  49. package/dist/packages/mcp/assets/kb/app-help/spectate-an-agent.md +2 -1
  50. package/dist/packages/mcp/assets/kb/docs/docs-agents-agent-paper-trading.md +3 -2
  51. package/dist/packages/mcp/assets/kb/docs/docs-analyst-starting-a-run.md +2 -0
  52. package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-y-axis.md +32 -0
  53. package/dist/packages/mcp/assets/kb/docs/docs-configuration-price-display-mode.md +3 -3
  54. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
  55. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +1 -1
  56. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-running-the-tests.md +1 -1
  57. package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +1 -0
  58. package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +60 -0
  59. package/dist/packages/mcp/assets/kb/docs/docs-larp-the-verdict.md +0 -2
  60. package/dist/packages/mcp/assets/kb/docs/docs-larp-what-it-looks-at.md +4 -1
  61. package/dist/packages/mcp/assets/kb/docs/docs-larp.md +1 -1
  62. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-order-plans-and-standing-mandates.md +1 -9
  63. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-paper-only-mandates.md +41 -0
  64. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-market-cap-axis.md +1 -1
  65. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-what-it-shows.md +1 -0
  66. package/dist/packages/mcp/assets/kb/docs/docs-scan-the-read.md +2 -0
  67. package/dist/packages/mcp/assets/kb/docs/docs-screens-dev-history.md +10 -7
  68. package/dist/packages/mcp/assets/kb/docs/docs-spectator-follow-over-mcp-in-the-running-terminal.md +1 -1
  69. package/dist/packages/mcp/assets/kb/docs/docs-spectator-limits.md +2 -0
  70. package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-agent-hud-spectator-v2.md +58 -0
  71. package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-brain-view-and-the-pnl-chip.md +46 -0
  72. package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-three-surfaces.md +1 -1
  73. package/dist/packages/mcp/assets/kb/docs/docs-spectator.md +1 -1
  74. package/dist/packages/mcp/src/paper-agent.js +3 -2
  75. package/dist/packages/mcp/src/server.js +162 -21
  76. package/dist/packages/mcp/src/spectator-sink.js +85 -0
  77. package/dist/packages/packs/src/index.js +1 -0
  78. package/dist/packages/packs/src/larp-score-backtest.js +119 -0
  79. package/dist/packages/popup/src/pages/chart-lib.js +36 -15
  80. package/dist/packages/popup/src/pages/chart.css +9 -0
  81. package/dist/packages/popup/src/pages/chart.html +4 -1
  82. package/dist/packages/popup/src/pages/chart.js +34 -5
  83. package/dist/packages/popup/src/pages/spectator.css +45 -0
  84. package/dist/packages/popup/src/pages/spectator.html +1 -0
  85. package/dist/packages/popup/src/pages/spectator.js +69 -0
  86. package/dist/packages/popup/src/protocol.js +2 -0
  87. package/dist/packages/popup/src/server.js +5 -1
  88. package/dist/packages/recorder/src/dev-history.js +7 -3
  89. package/dist/packages/tui/src/canvas.js +7 -0
  90. package/dist/packages/tui/src/fmt.js +53 -0
  91. package/dist/packages/tui/src/keymap.js +4 -0
  92. package/dist/packages/tui/src/widgets.js +9 -4
  93. package/package.json +1 -1
@@ -14,11 +14,12 @@ import { renderScanWhy, renderScanTicker } from "../../scan/ui.js";
14
14
  import { drawCoin, imagesOn } from "../../screens/coin.js";
15
15
  import { COIN_HERO_W, COIN_HERO_H } from '../../../../tui/src/index.js';
16
16
  import { renderSocialPanel } from "../../screens/social-panel.js";
17
+ import { brainFor, factsFromView, renderBrainPanel } from "../../screens/brain.js";
17
18
  import { launchIntelCells, holderTags } from "../intel.js";
18
19
  import { devBookOf, devRows } from "../dev-history.js";
19
20
  import { EX_POOL_TAGS } from '../../../../data/src/index.js';
20
21
  import { layoutFor } from "../../custom.js";
21
- import { chartMode, priceModeOf, mcapAt, positionPriceCells } from "../../display.js";
22
+ import { chartMode, chartTitle, priceModeOf, chartAxisOf, mcapAt, positionPriceCells } from "../../display.js";
22
23
  const dash = '—';
23
24
  const EX_POOL_TAG_SET = EX_POOL_TAGS;
24
25
  /** task-1367 holder-class badge colours: venues/escrows accent, the creator/linked warn, the rest muted. */
@@ -87,13 +88,13 @@ export function renderTokenLive(c, state, v) {
87
88
  const shownPx = livePx ?? v.price;
88
89
  // task-1366: mcap mode leads with the market cap (the live price × the polled supply ratio) and keeps the price beside it;
89
90
  // without a supply it shows the price and says so.
90
- const cm = chartMode(priceModeOf(state), v.price, v.mcap);
91
- if (cm.kind === 'mcap' && shownPx !== null) {
92
- x = c.text(x0, y, fmtMcap(mcapAt(shownPx, cm.scale)), { fg: t.text, bg: t.bg, b: true });
91
+ const pm = chartMode(priceModeOf(state), v.price, v.mcap);
92
+ if (pm.kind === 'mcap' && shownPx !== null) {
93
+ x = c.text(x0, y, fmtMcap(mcapAt(shownPx, pm.scale)), { fg: t.text, bg: t.bg, b: true });
93
94
  x = c.text(x + 1, y, `(${fmtPrice(shownPx)})`, { fg: t.dim, bg: t.bg });
94
95
  }
95
96
  else
96
- x = c.text(x0, y, shownPx === null ? dash : fmtPrice(shownPx), { fg: cm.label && shownPx !== null ? t.warn : t.text, bg: t.bg, b: true });
97
+ x = c.text(x0, y, shownPx === null ? dash : fmtPrice(shownPx), { fg: pm.label && shownPx !== null ? t.warn : t.text, bg: t.bg, b: true });
97
98
  if (state.live?.fastLane)
98
99
  x = badge(c, x + 1, y, liveBadge, liveBadge === 'LIVE' ? t.up : t.muted, { on: t.bg });
99
100
  const deltas = [['5m', v.chg.m5], ['1h', v.chg.h1], ['6h', v.chg.h6], ['24h', v.chg.h24]];
@@ -131,7 +132,9 @@ export function renderTokenLive(c, state, v) {
131
132
  const chartH = Math.max(10, Math.floor((bottom - y - posH) * lay.chartFrac));
132
133
  const cp = v.panels.candles;
133
134
  const chartCap = clip(cp.state === 'ok' || cp.fetchedAt !== null ? `ohlcv: geckoterminal${cp.stale || cp.state === 'error' ? ' · stale' : ''}` : panelCaption(cp, now, 'ohlcv unavailable'), 40);
134
- const cb = c.box(0, y, mainW, chartH, { title: cm.label ? (cm.kind === 'mcap' ? `${v.symbol} ${cm.label} · ${v.interval}` : `${v.symbol}/USD · ${cm.label} · ${v.interval}`) : `${v.symbol}/USD · ${v.interval}`, caption: chartCap, captionFg: cp.state === 'error' && cp.fetchedAt === null ? t.warn : undefined });
135
+ // task-1472: the chart's Y axis is its own setting (default MARKET CAP, `A` toggles); the header above keeps `$`.
136
+ const cm = chartMode(chartAxisOf(state), v.price, v.mcap);
137
+ const cb = c.box(0, y, mainW, chartH, { title: chartTitle(v.symbol, v.interval, cm), caption: chartCap, captionFg: cp.state === 'error' && cp.fetchedAt === null ? t.warn : undefined });
135
138
  // timeframe tabs sit on the top border, left of the caption (same spot as the fixture screen)
136
139
  let tx = cb.x + cb.w - 2 - (chartCap.length + 4) - INTERVALS.reduce((s, f) => s + f.length + 1, 0);
137
140
  for (const f of INTERVALS)
@@ -142,9 +145,12 @@ export function renderTokenLive(c, state, v) {
142
145
  c.textC(cb.x, cb.y + Math.floor(cb.h / 2), cb.w, cp.state === 'loading' ? 'loading candles…' : `no candles · ${cp.reason ?? 'source did not answer'}`, { fg: t.muted, bg: cb.bg, i: true });
143
146
  y += chartH;
144
147
  if (prow && src) {
145
- positionPanel(c, 0, y, mainW, prow, held?.kind ?? src.kind, priceModeOf(state), cm.scale);
148
+ positionPanel(c, 0, y, mainW, prow, held?.kind ?? src.kind, priceModeOf(state), pm.scale);
146
149
  y += posH;
147
150
  }
151
+ // task-1473: what the ON-CHAIN band derived this frame, for the brain view's vetting list (null under SOCIAL)
152
+ let brainTop10 = null;
153
+ let brainDev = null;
148
154
  // ---- task-1330: the SOCIAL panel (`s`) replaces the ON-CHAIN / HOLDERS band; the chart + position stay.
149
155
  if (state.tokenSocial) {
150
156
  renderSocialPanel(c, 0, y, mainW, bottom - y, state, { mint: v.mint, symbol: v.symbol, now });
@@ -182,9 +188,11 @@ export function renderTokenLive(c, state, v) {
182
188
  // 0.1.5 dev history (task-1425): the creator's recorded track record from the local recorder (keyless). The rugs
183
189
  // row sits with the other rug signals; launches / bag-exit / linked wallets follow the safety list.
184
190
  const book = devBookOf(state);
185
- const devAll = book ? devRows(book.forMint(v.mint, v.devAddress ?? null, state.live?.intel?.analysis(v.mint) ?? null)) : [];
191
+ const devAll = book ? devRows(book.forMint(v.mint, v.devAddress ?? null, state.live?.intel?.analysis(v.mint) ?? null, state.live?.intel?.entry(v.mint)?.at ?? null), now) : [];
186
192
  const devHead = devAll.slice(0, 1);
187
193
  const devTail = devAll.slice(1);
194
+ brainTop10 = tags.top10 ? tags.top10.exPool : v.top10;
195
+ brainDev = (devHead[0] ? [`${devHead[0][0]}: ${devHead[0][1]}`, devHead[0][2]] : v.devMints === null ? null : [`${v.devMints} mints by dev (jup)`, v.devMints > 3 ? 'warn' : 'ok']);
188
196
  const sniperRow = [cells.snipers.text, cells.snipers.level];
189
197
  const bundleRow = [cells.bundle.text, cells.bundle.level];
190
198
  const rows = [
@@ -201,7 +209,7 @@ export function renderTokenLive(c, state, v) {
201
209
  ['GoPlus', ...goplusRow(v)],
202
210
  ...devTail,
203
211
  ];
204
- for (const [label, val0, lvl] of rows) {
212
+ for (const [label, val0, lvl] of fitRows(rows, sb.y + sb.h - sy)) {
205
213
  if (sy >= sb.y + sb.h)
206
214
  break;
207
215
  const color = lvl === 'ok' ? t.up : lvl === 'warn' ? t.warn : lvl === 'bad' ? t.down : t.dim;
@@ -327,8 +335,15 @@ export function renderTokenLive(c, state, v) {
327
335
  }
328
336
  }
329
337
  }
330
- if (showAi)
331
- analystPlaceholder(c, mainW, top, aiW, bottom - top, v, state);
338
+ if (showAi) {
339
+ // task-1473: in an auto mode (following an agent / AI scan) the panel is the BRAIN VIEW; otherwise the analyst
340
+ const facts = factsFromView(v, { top10: brainTop10, dev: brainDev, larpVerdict: state.larpByMint?.get(v.mint)?.verdict ?? null, price: shownPx });
341
+ const brain = brainFor(state, facts, isCalm(state.vfx), state.live?.scanMeterLabel?.() ?? '');
342
+ if (brain)
343
+ renderBrainPanel(c, mainW, top, aiW, bottom - top, brain.model, brain.opts);
344
+ else
345
+ analystPlaceholder(c, mainW, top, aiW, bottom - top, v, state);
346
+ }
332
347
  const watched = state.live?.watch?.has(v.mint) ? 'unwatch' : 'watch';
333
348
  // task-1311: `i` asks claude about this token (chat), `a` runs the paid dossier on demand, `y`
334
349
  // copies the full contract address.
@@ -402,6 +417,21 @@ function positionPanel(c, x, y, w, r, kind, mode = 'price', supply = null) {
402
417
  function clip(s, max) {
403
418
  return s.length > max ? s.slice(0, Math.max(1, max - 1)) + '…' : s;
404
419
  }
420
+ /**
421
+ * task-1463: the ON-CHAIN rows for a panel `space` lines tall. When they all fit they are returned unchanged. When
422
+ * they do not, a short panel must not drop a red flag silently (e.g. `Dev rugs 2/3`, the last rows of the list):
423
+ * bad rows are kept first, then warn rows, then the rest in order, the kept rows stay in their order, and the last
424
+ * line says how many rows are hidden.
425
+ */
426
+ export function fitRows(rows, space) {
427
+ if (rows.length <= space || space <= 0)
428
+ return rows;
429
+ const slots = space - 1;
430
+ const rank = (l) => (l === 'bad' ? 0 : l === 'warn' ? 1 : 2);
431
+ const keep = new Set(rows.map((r, i) => [rank(r[2]), i]).sort((a, b) => a[0] - b[0] || a[1] - b[1]).slice(0, slots).map(([, i]) => i));
432
+ const hidden = rows.length - keep.size;
433
+ return [...rows.filter((_, i) => keep.has(i)), [`+${hidden} rows hidden`, 'taller window', 'na']];
434
+ }
405
435
  /** Token amounts up to trillions (fmtNum stops at B). */
406
436
  function fmtAmt(n) {
407
437
  if (n >= 1e12)
@@ -89,7 +89,7 @@ export function renderWatchlistLive(c, state, live) {
89
89
  } });
90
90
  }
91
91
  if (rows.length)
92
- table(c, box.x + 1, box.y, box.w - 2, box.h, { columns: promoteMcap(cols, mode), rows, selected: sel, bg: box.bg });
92
+ table(c, box.x + 1, box.y, box.w - 2, box.h, { columns: promoteMcap(cols, mode), rows, selected: sel, bg: box.bg, rowKey: (r) => r.entry.mint });
93
93
  else
94
94
  c.textC(box.x, box.y + Math.min(3, Math.floor(box.h / 2)), box.w, 'watchlist is empty · press w on any token (screener, new pairs, token screen) to add it', { fg: t.muted, bg: box.bg, i: true });
95
95
  const y2 = top + listH;
@@ -669,6 +669,8 @@ export function positionRow(h, tickUsd, solUsd) {
669
669
  }
670
670
  /** How long a `held()` lookup is reused before re-reading the ledger (the render loop calls it per frame). */
671
671
  export const HELD_MEMO_MS = 250;
672
+ /** task-1473 r2: how long the open-mints list is reused (a ledger read; the chip samples it every tick). */
673
+ export const OPEN_MINTS_MEMO_MS = 1000;
672
674
  /** Minimum spacing of the token screen's automatic mark pass for a held coin missing SOL/USD or a mark. */
673
675
  export const HELD_AUTOMARK_MS = 30_000;
674
676
  /** Live PAPER positions + PnL: marks held tokens from the keyless price feed. */
@@ -738,6 +740,22 @@ export class PositionsLive {
738
740
  }
739
741
  return v;
740
742
  }
743
+ /** task-1473 r2: mints of the open paper positions (the AI-scan PnL chip), memoised OPEN_MINTS_MEMO_MS. */
744
+ openMints() {
745
+ const now = this.now();
746
+ if (this.openMemo && now - this.openMemo.at >= 0 && now - this.openMemo.at < OPEN_MINTS_MEMO_MS)
747
+ return this.openMemo.v;
748
+ let v = [];
749
+ try {
750
+ v = this.wallet.positions().filter((p) => p.tokensRaw > 0n).map((p) => p.mint);
751
+ }
752
+ catch {
753
+ v = [];
754
+ }
755
+ this.openMemo = { at: now, v };
756
+ return v;
757
+ }
758
+ openMemo = null;
741
759
  /**
742
760
  * Mark held tokens in SOL. Prices come from DexScreener (its own fast 300 rpm bucket), not the
743
761
  * keyless `jupiter` bucket the screener feed and quotes share — so a mark never queues behind them.
@@ -917,6 +935,8 @@ export class LivePositions {
917
935
  this.memo.set(mint, { at: now, v });
918
936
  return v;
919
937
  }
938
+ /** task-1473 r2: mints of the open live positions (the AI-scan PnL chip). Memoised with positions(). */
939
+ openMints() { return this.positions().filter((p) => p.tokensRaw > 0n).map((p) => p.mint); }
920
940
  /** Forget memoised reads (after a fill). */
921
941
  invalidate() { this.memo.clear(); this.rowsMemo = null; }
922
942
  }
@@ -936,6 +956,8 @@ export class CombinedPositions {
936
956
  return p;
937
957
  }
938
958
  solUsd() { return this.paper.solUsd() ?? this.live?.solUsd() ?? null; }
959
+ /** task-1473 r2: every open mint, live first (the AI-scan PnL chip picks one). */
960
+ openMints() { return [...new Set([...(this.live?.openMints() ?? []), ...this.paper.openMints()])]; }
939
961
  }
940
962
  function msg(e) {
941
963
  return e instanceof Error ? e.message : String(e);
@@ -25,7 +25,7 @@ import { parseMintTarget, parseCoinTarget, resolveCoinTarget, isBareOpen } from
25
25
  import { THEMES, isThemeName, DEFAULT_THEME, THEME_NAMES, Flusher, ENTER_SCREEN, LEAVE_SCREEN, DEFAULT_KEYMAP, Keymap, nextTheme, parseKeys, keyId, detectColorDepth, parseColorDepth, blinkFrame, blinkFrames, stepVfx, pulse, isCalm, fxBusy, blendActivity, ImageLayer, queryTerminal, resolveProtocol, fireMoment, fireInflight, clearInflight, skipMoment, momentKindFor, playMoment, soundOn, breakpoint, fmtUsd as fmtUsdTui, } from '../../tui/src/index.js';
26
26
  import { resolveRpcUrl, redactSecrets, defaultDbPath, extraRpcUrls, wsUrlFor, SOLANA_PUBLIC_RPC_WS, parseHttpUrl, validateAnalystAction, ACTION_RATE, ACTION_TIER, configChangeForAction, ImageService, encodePng, SummaryLimiter, buildSummaryWindow, KolStore, Cache, PackError, normalizeHandle, terminalSafe } from '../../data/src/index.js';
27
27
  import { Recorder, recorderEnabled, MaintenanceSchedule } from '../../recorder/src/index.js';
28
- import { makeFixtureState, makeFeedState, syncFeed, renderFrame, isScreenKey, activityFor, SCREEN_NAMES, analystKey, syncAnalyst, pinNewpairsSelection, ANALYST_TAB_KEY, computeNarrativeView, newPlansUi, plansService, plansRows, newFollowsUi, copyService, copyAutoService, MIRROR_DEFAULT_CAP_USD } from "./state.js";
28
+ import { makeFixtureState, makeFeedState, syncFeed, renderFrame, isScreenKey, activityFor, SCREEN_NAMES, analystKey, syncAnalyst, pinNewpairsSelection, ANALYST_TAB_KEY, computeNarrativeView, newPlansUi, plansService, plansRows, newFollowsUi, copyService, copyAutoService, MIRROR_DEFAULT_CAP_USD, pnlChipFor } from "./state.js";
29
29
  import { mulberry32, tick, spawnPair, shock } from "./fixtures.js";
30
30
  import { createLiveFeed, createReplayFeed } from "./feed.js";
31
31
  import { LiveContext, INTERVALS, WATCH_REFRESH_MS, POSITIONS_REFRESH_MS, popupBubbleVM } from "./live/index.js";
@@ -39,7 +39,9 @@ import { parseFilter } from "./screener/filter.js";
39
39
  import { screenerView, setFilter, setSort, setPreset, stepPreset, clampSelection, openFilterEditor, openSortMenu, filterEditorKey, sortMenuKey, } from "./screener/view.js";
40
40
  import { readBootState, bootPlan, recordBoot, bootTrace, dextermHome } from "./boot-state.js";
41
41
  import { socialOf, kolOf, trendingRows, kolRows, nextSocialFilter, feedsPopupModel } from "./social/view.js";
42
- import { TokenPopups, tokenDetailVMFromView } from "./popup.js";
42
+ import { TokenPopups, tokenDetailVMFromView, currentPositionFills } from "./popup.js";
43
+ import { OwnPnlTracker } from "./screens/pnl-chip.js";
44
+ import { positionRow } from "./live/trade.js";
43
45
  import { widgetPage, widgetEnabled, syncWidgetPopups } from "./widgets.js";
44
46
  import { recordLarpVerdict } from "./larp-record.js";
45
47
  import { rulesUiOf, rulesTick, rulesMove, beginRuleInput, saveRuleInput, dryRunNow, removeSelectedRule, toggleSelectedRule, dismissSelectedProposal, actOnSelectedProposal, RULE_INPUT_MAX } from "./rules.js";
@@ -53,7 +55,7 @@ import { resolveAnalystResults } from "./analyst/results.js";
53
55
  import { parseScanCommand, SCAN_FLAGS, checkScanFlag, SCAN_HELP } from "./scan/command.js";
54
56
  import { parseWalletCommand, WALLET_HELP } from "./wallet-command.js";
55
57
  import { X_HELP } from "./x-cmd.js";
56
- import { makeScanUi, MODE_LABEL, SCAN_ACTION_TYPES, SCAN_TICK_MS, SCAN_RECOMPUTE_MS } from "./scan/ui.js";
58
+ import { makeScanUi, scanLog, MODE_LABEL, SCAN_ACTION_TYPES, SCAN_TICK_MS, SCAN_RECOMPUTE_MS } from "./scan/ui.js";
57
59
  import { buildObservations, observeSig } from "./scan/observe.js";
58
60
  import { cleanText } from "./screener/rows.js";
59
61
  import { resolveImagesConfig } from "./images-config.js";
@@ -62,7 +64,8 @@ import { VoiceController, selectStt, selectTts, SayTts, voiceStyleDefaults } fro
62
64
  import { LivePlanExecutor, defaultPlansJournalPath } from '../../data/src/trade/plans/index.js';
63
65
  import { isWalletAddress, shortAddr, buildMirror, leaderSoldSince, autoSizeCap } from '../../data/src/trade/copy/index.js';
64
66
  import { FOLLOW_INPUT_MAX } from "./screens/follows.js";
65
- import { isPriceMode, configPriceMode, togglePriceMode, priceModeOf, planLevels } from "./display.js";
67
+ import { HUD_MIN_H, hudAnimating } from "./screens/agent-hud.js";
68
+ import { isPriceMode, configPriceMode, togglePriceMode, priceModeOf, planLevels, configChartAxis, setChartAxis, chartAxisOf } from "./display.js";
66
69
  import { VERSION } from "./version.js";
67
70
  import { initCustom, planWorkspace, planPopupOpens, findWorkspace, nextWorkspaceName, workspaceSlots, saveWorkspace, themeFor, themeByName, cycleTheme, themeNames, CHORD_TTL_MS, MAX_WORKSPACE_NAME } from "./custom.js";
68
71
  import { DEFAULT_WORKSPACE, MORNING_HUD, applyAndJournal, policyCheck } from '../../custom/src/index.js';
@@ -124,19 +127,35 @@ export function buildScreenState(state) {
124
127
  const sc = state.screener;
125
128
  // The screener and the Analyst-tab coin list both show the filtered/sorted screener view, so the
126
129
  // selection + visible rows must come from that same view (task-1313); other screens use the raw pool.
127
- const rows = state.screen === 'screener' || state.screen === 'analyst' ? screenerView(sc) : sc.rows;
130
+ // The new-pairs screen shows the live PumpPortal list (task-1443 capture r2: it used to report the screener pool
131
+ // here, so an agent watching new pairs could not read what was on screen or what was highlighted).
132
+ const newPairs = state.screen === 'newpairs' && live ? live.newpairs.list() : null;
133
+ const rows = state.screen === 'screener' || state.screen === 'analyst' ? screenerView(sc) : newPairs ?? sc.rows;
128
134
  const toRow = (r) => {
129
135
  const x = r;
130
136
  if (!x?.mint)
131
137
  return null;
132
138
  return { mint: x.mint, symbol: x.symbol ?? '', priceUsd: x.price ?? null, chg1h: x.chg?.h1 ?? null, chg24h: x.chg?.h24 ?? null, liq: x.liq ?? null, vol24: x.vol24 ?? null };
133
139
  };
134
- const visible = rows.slice(0, 12).map(toRow).filter((r) => r !== null);
140
+ const toNewPairRow = (r) => {
141
+ const x = r;
142
+ if (!x?.mint)
143
+ return null;
144
+ return {
145
+ mint: x.mint, symbol: x.symbol ?? '', priceUsd: x.price ?? null, chg1h: x.chg?.h1 ?? null, chg24h: null, liq: x.liq ?? null, vol24: x.vol ?? null,
146
+ ageSec: Number.isFinite(x.createdAt) ? Math.max(0, Math.round((Date.now() - x.createdAt) / 1000)) : null,
147
+ mcap: x.mcap ?? null, chg5m: x.chg?.m5 ?? null, bonding: x.bonding ?? null, buys: x.buys ?? null, sells: x.sells ?? null,
148
+ };
149
+ };
150
+ const mapRow = newPairs ? toNewPairRow : toRow;
151
+ const visible = rows.slice(0, 12).map(mapRow).filter((r) => r !== null);
135
152
  let selected = null;
136
153
  if (state.screen === 'token' && live?.token?.view)
137
154
  selected = { mint: live.token.view.mint, symbol: live.token.view.symbol };
138
155
  else {
139
- const r = toRow(rows[state.selected || 0]);
156
+ // Same pick as the new-pairs screen's own highlight (selectedMint first, then the cursor row).
157
+ const hit = newPairs && state.selectedMint ? newPairs.find((x) => x.mint === state.selectedMint) : undefined;
158
+ const r = mapRow(hit ?? rows[state.selected || 0]);
140
159
  if (r)
141
160
  selected = { mint: r.mint, symbol: r.symbol };
142
161
  }
@@ -267,7 +286,7 @@ export function shouldDrawFrame(g) {
267
286
  return true;
268
287
  if (g.calm)
269
288
  return g.onTick;
270
- if (g.fxBusy)
289
+ if (g.fxBusy || g.hud)
271
290
  return true;
272
291
  return g.frame % g.idleEvery === 0;
273
292
  }
@@ -306,6 +325,8 @@ export const HELP = `dexterm ${VERSION} (live: screener, token, new pairs, watch
306
325
  --record FILE save every live response (and the PumpPortal session) to FILE (replay it with --replay)
307
326
  --spectate-mcp follow an MCP agent's allowed navigation in this terminal; Ctrl+P pauses, Ctrl+K stops,
308
327
  and any normal user keypress takes control (refused actions never move the UI)
328
+ --spectate-thoughts FILE (with --spectate-mcp) stream the agent's own words into the thought ribbon from a JSONL
329
+ file it writes (e.g. claude -p --output-format stream-json); display only, scrubbed
309
330
  --agent-paper MANDATE_ID (experimental, with --spectate-mcp) let that MCP agent PAPER-trade inside a mandate you
310
331
  armed: simulated fills only, never signs or sends, never raises a cap; Ctrl+P / Ctrl+K /
311
332
  any key also pause, stop or take over its paper trading
@@ -419,7 +440,7 @@ export function parseArgs(argv) {
419
440
  sound: false,
420
441
  trade: [], live: true,
421
442
  workspace: null, priceMode: null,
422
- spectateMcp: false, agentPaper: null,
443
+ spectateMcp: false, spectateThoughts: null, agentPaper: null,
423
444
  help: false, version: false, unknown: [],
424
445
  };
425
446
  for (let i = 0; i < argv.length; i++) {
@@ -611,6 +632,8 @@ export function parseArgs(argv) {
611
632
  // task-1421: follow an agent driving over MCP (the running terminal moves with it; a read-only stream opens).
612
633
  else if (k === '--spectate-mcp')
613
634
  a.spectateMcp = true;
635
+ else if (k === '--spectate-thoughts')
636
+ a.spectateThoughts = str();
614
637
  // task-1443: agent PAPER trading inside a mandate the user armed (validated against the plans journal at start).
615
638
  else if (k === '--agent-paper') {
616
639
  const s = str();
@@ -655,6 +678,8 @@ export function parseArgs(argv) {
655
678
  throw new Error('--scan needs live data or a recording (drop --fixtures)');
656
679
  if (a.scan && a.screen)
657
680
  throw new Error('--scan is interactive (drop --screen)');
681
+ if (a.spectateThoughts && !a.spectateMcp)
682
+ throw new Error('--spectate-thoughts needs --spectate-mcp (the thought ribbon is part of the follow view)');
658
683
  if (a.agentPaper && !a.spectateMcp)
659
684
  throw new Error('--agent-paper needs --spectate-mcp (every agent paper trade must be visible in this terminal)');
660
685
  if (a.agentPaper && a.db === ':memory:')
@@ -1127,6 +1152,7 @@ export async function main(argv, io = process, proc = process) {
1127
1152
  for (const e of state.custom.themeErrors)
1128
1153
  io.stderr.write(`dexterm: ${e}\n`);
1129
1154
  state.priceMode = args.priceMode ?? configPriceMode(state.custom.config); // task-1366: --price-mode wins for the run
1155
+ state.chartAxis = configChartAxis(state.custom.config); // task-1472: default MARKET CAP, `A` toggles + saves
1130
1156
  if (args.workspace) {
1131
1157
  const r = switchWorkspace(state, args.workspace, []);
1132
1158
  if (!r.ok) {
@@ -2471,6 +2497,9 @@ export function applyAction(state, action, stack) {
2471
2497
  case 'display.priceMode':
2472
2498
  state.pendingToast = togglePriceMode(state);
2473
2499
  break; // task-1366: display only, persisted
2500
+ case 'display.chartAxis':
2501
+ state.pendingToast = setChartAxis(state);
2502
+ break; // task-1472: both charts' Y axis, display only, persisted
2474
2503
  case 'theme.next':
2475
2504
  state.theme = state.custom ? cycleTheme(state.custom, state.theme) : nextTheme(state.theme);
2476
2505
  break; // task-1358: user themes join the cycle
@@ -2634,6 +2663,7 @@ export function scanCommand(state, cmd, stack, now = Date.now()) {
2634
2663
  if (!e.start(now))
2635
2664
  return `AI scan is already on (${MODE_LABEL[e.mode]})`;
2636
2665
  live.scanMeter = scanMeterOf(e, now);
2666
+ scanLog(scan, now, 'start', '', `scan on · observe-only · locks at score ≥ ${e.cfg.minScore} · liquidity ≥ $${Math.round(e.cfg.minLiqUsd / 1000)}K`);
2637
2667
  return `AI scan ON · observe-only · warming up ${e.cfg.warmupS}s · locks at score ≥ ${e.cfg.minScore} · ai ≤ ${e.cfg.maxAiPerHour}/h · Z stop`;
2638
2668
  case 'off': {
2639
2669
  if (!e.on)
@@ -2641,6 +2671,7 @@ export function scanCommand(state, cmd, stack, now = Date.now()) {
2641
2671
  stopTurn();
2642
2672
  const was = e.lock?.symbol;
2643
2673
  e.stop(now);
2674
+ scanLog(scan, now, 'stop', was ?? '', was ? 'scan stopped · released' : 'scan stopped');
2644
2675
  live.scanMeter = null;
2645
2676
  scan.returnTo = null;
2646
2677
  return `AI scan stopped${was ? ` (released ${sym(was)})` : ''}`;
@@ -2653,6 +2684,7 @@ export function scanCommand(state, cmd, stack, now = Date.now()) {
2653
2684
  const locked = e.lock;
2654
2685
  stopTurn();
2655
2686
  e.resume(now);
2687
+ scanLog(scan, now, 'release', locked?.symbol ?? '', `released · cooling down ${Math.round(e.cfg.coinCooldownS / 60)}m · watching for the next coin`);
2656
2688
  live.scanMeter = scanMeterOf(e, now);
2657
2689
  // back from the coin the lock pulled up to where the user was, so the ticker is in view
2658
2690
  if (locked && state.screen === 'token' && live.token?.view.mint === locked.mint)
@@ -2995,6 +3027,61 @@ export function applyAnalystAction(state, raw, stack) {
2995
3027
  times.push(now);
2996
3028
  return { ok: true, label: res.label };
2997
3029
  }
3030
+ /**
3031
+ * task-1466: the price this terminal itself shows for a coin, to mark the followed agent's open PAPER position (the
3032
+ * HUD's live PnL): the open token's push price (LIVE) or polled price, else the screener row's price. null if the
3033
+ * terminal has no price for it. Never the agent's own number.
3034
+ */
3035
+ export function markPriceFor(state, mint) {
3036
+ const live = state.live;
3037
+ if (live?.token?.view.mint === mint) {
3038
+ const px = (live.badgeFor(mint) === 'LIVE' ? live.livePrice(mint) : null) ?? live.token.view.price;
3039
+ if (typeof px === 'number' && Number.isFinite(px) && px > 0)
3040
+ return px;
3041
+ }
3042
+ const row = state.screener.rows.find((r) => r.mint === mint);
3043
+ if (row && typeof row.price === 'number' && Number.isFinite(row.price) && row.price > 0)
3044
+ return row.price;
3045
+ // task-1473: the held coin scrolled out of the list (run25): its fast-lane push price (the held mint is pinned there)
3046
+ const fast = live?.livePrice(mint) ?? null;
3047
+ return typeof fast === 'number' && Number.isFinite(fast) && fast > 0 ? fast : null;
3048
+ }
3049
+ let ownOpenedMemo = null;
3050
+ /**
3051
+ * task-1473 r2: the user's OWN held position for the AI-scan PnL chip, marked with this terminal's own price (same
3052
+ * task-1323 position row the token screen uses): the scan's locked coin when held, else the open token, else the
3053
+ * first open mint (live first). null when flat. Reads the ledgers only (memoised); never trades, sizes or signs.
3054
+ */
3055
+ export function ownHeldSample(state, now) {
3056
+ const live = state.live;
3057
+ if (!live)
3058
+ return null;
3059
+ const src = live.positionSource;
3060
+ const open = src.openMints();
3061
+ if (open.length === 0)
3062
+ return null;
3063
+ const prefer = [state.scan?.engine.lock?.mint, live.token?.view.mint].filter((m) => !!m);
3064
+ const mint = prefer.find((m) => open.includes(m)) ?? open[0];
3065
+ const held = src.held(mint);
3066
+ if (!held)
3067
+ return null;
3068
+ const r = positionRow(held, markPriceFor(state, mint), state.sol?.price ?? src.solUsd());
3069
+ if (!ownOpenedMemo || ownOpenedMemo.mint !== mint || now - ownOpenedMemo.at > 5000) {
3070
+ let t = null;
3071
+ try {
3072
+ t = currentPositionFills([...(live.livePositions?.records(200, mint) ?? []), ...live.wallet.trades(500).filter((x) => x.mint === mint)])[0]?.t ?? null;
3073
+ }
3074
+ catch {
3075
+ t = null;
3076
+ }
3077
+ ownOpenedMemo = { mint, at: now, t };
3078
+ }
3079
+ return {
3080
+ mint, symbol: held.symbol, kind: held.kind === 'live' ? 'live' : 'paper',
3081
+ entryUsd: r.avgEntryUsd, markUsd: r.priceUsd, pnlPct: r.unrealizedPct, pnlUsd: r.unrealizedUsd,
3082
+ openedAt: ownOpenedMemo.t, targetUsd: r.targetUsd,
3083
+ };
3084
+ }
2998
3085
  /**
2999
3086
  * task-1421: move the REAL terminal to mirror an agent's navigation (follow mode, on the owner's own
3000
3087
  * machine). This is the same VIEW-ONLY navigation the analyst / go-to path uses — open a token, switch
@@ -3912,6 +3999,7 @@ export function interactive(state, io, depth, args, proc = process) {
3912
3999
  // task-1421: the MCP follow session (set below when `--spectate-mcp` is on). Declared here so the quit /
3913
4000
  // terminal-guard teardown closures can stop it.
3914
4001
  let followSession = null;
4002
+ let lastHudMark = null; // task-1466: the last terminal price that marked the agent's PAPER position
3915
4003
  // task-1443: `--agent-paper <mandate-id>` (with --spectate-mcp) lets that agent PAPER-trade inside the mandate.
3916
4004
  const agentPaper = args.spectateMcp && args.agentPaper ? new AgentPaperSwitch({ env: io.env, mandateId: args.agentPaper, db: args.db ?? defaultDbPath(io.env) }) : null;
3917
4005
  // Idle economy (task-1303, retuned for task-1319 AC3): between events the ambient rain redraws at
@@ -4142,13 +4230,20 @@ export function interactive(state, io, depth, args, proc = process) {
4142
4230
  }
4143
4231
  scheduleDraw();
4144
4232
  },
4145
- onStatus: (line) => { say(line, 'ai', 4); scheduleDraw(); },
4233
+ // task-1466: the HUD dock says what the agent did; the toast is only the fallback for a terminal too short for it.
4234
+ onStatus: (line) => { if (size()[1] < HUD_MIN_H)
4235
+ say(line, 'ai', 4); scheduleDraw(); },
4146
4236
  onEvent: () => scheduleDraw(),
4147
4237
  onControl: agentPaper ? (st) => agentPaper.control(st) : undefined,
4148
4238
  snapshot: () => followSnapshot(state),
4239
+ thoughtsFile: args.spectateThoughts ?? undefined,
4149
4240
  });
4241
+ // task-1466 spectator v2: the agent HUD (phase rail, heartbeat, thought ribbon, call timeline, the PAPER
4242
+ // execution moment, pause / kill) draws over this terminal while it follows. Calm mode = reduced motion.
4243
+ state.agentHud = { story: followSession.story, clock: () => Date.now(), dataLabel: args.replay ? 'REPLAY DATA' : null };
4150
4244
  void followSession.start();
4151
- say('following agents over MCP · ctrl+p pause · ctrl+k stop · any key takes over', 'ai', 6);
4245
+ if (size()[1] < HUD_MIN_H)
4246
+ say('following agents over MCP · ctrl+p pause · ctrl+k stop · any key takes over', 'ai', 6);
4152
4247
  if (agentPaper)
4153
4248
  say(agentPaper.start(), 'ai', 8);
4154
4249
  }
@@ -4256,6 +4351,7 @@ export function interactive(state, io, depth, args, proc = process) {
4256
4351
  scan.blocked.push(type);
4257
4352
  if (scan.blocked.length > 6)
4258
4353
  scan.blocked.shift();
4354
+ scanLog(scan, Date.now(), 'blocked', scan.engine.lock?.symbol ?? '', `blocked analyst action "${type}" · observe-only`);
4259
4355
  }
4260
4356
  scanSay(`AI scan blocked analyst action "${type}" · observe-only (${error})`, 'warn', 5);
4261
4357
  scheduleDraw();
@@ -4287,6 +4383,7 @@ export function interactive(state, io, depth, args, proc = process) {
4287
4383
  state.aiFocused = false;
4288
4384
  state.aiScroll = 0;
4289
4385
  state.aiRevealAll = false;
4386
+ scanLog(scan, Date.now(), 'lock', d.symbol, `locked · score ${d.score} · ${d.hits.map((h) => h.reason).join(' · ')}`);
4290
4387
  startScanSession(d.mint, d.symbol, d.question, `AI scan · score ${d.score} · ${d.hits.map((h) => h.id).join(' + ')} → short read`);
4291
4388
  syncAnalyst(state);
4292
4389
  live.syncFastSet();
@@ -4304,6 +4401,7 @@ export function interactive(state, io, depth, args, proc = process) {
4304
4401
  const sess = scan.session;
4305
4402
  if (!sess)
4306
4403
  return;
4404
+ scanLog(scan, Date.now(), 'refresh', d.symbol, `moved: ${d.moved.join(', ')} · refreshing the read`);
4307
4405
  sess.ask(d.question, scanScreenState(state, d.mint, d.symbol), `AI scan refresh · ${d.moved.join(' · ')}`);
4308
4406
  if (state.analyst === sess) {
4309
4407
  state.aiScroll = 0;
@@ -4342,7 +4440,8 @@ export function interactive(state, io, depth, args, proc = process) {
4342
4440
  return;
4343
4441
  if (scan.session?.busy)
4344
4442
  scan.session.interrupt();
4345
- scan.engine.cancelAnalysis(Date.now());
4443
+ if (scan.engine.cancelAnalysis(Date.now()))
4444
+ scanLog(scan, Date.now(), 'cancel', scan.engine.lock?.symbol ?? '', 'analysis cancelled by you · coin held, nothing else locks');
4346
4445
  if (live)
4347
4446
  live.scanMeter = scanMeterOf(scan.engine, Date.now());
4348
4447
  scanSay(`analysis cancelled · scan holds $${cleanText(scan.engine.lock?.symbol ?? '', 12)} · R resume scan · Z stop`, 'ai', 5);
@@ -4428,6 +4527,10 @@ export function interactive(state, io, depth, args, proc = process) {
4428
4527
  solUsd: () => state.sol?.price ?? live.positions.solUsd(),
4429
4528
  imagePath: (v) => popupImagePath(state, v.image),
4430
4529
  priceMode: () => priceModeOf(state), // task-1366
4530
+ chartAxis: () => chartAxisOf(state), // task-1472: the popup's Y axis
4531
+ setChartAxis: (a) => { say(setChartAxis(state, a), 'info', 3); if (!done)
4532
+ draw(); }, // task-1472: the popup's MCAP | PRICE control
4533
+ pnlChip: () => pnlChipFor(state, state.agentHud?.clock() ?? Date.now()), // task-1473 r2: the chip on the chart popup
4431
4534
  } : null,
4432
4535
  });
4433
4536
  }
@@ -6199,6 +6302,36 @@ export function interactive(state, io, depth, args, proc = process) {
6199
6302
  }
6200
6303
  // market → rain: EMA on activity every tick, mood from the data every 4 s unless an event holds it
6201
6304
  blendActivity(v, activityFor(state), v.frame % (tickEvery * 8) === 0 && v.frame >= moodHoldUntil);
6305
+ // task-1466: the agent's open PAPER position is marked with this terminal's own live price (the HUD's PnL).
6306
+ // Only a NEW price marks (a 10 s-old view price must not overwrite a fresher one a tool result carried).
6307
+ if (followSession && state.live && state.live.heldMint !== followSession.story.heldMint) {
6308
+ state.live.heldMint = followSession.story.heldMint;
6309
+ state.live.syncFastSet();
6310
+ } // task-1473
6311
+ if (followSession) {
6312
+ const held = followSession.story.heldMint;
6313
+ const px = held ? markPriceFor(state, held) : null;
6314
+ if (held && px !== null && px !== lastHudMark) {
6315
+ followSession.story.mark(held, px, Date.now());
6316
+ lastHudMark = px;
6317
+ }
6318
+ }
6319
+ // task-1473 r2: AI scan mode + the user's OWN held position → the PnL chip (band row, dock, chart popup)
6320
+ if (state.scan?.engine.on && state.live) {
6321
+ state.ownPnl ??= new OwnPnlTracker();
6322
+ state.ownPnl.update(ownHeldSample(state, Date.now()), Date.now());
6323
+ if (!followSession && state.live.heldMint !== state.ownPnl.mint) {
6324
+ state.live.heldMint = state.ownPnl.mint;
6325
+ state.live.syncFastSet();
6326
+ } // run25: pin it
6327
+ }
6328
+ else if (state.ownPnl?.mint) {
6329
+ state.ownPnl.update(null, 0);
6330
+ if (!followSession && state.live?.heldMint) {
6331
+ state.live.heldMint = null;
6332
+ state.live.syncFastSet();
6333
+ }
6334
+ }
6202
6335
  }
6203
6336
  if (state.ai && state.ai.phase === 'thinking') {
6204
6337
  state.ai.ticks += 1;
@@ -6228,6 +6361,7 @@ export function interactive(state, io, depth, args, proc = process) {
6228
6361
  aiLive: !!(state.ai && state.ai.phase !== 'done'),
6229
6362
  awake: v.frame < wakeUntil,
6230
6363
  fxBusy: fxBusy(v),
6364
+ hud: followSession !== null && hudAnimating(followSession.story.view(Date.now()), Date.now()),
6231
6365
  frame: v.frame,
6232
6366
  idleEvery,
6233
6367
  }))