dexterm-cli 0.1.4 → 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 (131) 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-paper.js +60 -0
  10. package/dist/packages/cli/src/agent-spectator.js +17 -1
  11. package/dist/packages/cli/src/analyst/backend.js +206 -0
  12. package/dist/packages/cli/src/analyst/codex.js +301 -0
  13. package/dist/packages/cli/src/analyst/larp-gauge.js +60 -0
  14. package/dist/packages/cli/src/analyst/session.js +21 -3
  15. package/dist/packages/cli/src/app-help-gen.js +21 -12
  16. package/dist/packages/cli/src/backtest-cmd.js +13 -2
  17. package/dist/packages/cli/src/display.js +55 -9
  18. package/dist/packages/cli/src/doctor.js +58 -2
  19. package/dist/packages/cli/src/live/dev-history.js +115 -0
  20. package/dist/packages/cli/src/live/index.js +5 -1
  21. package/dist/packages/cli/src/live/newpairs.js +12 -0
  22. package/dist/packages/cli/src/live/screens/newpairs-live.js +1 -1
  23. package/dist/packages/cli/src/live/screens/token-live.js +51 -11
  24. package/dist/packages/cli/src/live/screens/watchlist-live.js +1 -1
  25. package/dist/packages/cli/src/live/token.js +1 -0
  26. package/dist/packages/cli/src/live/trade.js +22 -0
  27. package/dist/packages/cli/src/main.js +267 -19
  28. package/dist/packages/cli/src/mcp-follow.js +137 -10
  29. package/dist/packages/cli/src/plan-cmd.js +15 -5
  30. package/dist/packages/cli/src/popup.js +17 -1
  31. package/dist/packages/cli/src/record-cmd.js +2 -0
  32. package/dist/packages/cli/src/rules.js +25 -2
  33. package/dist/packages/cli/src/scan/ui.js +7 -1
  34. package/dist/packages/cli/src/screens/agent-hud.js +741 -0
  35. package/dist/packages/cli/src/screens/analyst-session.js +9 -3
  36. package/dist/packages/cli/src/screens/analyst.js +4 -3
  37. package/dist/packages/cli/src/screens/brain.js +608 -0
  38. package/dist/packages/cli/src/screens/newpairs.js +1 -1
  39. package/dist/packages/cli/src/screens/pnl-chip.js +143 -0
  40. package/dist/packages/cli/src/screens/screener.js +1 -0
  41. package/dist/packages/cli/src/screens/token.js +16 -7
  42. package/dist/packages/cli/src/screens/trending.js +1 -1
  43. package/dist/packages/cli/src/state.js +69 -5
  44. package/dist/packages/cli/src/version.js +1 -1
  45. package/dist/packages/custom/src/rules.js +2 -2
  46. package/dist/packages/custom/src/schema.js +13 -3
  47. package/dist/packages/data/src/fixtures.js +28 -6
  48. package/dist/packages/data/src/index.js +5 -0
  49. package/dist/packages/data/src/larp/score.js +220 -0
  50. package/dist/packages/data/src/larp/x-account.js +316 -0
  51. package/dist/packages/data/src/sources/jupiter.js +1 -0
  52. package/dist/packages/data/src/trade/agent/paper-desk.js +401 -0
  53. package/dist/packages/data/src/trade/agent/rendezvous.js +161 -0
  54. package/dist/packages/data/src/trade/plans/engine.js +7 -0
  55. package/dist/packages/data/src/trade/plans/mandate.js +28 -15
  56. package/dist/packages/data/src/trade/plans/service.js +1 -1
  57. package/dist/packages/mcp/assets/kb/app-help/agent-paper-trade.md +38 -0
  58. package/dist/packages/mcp/assets/kb/app-help/ask-the-analyst.md +1 -1
  59. package/dist/packages/mcp/assets/kb/app-help/connect-an-agent.md +1 -1
  60. package/dist/packages/mcp/assets/kb/app-help/dev-history.md +32 -0
  61. package/dist/packages/mcp/assets/kb/app-help/larp-check.md +5 -1
  62. package/dist/packages/mcp/assets/kb/app-help/paper-trade.md +1 -1
  63. package/dist/packages/mcp/assets/kb/app-help/scan-mode.md +1 -1
  64. package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +4 -1
  65. package/dist/packages/mcp/assets/kb/app-help/screen-token.md +4 -0
  66. package/dist/packages/mcp/assets/kb/app-help/spectate-an-agent.md +2 -1
  67. package/dist/packages/mcp/assets/kb/app-help/use-codex-analyst.md +31 -0
  68. package/dist/packages/mcp/assets/kb/docs/docs-agents-agent-paper-trading.md +51 -0
  69. package/dist/packages/mcp/assets/kb/docs/docs-agents-directory-metadata-prepared-not-submitted.md +36 -0
  70. package/dist/packages/mcp/assets/kb/docs/docs-agents-install-in-one-command.md +2 -0
  71. package/dist/packages/mcp/assets/kb/docs/docs-analyst-if-claude-is-missing.md +1 -1
  72. package/dist/packages/mcp/assets/kb/docs/docs-analyst-starting-a-run.md +2 -0
  73. package/dist/packages/mcp/assets/kb/docs/docs-analyst-use-codex-instead-of-claude.md +35 -0
  74. package/dist/packages/mcp/assets/kb/docs/docs-beginners-macos-or-linux-npm.md +1 -1
  75. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +1 -1
  76. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-powershell-experimental.md +1 -1
  77. package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-y-axis.md +32 -0
  78. package/dist/packages/mcp/assets/kb/docs/docs-configuration-environment-variables.md +2 -0
  79. package/dist/packages/mcp/assets/kb/docs/docs-configuration-price-display-mode.md +3 -3
  80. package/dist/packages/mcp/assets/kb/docs/docs-configuration-rules-engine.md +1 -0
  81. package/dist/packages/mcp/assets/kb/docs/docs-configuration-the-analyst-s-launch.md +2 -0
  82. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +13 -3
  83. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +1 -1
  84. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-running-the-tests.md +1 -1
  85. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +2 -2
  86. package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +1 -2
  87. package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +60 -0
  88. package/dist/packages/mcp/assets/kb/docs/docs-larp-the-verdict.md +0 -2
  89. package/dist/packages/mcp/assets/kb/docs/docs-larp-what-it-looks-at.md +4 -1
  90. package/dist/packages/mcp/assets/kb/docs/docs-larp.md +1 -1
  91. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-letting-an-agent-paper-trade-experimental.md +32 -0
  92. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-order-plans-and-standing-mandates.md +1 -9
  93. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-paper-only-mandates.md +41 -0
  94. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-what-the-live-path-keeps.md +1 -1
  95. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-market-cap-axis.md +1 -1
  96. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-what-it-shows.md +1 -0
  97. package/dist/packages/mcp/assets/kb/docs/docs-scan-the-read.md +2 -0
  98. package/dist/packages/mcp/assets/kb/docs/docs-screens-dev-history.md +47 -0
  99. package/dist/packages/mcp/assets/kb/docs/docs-screens-how-well-it-predicts-measured-held-out.md +46 -0
  100. package/dist/packages/mcp/assets/kb/docs/docs-spectator-follow-over-mcp-in-the-running-terminal.md +1 -1
  101. package/dist/packages/mcp/assets/kb/docs/docs-spectator-limits.md +2 -0
  102. package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-agent-hud-spectator-v2.md +58 -0
  103. package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-brain-view-and-the-pnl-chip.md +46 -0
  104. package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-three-surfaces.md +1 -1
  105. package/dist/packages/mcp/assets/kb/docs/docs-spectator.md +1 -1
  106. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-doctor-lines.md +2 -0
  107. package/dist/packages/mcp/src/index.js +5 -2
  108. package/dist/packages/mcp/src/paper-agent.js +93 -0
  109. package/dist/packages/mcp/src/server.js +354 -19
  110. package/dist/packages/mcp/src/spectator-sink.js +158 -4
  111. package/dist/packages/mcp/src/user-content.js +1 -0
  112. package/dist/packages/packs/src/index.js +1 -0
  113. package/dist/packages/packs/src/larp-score-backtest.js +119 -0
  114. package/dist/packages/popup/src/pages/chart-lib.js +36 -15
  115. package/dist/packages/popup/src/pages/chart.css +9 -0
  116. package/dist/packages/popup/src/pages/chart.html +4 -1
  117. package/dist/packages/popup/src/pages/chart.js +34 -5
  118. package/dist/packages/popup/src/pages/spectator.css +45 -0
  119. package/dist/packages/popup/src/pages/spectator.html +1 -0
  120. package/dist/packages/popup/src/pages/spectator.js +69 -0
  121. package/dist/packages/popup/src/protocol.js +2 -0
  122. package/dist/packages/popup/src/server.js +5 -1
  123. package/dist/packages/recorder/src/dev-history.js +133 -0
  124. package/dist/packages/recorder/src/index.js +1 -0
  125. package/dist/packages/recorder/src/recorder.js +23 -0
  126. package/dist/packages/recorder/src/schema.js +11 -0
  127. package/dist/packages/tui/src/canvas.js +7 -0
  128. package/dist/packages/tui/src/fmt.js +53 -0
  129. package/dist/packages/tui/src/keymap.js +4 -0
  130. package/dist/packages/tui/src/widgets.js +9 -4
  131. package/package.json +1 -1
@@ -25,11 +25,13 @@ 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";
32
+ import { FOLLOW_NAV_ACTIONS } from '../../mcp/src/index.js';
32
33
  import { McpFollowSession } from "./mcp-follow.js";
34
+ import { AgentPaperSwitch } from "./agent-paper.js";
33
35
  import { TOKEN_REFRESH_MS, CandlePrefetcher } from "./live/token.js";
34
36
  import { PRESETS, presetSpec } from "./screener/presets.js";
35
37
  import { parseSortArg, SORT_KEYS } from "./screener/sort.js";
@@ -37,7 +39,9 @@ import { parseFilter } from "./screener/filter.js";
37
39
  import { screenerView, setFilter, setSort, setPreset, stepPreset, clampSelection, openFilterEditor, openSortMenu, filterEditorKey, sortMenuKey, } from "./screener/view.js";
38
40
  import { readBootState, bootPlan, recordBoot, bootTrace, dextermHome } from "./boot-state.js";
39
41
  import { socialOf, kolOf, trendingRows, kolRows, nextSocialFilter, feedsPopupModel } from "./social/view.js";
40
- 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";
41
45
  import { widgetPage, widgetEnabled, syncWidgetPopups } from "./widgets.js";
42
46
  import { recordLarpVerdict } from "./larp-record.js";
43
47
  import { rulesUiOf, rulesTick, rulesMove, beginRuleInput, saveRuleInput, dryRunNow, removeSelectedRule, toggleSelectedRule, dismissSelectedProposal, actOnSelectedProposal, RULE_INPUT_MAX } from "./rules.js";
@@ -46,11 +50,12 @@ import { copyAlertToDelivery } from "./alerts/format.js";
46
50
  import { readTelegramCreds, readDiscordCreds } from "./alerts/creds.js";
47
51
  import { launchIntelCells, launchRetryable } from "./live/intel.js";
48
52
  import { AnalystSession } from "./analyst/session.js";
53
+ import { analystCliOnPath, parseEngineChoice, resolveEngine } from "./analyst/codex.js";
49
54
  import { resolveAnalystResults } from "./analyst/results.js";
50
55
  import { parseScanCommand, SCAN_FLAGS, checkScanFlag, SCAN_HELP } from "./scan/command.js";
51
56
  import { parseWalletCommand, WALLET_HELP } from "./wallet-command.js";
52
57
  import { X_HELP } from "./x-cmd.js";
53
- 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";
54
59
  import { buildObservations, observeSig } from "./scan/observe.js";
55
60
  import { cleanText } from "./screener/rows.js";
56
61
  import { resolveImagesConfig } from "./images-config.js";
@@ -59,7 +64,8 @@ import { VoiceController, selectStt, selectTts, SayTts, voiceStyleDefaults } fro
59
64
  import { LivePlanExecutor, defaultPlansJournalPath } from '../../data/src/trade/plans/index.js';
60
65
  import { isWalletAddress, shortAddr, buildMirror, leaderSoldSince, autoSizeCap } from '../../data/src/trade/copy/index.js';
61
66
  import { FOLLOW_INPUT_MAX } from "./screens/follows.js";
62
- 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";
63
69
  import { VERSION } from "./version.js";
64
70
  import { initCustom, planWorkspace, planPopupOpens, findWorkspace, nextWorkspaceName, workspaceSlots, saveWorkspace, themeFor, themeByName, cycleTheme, themeNames, CHORD_TTL_MS, MAX_WORKSPACE_NAME } from "./custom.js";
65
71
  import { DEFAULT_WORKSPACE, MORNING_HUD, applyAndJournal, policyCheck } from '../../custom/src/index.js';
@@ -101,24 +107,55 @@ export function appStateBlock(state) {
101
107
  alerts, // booleans only — never a token, webhook or key
102
108
  };
103
109
  }
110
+ /**
111
+ * task-1443: the reduced screen snapshot a terminal following an EXTERNAL agent (`--spectate-mcp`) publishes for
112
+ * that agent's `dexterm mcp` server: what is on screen plus a VIEW-ONLY nav allow-list. Never the watchlist,
113
+ * tickets, positions or app settings, and only the four follow nav action types (no watchlist add, no ticket).
114
+ */
115
+ export function followSnapshot(state) {
116
+ const ss = buildScreenState(state);
117
+ const a = ss.actions;
118
+ return {
119
+ screen: ss.screen, selected: ss.selected, selectedDetail: ss.selectedDetail, filter: ss.filter, sort: ss.sort,
120
+ visible: ss.visible, visibleMints: ss.visibleMints,
121
+ actions: { enabled: a.enabled, mints: a.mints, feeds: a.feeds, sortKeys: a.sortKeys, screens: a.screens, workspaces: [], themes: [], types: [...FOLLOW_NAV_ACTIONS] },
122
+ now: ss.now,
123
+ };
124
+ }
104
125
  export function buildScreenState(state) {
105
126
  const live = state.live;
106
127
  const sc = state.screener;
107
128
  // The screener and the Analyst-tab coin list both show the filtered/sorted screener view, so the
108
129
  // selection + visible rows must come from that same view (task-1313); other screens use the raw pool.
109
- 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;
110
134
  const toRow = (r) => {
111
135
  const x = r;
112
136
  if (!x?.mint)
113
137
  return null;
114
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 };
115
139
  };
116
- 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);
117
152
  let selected = null;
118
153
  if (state.screen === 'token' && live?.token?.view)
119
154
  selected = { mint: live.token.view.mint, symbol: live.token.view.symbol };
120
155
  else {
121
- 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]);
122
159
  if (r)
123
160
  selected = { mint: r.mint, symbol: r.symbol };
124
161
  }
@@ -249,7 +286,7 @@ export function shouldDrawFrame(g) {
249
286
  return true;
250
287
  if (g.calm)
251
288
  return g.onTick;
252
- if (g.fxBusy)
289
+ if (g.fxBusy || g.hud)
253
290
  return true;
254
291
  return g.frame % g.idleEvery === 0;
255
292
  }
@@ -288,6 +325,11 @@ export const HELP = `dexterm ${VERSION} (live: screener, token, new pairs, watch
288
325
  --record FILE save every live response (and the PumpPortal session) to FILE (replay it with --replay)
289
326
  --spectate-mcp follow an MCP agent's allowed navigation in this terminal; Ctrl+P pauses, Ctrl+K stops,
290
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
330
+ --agent-paper MANDATE_ID (experimental, with --spectate-mcp) let that MCP agent PAPER-trade inside a mandate you
331
+ armed: simulated fills only, never signs or sends, never raises a cap; Ctrl+P / Ctrl+K /
332
+ any key also pause, stop or take over its paper trading
291
333
  --max-requests N hard cap on live requests for this run (default: none)
292
334
  --max-credits N hard cap on Helius credits for this run (default: 5000)
293
335
  --mint MINT|LINK token to open on the token screen (a CA or a dexscreener / pump.fun / solscan / birdeye link)
@@ -302,6 +344,8 @@ export const HELP = `dexterm ${VERSION} (live: screener, token, new pairs, watch
302
344
  --selected N highlighted row / token index
303
345
  --seed N fixture PRNG seed (default 7)
304
346
  --no-ai hide the analyst panel on the token screen
347
+ --analyst-engine E auto | claude | codex: which of YOUR installed CLIs runs the analyst (default auto = claude
348
+ if installed, else codex; DEXTERM_ANALYST_ENGINE or config analyst.engine too; /engine in chat)
305
349
 
306
350
  --intensity LEVEL off | normal | fever (default fever: rain, trails, glitch, blink, scanlines at 30 fps;
307
351
  normal: 15 fps, sparser rain, no idle aberration; off = calm: neon theme, no motion)
@@ -389,14 +433,14 @@ export function parseArgs(argv) {
389
433
  screen: null, fixtures: false, replay: null, record: null, filter: null, sort: null, preset: null, maxRequests: 0, maxCredits: 0, db: null,
390
434
  mint: null, interval: '1h', listen: 20, verbose: false,
391
435
  width: null, height: null, html: null, export: null, theme: DEFAULT_THEME, selected: 0, seed: 7,
392
- noAi: false, title: null, plain: false, ansi: false, color: null, tz: null,
436
+ noAi: false, analystEngine: null, title: null, plain: false, ansi: false, color: null, tz: null,
393
437
  calm: false, intensity: null, fps: null, frame: 0, t: 1, event: null, ai: null, mood: null, activity: null, noBoot: false, fit: null, fontPx: null, asciiRain: false, noImages: false,
394
438
  scan: false, scanConfig: {},
395
439
  noRecorder: false,
396
440
  sound: false,
397
441
  trade: [], live: true,
398
442
  workspace: null, priceMode: null,
399
- spectateMcp: false,
443
+ spectateMcp: false, spectateThoughts: null, agentPaper: null,
400
444
  help: false, version: false, unknown: [],
401
445
  };
402
446
  for (let i = 0; i < argv.length; i++) {
@@ -485,6 +529,13 @@ export function parseArgs(argv) {
485
529
  }
486
530
  else if (k === '--no-ai')
487
531
  a.noAi = true;
532
+ else if (k === '--analyst-engine') {
533
+ const s = str();
534
+ const e = parseEngineChoice(s);
535
+ if (!e)
536
+ throw new Error(`unknown analyst engine ${s} (expected auto|claude|codex)`);
537
+ a.analystEngine = e;
538
+ }
488
539
  else if (k === '--calm')
489
540
  a.calm = true;
490
541
  else if (k === '--intensity') {
@@ -581,6 +632,15 @@ export function parseArgs(argv) {
581
632
  // task-1421: follow an agent driving over MCP (the running terminal moves with it; a read-only stream opens).
582
633
  else if (k === '--spectate-mcp')
583
634
  a.spectateMcp = true;
635
+ else if (k === '--spectate-thoughts')
636
+ a.spectateThoughts = str();
637
+ // task-1443: agent PAPER trading inside a mandate the user armed (validated against the plans journal at start).
638
+ else if (k === '--agent-paper') {
639
+ const s = str();
640
+ if (!/^[A-Za-z0-9_-]{1,64}$/.test(s))
641
+ throw new Error(`--agent-paper expects a mandate id (see \`dexterm mandate list\`), got ${JSON.stringify(s.slice(0, 20))}`);
642
+ a.agentPaper = s;
643
+ }
584
644
  else if (k === '--workspace') {
585
645
  const s = str();
586
646
  if (!/^[a-z][a-z0-9-]{0,39}(\/[a-z][a-z0-9-]{0,39})?$/.test(s))
@@ -618,6 +678,12 @@ export function parseArgs(argv) {
618
678
  throw new Error('--scan needs live data or a recording (drop --fixtures)');
619
679
  if (a.scan && a.screen)
620
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)');
683
+ if (a.agentPaper && !a.spectateMcp)
684
+ throw new Error('--agent-paper needs --spectate-mcp (every agent paper trade must be visible in this terminal)');
685
+ if (a.agentPaper && a.db === ':memory:')
686
+ throw new Error('--agent-paper needs an on-disk --db (the agent books into the same paper wallet)');
621
687
  return a;
622
688
  }
623
689
  /**
@@ -1019,6 +1085,8 @@ export async function main(argv, io = process, proc = process) {
1019
1085
  });
1020
1086
  recorder.attach(feed.dl.store);
1021
1087
  ctx.recorder = recorder;
1088
+ // 0.1.5 dev history: PumpPortal / RugCheck creators of new pairs go into the creator index too.
1089
+ ctx.newpairs.onCreator = (mint, creator, source, createdAt) => { recorder.recordCreator(mint, creator, source, undefined, createdAt); };
1022
1090
  }
1023
1091
  if (args.verbose)
1024
1092
  io.stderr.write(`dexterm: rpc ${rpc.provider} (${rpc.basis}) · db ${dbPath} · recorder ${ctx.recorder ? 'on' : 'off'} · live trading ${args.live && !io.env.DEXTERM_OFFLINE ? 'available' : 'off'}\n`);
@@ -1084,6 +1152,7 @@ export async function main(argv, io = process, proc = process) {
1084
1152
  for (const e of state.custom.themeErrors)
1085
1153
  io.stderr.write(`dexterm: ${e}\n`);
1086
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
1087
1156
  if (args.workspace) {
1088
1157
  const r = switchWorkspace(state, args.workspace, []);
1089
1158
  if (!r.ok) {
@@ -2428,6 +2497,9 @@ export function applyAction(state, action, stack) {
2428
2497
  case 'display.priceMode':
2429
2498
  state.pendingToast = togglePriceMode(state);
2430
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
2431
2503
  case 'theme.next':
2432
2504
  state.theme = state.custom ? cycleTheme(state.custom, state.theme) : nextTheme(state.theme);
2433
2505
  break; // task-1358: user themes join the cycle
@@ -2591,6 +2663,7 @@ export function scanCommand(state, cmd, stack, now = Date.now()) {
2591
2663
  if (!e.start(now))
2592
2664
  return `AI scan is already on (${MODE_LABEL[e.mode]})`;
2593
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`);
2594
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`;
2595
2668
  case 'off': {
2596
2669
  if (!e.on)
@@ -2598,6 +2671,7 @@ export function scanCommand(state, cmd, stack, now = Date.now()) {
2598
2671
  stopTurn();
2599
2672
  const was = e.lock?.symbol;
2600
2673
  e.stop(now);
2674
+ scanLog(scan, now, 'stop', was ?? '', was ? 'scan stopped · released' : 'scan stopped');
2601
2675
  live.scanMeter = null;
2602
2676
  scan.returnTo = null;
2603
2677
  return `AI scan stopped${was ? ` (released ${sym(was)})` : ''}`;
@@ -2610,6 +2684,7 @@ export function scanCommand(state, cmd, stack, now = Date.now()) {
2610
2684
  const locked = e.lock;
2611
2685
  stopTurn();
2612
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`);
2613
2688
  live.scanMeter = scanMeterOf(e, now);
2614
2689
  // back from the coin the lock pulled up to where the user was, so the ticker is in view
2615
2690
  if (locked && state.screen === 'token' && live.token?.view.mint === locked.mint)
@@ -2639,6 +2714,50 @@ export function applyScanAnalystAction(state, raw) {
2639
2714
  const res = validateAnalystAction(raw, { ...allow, types: SCAN_ACTION_TYPES });
2640
2715
  return { ok: false, error: res.ok ? 'AI scan mode is observe-only' : res.error };
2641
2716
  }
2717
+ /**
2718
+ * The analyst engine for a run (0.1.5): `--analyst-engine`, then DEXTERM_ANALYST_ENGINE, then config
2719
+ * `analyst.engine`, then auto (claude when installed, else codex). Only the user picks it; models cannot.
2720
+ */
2721
+ export function analystEngineFor(flag, env, configured, has = (b) => analystCliOnPath(b, env)) {
2722
+ const choice = parseEngineChoice(flag) ?? parseEngineChoice(env.DEXTERM_ANALYST_ENGINE) ?? parseEngineChoice(configured) ?? 'auto';
2723
+ return resolveEngine(choice, has);
2724
+ }
2725
+ /** `/engine [auto|claude|codex]` in the chat box (0.1.5). Null when the input is not this command. */
2726
+ export function parseEngineCommand(q) {
2727
+ const m = /^\/engine(?:\s+(\S+))?\s*$/i.exec(q.trim());
2728
+ if (!m)
2729
+ return null;
2730
+ if (!m[1])
2731
+ return { show: true };
2732
+ const c = parseEngineChoice(m[1]);
2733
+ return c ? { choice: c } : { error: `unknown analyst engine "${m[1].slice(0, 16)}" (use /engine auto|claude|codex)` };
2734
+ }
2735
+ /**
2736
+ * Apply `/engine`: switch the CLI for NEW conversations this run (cached per-coin chats are dropped so the next
2737
+ * `a` starts on the chosen engine; a running turn finishes where it started). Returns the line to show.
2738
+ */
2739
+ export function engineCommand(state, cmd, env, has = (b) => analystCliOnPath(b, env)) {
2740
+ const installed = ['claude', 'codex'].filter((b) => has(b));
2741
+ const list = installed.length ? installed.join(' + ') : 'neither claude nor codex';
2742
+ if ('error' in cmd)
2743
+ return cmd.error;
2744
+ const cur = state.analystEngine ?? 'claude';
2745
+ if ('show' in cmd)
2746
+ return `analyst engine: ${cur} · installed: ${list} · /engine claude|codex|auto switches (config analyst.engine keeps it)`;
2747
+ const next = resolveEngine(cmd.choice, has);
2748
+ if (cmd.choice !== 'auto' && !has(next))
2749
+ return `${next} is not installed (installed: ${list}); ${next === 'codex' ? 'npm i -g @openai/codex && codex login' : 'install Claude Code, then run claude once to log in'}`;
2750
+ if (next !== cur) {
2751
+ for (const [k, sess] of state.analystByMint ?? []) {
2752
+ if (!sess.busy)
2753
+ state.analystByMint.delete(k);
2754
+ }
2755
+ if (state.analyst && !state.analyst.busy && state.analyst.engine !== next)
2756
+ state.analyst = null;
2757
+ }
2758
+ state.analystEngine = next;
2759
+ return `analyst engine: ${next} (your own ${next} CLI and login) for new conversations · set analyst.engine in config.json to keep it`;
2760
+ }
2642
2761
  /**
2643
2762
  * The ONE way a scan-driven analyst conversation is built (task-1324; the loop and the safety tests both
2644
2763
  * use it): the locked coin as the selected coin, `actionTypes: []` (observe-only at the validator, the
@@ -2652,7 +2771,7 @@ export function createScanSession(state, mint, symbol, hooks = {}, deps = {}) {
2652
2771
  const type = typeof raw.type === 'string' ? raw.type.replace(/[^a-z_]/gi, '').slice(0, 24) : '?';
2653
2772
  hooks.onBlocked?.(type, r.error ?? 'refused');
2654
2773
  };
2655
- return new AnalystSession({ skill: 'token-dossier', mint, symbol, screenState: ss, onAction, actionTypes: SCAN_ACTION_TYPES, scan: true }, hooks.onChange ?? (() => { }), deps);
2774
+ return new AnalystSession({ skill: 'token-dossier', mint, symbol, screenState: ss, onAction, actionTypes: SCAN_ACTION_TYPES, scan: true, ...(state.analystEngine === 'codex' ? { engine: 'codex' } : {}) }, hooks.onChange ?? (() => { }), deps);
2656
2775
  }
2657
2776
  /**
2658
2777
  * Resolve a mint to an openable seed from what the app already knows (screener view → new pairs →
@@ -2908,6 +3027,61 @@ export function applyAnalystAction(state, raw, stack) {
2908
3027
  times.push(now);
2909
3028
  return { ok: true, label: res.label };
2910
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
+ }
2911
3085
  /**
2912
3086
  * task-1421: move the REAL terminal to mirror an agent's navigation (follow mode, on the owner's own
2913
3087
  * machine). This is the same VIEW-ONLY navigation the analyst / go-to path uses — open a token, switch
@@ -3476,7 +3650,14 @@ export function terminalGuard(io, onFatal = () => { }, proc = process, hooks = {
3476
3650
  proc.exit(1);
3477
3651
  };
3478
3652
  const onExit = () => restore();
3479
- const onSignal = (sig) => { restore(); proc.exit(sig === 'SIGTERM' ? 143 : 129); };
3653
+ const onSignal = (sig) => {
3654
+ restore();
3655
+ try {
3656
+ hooks.onSignal?.();
3657
+ }
3658
+ catch { /* cleanup is best-effort; the exit still happens */ }
3659
+ proc.exit(sig === 'SIGTERM' ? 143 : 129);
3660
+ };
3480
3661
  const onTerm = () => onSignal('SIGTERM');
3481
3662
  const onHup = () => onSignal('SIGHUP');
3482
3663
  let installed = false;
@@ -3513,6 +3694,8 @@ export function fpsFor(args, intensity) {
3513
3694
  const SYNC_ON = '\x1b[?2026h';
3514
3695
  const SYNC_OFF = '\x1b[?2026l';
3515
3696
  export function interactive(state, io, depth, args, proc = process) {
3697
+ // 0.1.5: the analyst engine for new conversations (the user's own claude or codex CLI; never a shipped key).
3698
+ state.analystEngine ??= analystEngineFor(args.analystEngine, io.env, state.custom?.config.analyst?.engine);
3516
3699
  const out = io.stdout;
3517
3700
  const inp = io.stdin;
3518
3701
  const rnd = mulberry32(99);
@@ -3816,6 +3999,9 @@ export function interactive(state, io, depth, args, proc = process) {
3816
3999
  // task-1421: the MCP follow session (set below when `--spectate-mcp` is on). Declared here so the quit /
3817
4000
  // terminal-guard teardown closures can stop it.
3818
4001
  let followSession = null;
4002
+ let lastHudMark = null; // task-1466: the last terminal price that marked the agent's PAPER position
4003
+ // task-1443: `--agent-paper <mandate-id>` (with --spectate-mcp) lets that agent PAPER-trade inside the mandate.
4004
+ const agentPaper = args.spectateMcp && args.agentPaper ? new AgentPaperSwitch({ env: io.env, mandateId: args.agentPaper, db: args.db ?? defaultDbPath(io.env) }) : null;
3819
4005
  // Idle economy (task-1303, retuned for task-1319 AC3): between events the ambient rain redraws at
3820
4006
  // IDLE_FPS instead of the full frame cap, cutting idle CPU. A keypress opens a KEY_WAKE_S full-rate
3821
4007
  // window (`wakeUntil`) so input and transitions stay smooth; data landings paint at once on their own
@@ -3844,10 +4030,13 @@ export function interactive(state, io, depth, args, proc = process) {
3844
4030
  say(`trade moment failed: ${describeError(e)}`, 'warn');
3845
4031
  }
3846
4032
  };
3847
- const guard = terminalGuard(io, () => { done = true; prefetcher?.close(); if (timer)
3848
- clearInterval(timer); void followSession?.stop(); state.analyst?.cancel(); if (state.analystByMint)
4033
+ const teardown = () => { done = true; prefetcher?.close(); if (timer)
4034
+ clearInterval(timer); void followSession?.stop(); agentPaper?.stop(); state.analyst?.cancel(); if (state.analystByMint)
3849
4035
  for (const s of state.analystByMint.values())
3850
- s.cancel(); state.popups?.shutdown(); voice?.disable(); images?.close(); live?.close(); feed?.close(); }, proc, { beforeLeave: () => layer?.teardown() ?? '' });
4036
+ s.cancel(); state.popups?.shutdown(); voice?.disable(); images?.close(); live?.close(); feed?.close(); };
4037
+ // task-1443: SIGTERM/SIGHUP (a closed terminal window) runs the same teardown as a fatal error, so the agent-paper
4038
+ // rendezvous, the spectator rendezvous and the --record file never outlive the terminal that owns them.
4039
+ const guard = terminalGuard(io, teardown, proc, { beforeLeave: () => layer?.teardown() ?? '', onSignal: teardown });
3851
4040
  let lastTokenRefresh = 0;
3852
4041
  let lastWatchRefresh = 0;
3853
4042
  let lastPositionsRefresh = 0;
@@ -4041,11 +4230,22 @@ export function interactive(state, io, depth, args, proc = process) {
4041
4230
  }
4042
4231
  scheduleDraw();
4043
4232
  },
4044
- 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(); },
4045
4236
  onEvent: () => scheduleDraw(),
4237
+ onControl: agentPaper ? (st) => agentPaper.control(st) : undefined,
4238
+ snapshot: () => followSnapshot(state),
4239
+ thoughtsFile: args.spectateThoughts ?? undefined,
4046
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 };
4047
4244
  void followSession.start();
4048
- 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);
4247
+ if (agentPaper)
4248
+ say(agentPaper.start(), 'ai', 8);
4049
4249
  }
4050
4250
  // Launch a real Claude analyst session when a key asked for one (applyAction cannot spawn). A
4051
4251
  // `question` means start it as a chat turn, not a paid structured skill run (task-1311, QA B3).
@@ -4113,7 +4313,7 @@ export function interactive(state, io, depth, args, proc = process) {
4113
4313
  maybeSpeakVoiceAnswer(session); // task-1282: speak a voice-initiated answer when it settles
4114
4314
  scheduleDraw();
4115
4315
  };
4116
- const session = new AnalystSession({ skill: p.skill, mint: p.mint, symbol: p.symbol, screenState: buildScreenState(state), model: p.model, onAction }, onChange);
4316
+ const session = new AnalystSession({ skill: p.skill, mint: p.mint, symbol: p.symbol, screenState: buildScreenState(state), model: p.model, onAction, ...(state.analystEngine === 'codex' ? { engine: 'codex' } : {}) }, onChange);
4117
4317
  if (p.question)
4118
4318
  session.ask(p.question, buildScreenState(state));
4119
4319
  else
@@ -4151,6 +4351,7 @@ export function interactive(state, io, depth, args, proc = process) {
4151
4351
  scan.blocked.push(type);
4152
4352
  if (scan.blocked.length > 6)
4153
4353
  scan.blocked.shift();
4354
+ scanLog(scan, Date.now(), 'blocked', scan.engine.lock?.symbol ?? '', `blocked analyst action "${type}" · observe-only`);
4154
4355
  }
4155
4356
  scanSay(`AI scan blocked analyst action "${type}" · observe-only (${error})`, 'warn', 5);
4156
4357
  scheduleDraw();
@@ -4182,6 +4383,7 @@ export function interactive(state, io, depth, args, proc = process) {
4182
4383
  state.aiFocused = false;
4183
4384
  state.aiScroll = 0;
4184
4385
  state.aiRevealAll = false;
4386
+ scanLog(scan, Date.now(), 'lock', d.symbol, `locked · score ${d.score} · ${d.hits.map((h) => h.reason).join(' · ')}`);
4185
4387
  startScanSession(d.mint, d.symbol, d.question, `AI scan · score ${d.score} · ${d.hits.map((h) => h.id).join(' + ')} → short read`);
4186
4388
  syncAnalyst(state);
4187
4389
  live.syncFastSet();
@@ -4199,6 +4401,7 @@ export function interactive(state, io, depth, args, proc = process) {
4199
4401
  const sess = scan.session;
4200
4402
  if (!sess)
4201
4403
  return;
4404
+ scanLog(scan, Date.now(), 'refresh', d.symbol, `moved: ${d.moved.join(', ')} · refreshing the read`);
4202
4405
  sess.ask(d.question, scanScreenState(state, d.mint, d.symbol), `AI scan refresh · ${d.moved.join(' · ')}`);
4203
4406
  if (state.analyst === sess) {
4204
4407
  state.aiScroll = 0;
@@ -4237,7 +4440,8 @@ export function interactive(state, io, depth, args, proc = process) {
4237
4440
  return;
4238
4441
  if (scan.session?.busy)
4239
4442
  scan.session.interrupt();
4240
- 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');
4241
4445
  if (live)
4242
4446
  live.scanMeter = scanMeterOf(scan.engine, Date.now());
4243
4447
  scanSay(`analysis cancelled · scan holds $${cleanText(scan.engine.lock?.symbol ?? '', 12)} · R resume scan · Z stop`, 'ai', 5);
@@ -4323,6 +4527,10 @@ export function interactive(state, io, depth, args, proc = process) {
4323
4527
  solUsd: () => state.sol?.price ?? live.positions.solUsd(),
4324
4528
  imagePath: (v) => popupImagePath(state, v.image),
4325
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
4326
4534
  } : null,
4327
4535
  });
4328
4536
  }
@@ -5037,6 +5245,7 @@ export function interactive(state, io, depth, args, proc = process) {
5037
5245
  if (timer)
5038
5246
  clearInterval(timer);
5039
5247
  void followSession?.stop();
5248
+ agentPaper?.stop();
5040
5249
  guard.restore();
5041
5250
  guard.uninstall();
5042
5251
  cancelAllAnalysts();
@@ -5635,6 +5844,14 @@ export function interactive(state, io, depth, args, proc = process) {
5635
5844
  draw();
5636
5845
  continue;
5637
5846
  }
5847
+ // 0.1.5: `/engine [auto|claude|codex]` switches the analyst CLI for new conversations; never sent to a model.
5848
+ const en0 = q.startsWith('/') ? parseEngineCommand(q) : null;
5849
+ if (en0) {
5850
+ state.aiInput = '';
5851
+ say(engineCommand(state, en0, io.env), typeof en0 === 'object' && 'error' in en0 ? 'warn' : 'info', 6);
5852
+ draw();
5853
+ continue;
5854
+ }
5638
5855
  // task-1324: `/scan …` in the chat input is a command, never sent to claude.
5639
5856
  const sc0 = q.startsWith('/') ? parseScanCommand(q) : null;
5640
5857
  if (sc0) {
@@ -6085,6 +6302,36 @@ export function interactive(state, io, depth, args, proc = process) {
6085
6302
  }
6086
6303
  // market → rain: EMA on activity every tick, mood from the data every 4 s unless an event holds it
6087
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
+ }
6088
6335
  }
6089
6336
  if (state.ai && state.ai.phase === 'thinking') {
6090
6337
  state.ai.ticks += 1;
@@ -6114,6 +6361,7 @@ export function interactive(state, io, depth, args, proc = process) {
6114
6361
  aiLive: !!(state.ai && state.ai.phase !== 'done'),
6115
6362
  awake: v.frame < wakeUntil,
6116
6363
  fxBusy: fxBusy(v),
6364
+ hud: followSession !== null && hudAnimating(followSession.story.view(Date.now()), Date.now()),
6117
6365
  frame: v.frame,
6118
6366
  idleEvery,
6119
6367
  }))