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
@@ -1,4 +1,5 @@
1
- // dexterm — the price display mode (task-1366): show USD price or MARKET CAP on every screen and both charts.
1
+ // dexterm — the price display mode (task-1366): show USD price or MARKET CAP on every screen; task-1472: the chart
2
+ // Y axis (both charts) is its own setting, `display.chartAxis`, default MARKET CAP, toggled with `A`.
2
3
  //
3
4
  // A DISPLAY mode only. Market cap = price × supply, where the supply is what the data layer already reports
4
5
  // (the row's mcap / price at the latest tick: the "supply ratio"). Historical candles are scaled by the CURRENT
@@ -7,13 +8,15 @@
7
8
  // missing / zero), the screen falls back to price and SAYS so; it never invents a number.
8
9
  //
9
10
  // No network calls, nothing here is read by the signer, the caps, tickets, plans or the confirm gate.
10
- import { fmtMcap, fmtMcapBare, fmtPrice } from '../../tui/src/index.js';
11
- import { applyAndJournal, configPath, dextermHome, loadConfig, DEFAULT_PRICE_MODE, PRICE_MODES } from '../../custom/src/index.js';
12
- export { PRICE_MODES, DEFAULT_PRICE_MODE };
11
+ import { fmtMcap, fmtCompact, fmtPrice } from '../../tui/src/index.js';
12
+ import { applyAndJournal, configPath, dextermHome, loadConfig, DEFAULT_PRICE_MODE, PRICE_MODES, CHART_AXES, DEFAULT_CHART_AXIS } from '../../custom/src/index.js';
13
+ export { PRICE_MODES, DEFAULT_PRICE_MODE, CHART_AXES, DEFAULT_CHART_AXIS };
13
14
  /** Axis / caption label for an mcap chart: the history is scaled by today's supply, and says so. */
14
15
  export const MCAP_AXIS_LABEL = 'mcap (current supply)';
15
16
  /** Shown wherever mcap mode is on but a token's supply is unknown, so the number on screen is a price. */
16
17
  export const MCAP_FALLBACK_NOTE = 'supply unknown · showing price';
18
+ /** task-1472: the chart title when the Y axis wants market cap but the token's supply is unknown (it plots price). */
19
+ export const MCAP_UNAVAILABLE_LABEL = 'price (mcap unavailable)';
17
20
  /** Short per-cell marker for a price shown in an MCAP column (fallback). */
18
21
  export const FALLBACK_MARK = 'ᵖ';
19
22
  export function isPriceMode(v) { return PRICE_MODES.includes(v); }
@@ -78,14 +81,24 @@ export function fallbackCount(rows, get) {
78
81
  export function fallbackCaption(n) {
79
82
  return n > 0 ? `${FALLBACK_MARK} ${n} price${n > 1 ? 's' : ''} (supply unknown)` : null;
80
83
  }
81
- /** The chart scale for a token: mcap mode with a known supply scales by the CURRENT supply ratio, else prices (+ a note). */
84
+ /**
85
+ * The chart scale for a token: an mcap axis with a known supply scales by the CURRENT supply ratio (labels in the shared
86
+ * compact k/M/B formatter), else prices (+ `price (mcap unavailable)`). task-1472: the charts pass the chart axis
87
+ * (chartAxisOf, default mcap); the token header still passes the table price mode.
88
+ */
82
89
  export function chartMode(mode, price, mcap) {
83
90
  if (mode !== 'mcap')
84
91
  return { scale: null, axisFmt: undefined, kind: 'price', label: null };
85
92
  const ratio = supplyRatio(price, mcap);
86
93
  if (ratio === null)
87
- return { scale: null, axisFmt: undefined, kind: 'price', label: MCAP_FALLBACK_NOTE };
88
- return { scale: ratio, axisFmt: fmtMcapBare, kind: 'mcap', label: MCAP_AXIS_LABEL };
94
+ return { scale: null, axisFmt: undefined, kind: 'price', label: MCAP_UNAVAILABLE_LABEL };
95
+ return { scale: ratio, axisFmt: fmtCompact, kind: 'mcap', label: MCAP_AXIS_LABEL };
96
+ }
97
+ /** task-1472: the chart box title: `WIF · MCAP (current supply) · 1h`, `WIF/USD · 1h`, or `WIF/USD · price (mcap unavailable) · 1h`. */
98
+ export function chartTitle(symbol, interval, cm, quote = 'USD') {
99
+ if (cm.kind === 'mcap')
100
+ return `${symbol} · MCAP ${MCAP_AXIS_LABEL.replace(/^mcap /, '')} · ${interval}`;
101
+ return cm.label ? `${symbol}/${quote} · ${cm.label} · ${interval}` : `${symbol}/${quote} · ${interval}`;
89
102
  }
90
103
  /**
91
104
  * One plan level with BOTH numbers, in the order the mode prefers: mcap mode `mc ≤ $300K ($0.0003)`, price mode
@@ -145,7 +158,7 @@ export function configPriceMode(cfg) {
145
158
  export function togglePriceMode(state) {
146
159
  const next = priceModeOf(state) === 'mcap' ? 'price' : 'mcap';
147
160
  state.priceMode = next;
148
- const what = next === 'mcap' ? `showing MARKET CAP · charts: ${MCAP_AXIS_LABEL}` : 'showing USD PRICE';
161
+ const what = next === 'mcap' ? 'showing MARKET CAP' : 'showing USD PRICE'; // task-1472: the charts follow A, not $
149
162
  const ui = state.custom;
150
163
  if (!ui)
151
164
  return `${what} (not saved: no config on this run)`;
@@ -159,6 +172,37 @@ export function togglePriceMode(state) {
159
172
  return `${what} (not saved: ${e.message})`;
160
173
  }
161
174
  }
175
+ export function isChartAxis(v) { return CHART_AXES.includes(v); }
176
+ /** task-1472: the chart Y axis in effect: the state's (from config or `A`), default MARKET CAP. */
177
+ export function chartAxisOf(state) {
178
+ return state?.chartAxis === 'price' ? 'price' : 'mcap';
179
+ }
180
+ /** task-1472: the chart axis a run starts with: config.display.chartAxis, else mcap (also for configs written before it). */
181
+ export function configChartAxis(cfg) {
182
+ const a = cfg?.display?.chartAxis;
183
+ return isChartAxis(a) ? a : DEFAULT_CHART_AXIS;
184
+ }
185
+ /**
186
+ * task-1472: set the chart Y axis (both chart surfaces) and persist it to config.display.chartAxis through the journal
187
+ * (actor user, so `undo` works). `next` absent = flip. Returns the toast text; a save failure keeps it for this run.
188
+ */
189
+ export function setChartAxis(state, next) {
190
+ const axis = next ?? (chartAxisOf(state) === 'mcap' ? 'price' : 'mcap');
191
+ state.chartAxis = axis;
192
+ const what = axis === 'mcap' ? 'chart Y axis: MARKET CAP' : 'chart Y axis: PRICE';
193
+ const ui = state.custom;
194
+ if (!ui)
195
+ return `${what} (not saved: no config on this run)`;
196
+ try {
197
+ const op = ui.config.display ? { op: 'add', path: '/display/chartAxis', value: axis } : { op: 'add', path: '/display', value: { chartAxis: axis } };
198
+ const { config } = applyAndJournal(ui.home, [op], { actor: 'user', why: `A chart axis ${axis}`, now: state.now.getTime() });
199
+ ui.config = config;
200
+ return `${what} · saved (A to switch)`;
201
+ }
202
+ catch (e) {
203
+ return `${what} (not saved: ${e.message})`;
204
+ }
205
+ }
162
206
  export const DISPLAY_USAGE = 'usage: dexterm display [price|mcap] show or set display.priceMode in ~/.dexterm/config.json ($ in the app toggles it)';
163
207
  /** `dexterm display [price|mcap]`: print the saved price display mode, or set it (journaled, actor user). No network. */
164
208
  export function displayCommand(argv, io) {
@@ -171,7 +215,9 @@ export function displayCommand(argv, io) {
171
215
  return 1;
172
216
  }
173
217
  const m = configPriceMode(loaded.config);
174
- io.stdout.write(`display.priceMode = ${m}${m === 'mcap' ? ` (tables show MCAP; charts: ${MCAP_AXIS_LABEL}; tokens without a supply fall back to price)` : ' (USD price per token)'}\n`);
218
+ io.stdout.write(`display.priceMode = ${m}${m === 'mcap' ? ' (tables show MCAP; tokens without a supply fall back to price)' : ' (USD price per token)'}\n`);
219
+ const a = configChartAxis(loaded.config);
220
+ io.stdout.write(`display.chartAxis = ${a}${a === 'mcap' ? ` (charts: ${MCAP_AXIS_LABEL}; A switches)` : ' (charts plot USD price; A switches)'}\n`);
175
221
  return 0;
176
222
  }
177
223
  if (!isPriceMode(arg)) {
@@ -17,6 +17,7 @@ import { XTokenStore } from '../../data/src/index.js';
17
17
  import { SayTts, voiceDoctor, resolveVoice, WHISPER_MODEL, resolvePiperRuntime, pickPlayer, PIPER_MODEL } from '../../voice/src/index.js';
18
18
  import { loadConfig, dextermHome } from '../../custom/src/index.js';
19
19
  import { kbStore, skillStore } from "./kb-skill-cmd.js";
20
+ import { analystCliOnPath, CODEX_MIN_VERSION, parseEngineChoice, resolveEngine, versionAtLeast } from "./analyst/codex.js";
20
21
  /** The minimum Node that runs dexterm (type-stripping of the .ts entry needs 22.18). */
21
22
  export const MIN_NODE = [22, 18, 0];
22
23
  /** Cheap PATH lookup (no spawn). Windows (task-1423): PATH is `;`-separated and commands carry an extension
@@ -141,6 +142,32 @@ export async function claudeProbe(timeoutMs = 4000, platform = process.platform,
141
142
  }
142
143
  });
143
144
  }
145
+ /**
146
+ * Default codex probe (0.1.5): PATH (spawnable form only), then `codex --version` and `codex login status`. The
147
+ * status command is the user's own CLI reporting on its own login; dexterm never reads the credential itself.
148
+ */
149
+ export async function codexProbe(timeoutMs = 4000, env = process.env) {
150
+ if (!analystCliOnPath('codex', env))
151
+ return { state: 'missing' };
152
+ const { execFile } = await import('node:child_process');
153
+ const run = (args) => new Promise((resolve) => {
154
+ try {
155
+ const child = execFile('codex', args, { timeout: timeoutMs, env }, (err, stdout, stderr) => {
156
+ const code = err ? (typeof err.code === 'number' ? err.code : null) : 0;
157
+ resolve({ code, out: `${stdout ?? ''}${stderr ?? ''}` });
158
+ });
159
+ child.on('error', () => resolve({ code: null, out: '' }));
160
+ }
161
+ catch {
162
+ resolve({ code: null, out: '' });
163
+ }
164
+ });
165
+ const v = await run(['--version']);
166
+ const version = /(\d+\.\d+\.\d+[\w.-]*)/.exec(v.out)?.[1] ?? null;
167
+ const st = await run(['login', 'status']);
168
+ const loggedIn = st.code === 0 ? !/not logged in/i.test(st.out) : st.code === null ? null : false;
169
+ return { state: 'present', version, loggedIn };
170
+ }
144
171
  /** Default writable check: create the directory, write and remove a temp file. Returns an error message or null. */
145
172
  export function writableProbe(dir) {
146
173
  try {
@@ -162,6 +189,8 @@ export function defaultDeps(env = process.env, isTTY = process.stdout.isTTY ===
162
189
  nodeVersion: process.versions.node,
163
190
  platform: process.platform,
164
191
  claude: () => claudeProbe(),
192
+ codex: () => codexProbe(4000, env),
193
+ analystEngine: env.DEXTERM_ANALYST_ENGINE ?? loadConfig(dextermHome(env)).config.analyst?.engine ?? null,
165
194
  probe: (url) => livingProbe(url),
166
195
  checkWritable: writableProbe,
167
196
  offline: env.DEXTERM_OFFLINE === '1' || env.DEXTERM_OFFLINE === 'true',
@@ -230,11 +259,20 @@ export async function runDoctor(deps) {
230
259
  }
231
260
  // 3. Claude CLI (the analyst). Not required to browse; required for the `a` analyst.
232
261
  const claude = await deps.claude();
233
- if (claude === 'missing') {
262
+ // 0.1.5: codex is the optional second engine; a missing claude is fine when codex can run the analyst.
263
+ const codex = deps.codex ? await deps.codex() : null;
264
+ const codexUsable = codex?.state === 'present';
265
+ if (claude === 'missing' && codexUsable) {
266
+ checks.push({
267
+ name: 'Claude CLI', status: 'pass',
268
+ detail: 'not found on PATH (optional now): the analyst runs on your codex CLI instead',
269
+ });
270
+ }
271
+ else if (claude === 'missing') {
234
272
  checks.push({
235
273
  name: 'Claude CLI', status: 'warn',
236
274
  detail: 'not found on PATH; the analyst (a) is disabled, the rest of dexterm works',
237
- fix: 'Install Claude Code (https://claude.com/claude-code) and run `claude` once to log in.',
275
+ fix: 'Install Claude Code (https://claude.com/claude-code) and run `claude` once to log in, or use Codex: `npm i -g @openai/codex && codex login`.',
238
276
  });
239
277
  }
240
278
  else {
@@ -243,6 +281,24 @@ export async function runDoctor(deps) {
243
281
  detail: 'found on PATH (the analyst runs your local `claude`; log in once with `claude` if the analyst errors)',
244
282
  });
245
283
  }
284
+ // 3b. Codex CLI (0.1.5): optional second analyst engine, the user's own codex login. Read-only lock needs >= CODEX_MIN_VERSION.
285
+ if (codex) {
286
+ const choice = parseEngineChoice(deps.analystEngine) ?? 'auto';
287
+ const engine = resolveEngine(choice, (b) => b === 'claude' ? claude !== 'missing' : codexUsable);
288
+ const eng = `analyst engine: ${engine}${choice === 'auto' ? ' (auto)' : ''}`;
289
+ if (codex.state === 'missing') {
290
+ checks.push({ name: 'Codex CLI', status: engine === 'codex' ? 'warn' : 'pass', detail: `not installed (optional second analyst engine) · ${eng}`, ...(engine === 'codex' ? { fix: 'npm i -g @openai/codex && codex login, or set analyst.engine to claude' } : {}) });
291
+ }
292
+ else if (codex.version && !versionAtLeast(codex.version, CODEX_MIN_VERSION)) {
293
+ checks.push({ name: 'Codex CLI', status: 'warn', detail: `codex-cli ${codex.version} is older than ${CODEX_MIN_VERSION}; the read-only analyst lock needs ${CODEX_MIN_VERSION}+ · ${eng}`, fix: 'npm i -g @openai/codex@latest' });
294
+ }
295
+ else if (codex.loggedIn === false) {
296
+ checks.push({ name: 'Codex CLI', status: 'warn', detail: `found${codex.version ? ` (codex-cli ${codex.version})` : ''}, not logged in · ${eng}`, fix: 'codex login' });
297
+ }
298
+ else {
299
+ checks.push({ name: 'Codex CLI', status: 'pass', detail: `found${codex.version ? ` (codex-cli ${codex.version})` : ''}${codex.loggedIn ? ', logged in' : ''}; runs the analyst read-only on your own codex login (/engine codex) · ${eng}` });
300
+ }
301
+ }
246
302
  // 4b. Login with X (optional, task-1357): the user's own X app + login unlock X posts in the SOCIAL panel / TRENDING.
247
303
  {
248
304
  const xs = new XTokenStore({ env: deps.env });
@@ -0,0 +1,115 @@
1
+ import { ageShort, creatorOf, devHistoryLine, queryDevHistory } from '../../../recorder/src/index.js';
2
+ export const DEV_TTL_MS = 60_000;
3
+ /** Short creator/funder address for the panel (never a full address in a 40-column cell). */
4
+ export const shortAddr = (a) => (a ? `${a.slice(0, 4)}…${a.slice(-4)}` : '—');
5
+ export class DevHistoryBook {
6
+ cache = new Map();
7
+ db;
8
+ now;
9
+ ttlMs;
10
+ constructor(db, now = () => Date.now(), ttlMs = DEV_TTL_MS) {
11
+ this.db = db;
12
+ this.now = now;
13
+ this.ttlMs = ttlMs;
14
+ }
15
+ /**
16
+ * The entry for `mint` (cached for `ttlMs`). `creatorHint` is the dev wallet a source named on screen (Jupiter
17
+ * `dev`) for a coin the recorder has not linked yet; `intel` is the coin's settled wallet-intel scan, if any, and
18
+ * `intelAt` when that scan settled (the linked row's age).
19
+ */
20
+ forMint(mint, creatorHint, intel, intelAt) {
21
+ const now = this.now();
22
+ const hit = this.cache.get(mint);
23
+ if (hit && now - hit.at < this.ttlMs && (hit.creator || !creatorHint) && (hit.linked || !intel))
24
+ return hit;
25
+ const db = this.db() ?? null;
26
+ let creator = db ? creatorOf(db, mint) : null;
27
+ if (!creator && creatorHint && /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(creatorHint))
28
+ creator = { mint, creator: creatorHint, source: 'jupiter-tokens', firstT: now, createdAt: null };
29
+ let report = null;
30
+ let linked = null;
31
+ if (creator && db) {
32
+ try {
33
+ report = queryDevHistory(db, { creator: creator.creator, mint, asOf: now });
34
+ }
35
+ catch {
36
+ report = null;
37
+ }
38
+ }
39
+ if (creator && intel)
40
+ linked = linkedFrom(intel, creator.creator, db, intelAt ?? null);
41
+ const line = !db ? 'unknown: the history recorder is off (--no-recorder / DEXTERM_RECORDER=0)'
42
+ : !creator ? 'unknown: no source named the creator yet'
43
+ : report ? devHistoryLine(report) : 'unknown: dev history unreadable';
44
+ const e = { mint, creator, report, linked, line, at: now };
45
+ this.cache.set(mint, e);
46
+ if (this.cache.size > 2000)
47
+ this.cache.delete(this.cache.keys().next().value);
48
+ return e;
49
+ }
50
+ /** The `devrugs` rule field: labelled prior rugs by this coin's creator; null = unknown (clauses on it are false). */
51
+ rugsFor(mint) {
52
+ const e = this.forMint(mint);
53
+ return e.report && e.report.labelled.n > 0 ? e.report.labelled.rug : null;
54
+ }
55
+ }
56
+ /** Linked wallets from a settled wallet-intel scan: the creator's funder and any multi-wallet cluster it sits in. */
57
+ export function linkedFrom(a, creator, db, at = null) {
58
+ const h = a.holders.find((x) => x.owner === creator);
59
+ const fundedBy = h?.funder?.address ?? a.funding[creator]?.funder ?? null;
60
+ const c = a.clusters.find((x) => x.members.includes(creator) && x.members.length > 1) ?? null;
61
+ let linkedCreators = 0, linkedRugs = 0, linkedNewestT = null;
62
+ if (c && db) {
63
+ try {
64
+ const others = c.members.filter((m) => m !== creator).slice(0, 50);
65
+ const launched = db.prepare('SELECT COUNT(*) AS n FROM creators WHERE creator = ?');
66
+ for (const w of others) {
67
+ if (Number(launched.get(w).n) <= 0)
68
+ continue;
69
+ linkedCreators++;
70
+ const r = queryDevHistory(db, { creator: w, devExit: false });
71
+ linkedRugs += r.labelled.rug;
72
+ if (r.labelled.newestT !== null)
73
+ linkedNewestT = Math.max(linkedNewestT ?? 0, r.labelled.newestT);
74
+ }
75
+ }
76
+ catch { /* an old DB without the table: nothing linked */ }
77
+ }
78
+ return { fundedBy, cluster: c ? { name: c.name, kind: c.kind, members: c.members.length, pct: c.pct } : null, linkedCreators, linkedRugs, linkedNewestT, source: h || c || a.funding[creator] ? 'wallet-intel scan' : null, at };
79
+ }
80
+ /** Short source tags for a panel cell: `rec` = dexterm's recorder, `scan` = the wallet-intel scan; creator links by feed. */
81
+ const LINK_TAG = { 'jupiter-tokens': 'jup', pumpportal: 'pump', rugcheck: 'rc' };
82
+ const linkTag = (src) => (src ? LINK_TAG[src] ?? src : '?');
83
+ const ageAt = (t, now) => ageShort(t === null || t === undefined ? null : Math.max(0, Math.round((now - t) / 1000)));
84
+ /**
85
+ * Rows for the token screen's DEV block: [label, value, level]. Every value names its source, sample and age (or
86
+ * says unknown): `rec` = dexterm's recorder, `scan` = the wallet-intel scan, `dev:jup` = which feed named the creator.
87
+ * Ages are measured from `now` (default: when the entry was read): the newest label / sighting / exit / scan behind it.
88
+ */
89
+ export function devRows(e, now = e.at) {
90
+ const rows = [];
91
+ const r = e.report;
92
+ const rugLevel = () => !r || r.labelled.n === 0 ? 'na' : r.serialRugger ? 'bad' : r.labelled.rug > 0 || (r.badRate ?? 0) >= 0.5 ? 'warn' : 'ok';
93
+ rows.push(['Dev rugs', r && r.labelled.n ? `${r.labelled.rug}/${r.labelled.n} · n=${r.labelled.n} · rec ${ageAt(r.labelled.newestT, now)}` : 'unknown', rugLevel()]);
94
+ rows.push(['Dev launches', r && r.status === 'ok' ? `n=${r.launches.n} · rec ${ageAt(r.launches.newestT, now)} · dev:${linkTag(e.creator?.source)}` : 'unknown', 'na']);
95
+ if (r && r.devExit.n)
96
+ rows.push(['Dev sold bag', `med ${Math.round(r.devExit.medianMin)}m · n=${r.devExit.n} · rec ${ageAt(r.devExit.newestT, now)}`, r.devExit.medianMin <= 60 ? 'warn' : 'ok']);
97
+ const l = e.linked;
98
+ if (l && (l.fundedBy || l.cluster)) {
99
+ const scan = `scan ${ageAt(l.at, now)}`;
100
+ rows.push(['Dev linked', l.cluster ? `${l.cluster.kind} ${l.cluster.members}w · ${scan}` : `funded by ${shortAddr(l.fundedBy)} · ${scan}`, 'na']);
101
+ if (l.linkedCreators)
102
+ rows.push(['Linked dev rugs', `${l.linkedRugs} · n=${l.linkedCreators} devs · rec ${ageAt(l.linkedNewestT, now)}`, l.linkedRugs > 0 ? 'bad' : 'na']);
103
+ }
104
+ return rows;
105
+ }
106
+ /** The app's one book (created on first use), reading the live recorder's DB. Null when there is no recorder. */
107
+ export function devBookOf(state) {
108
+ if (!state.live?.recorder)
109
+ return null;
110
+ if (!state.devBook) {
111
+ const live = state.live;
112
+ state.devBook = new DevHistoryBook(() => live?.recorder?.database ?? null);
113
+ }
114
+ return state.devBook;
115
+ }
@@ -332,13 +332,17 @@ export class LiveContext {
332
332
  }
333
333
  catch { /* keep the last value */ }
334
334
  }
335
- /** The fast set = the open token first, then the watchlist (the fast lane caps and drops the rest). */
335
+ /** task-1473: a coin the followed agent holds (PAPER): kept in the fast set so its mark survives list changes. */
336
+ heldMint = null;
337
+ /** The fast set = the open token first, then a followed agent's held coin, then the watchlist (the fast lane caps and drops the rest). */
336
338
  syncFastSet() {
337
339
  if (!this.fastLane)
338
340
  return;
339
341
  const mints = [];
340
342
  if (this.token)
341
343
  mints.push(this.token.mint);
344
+ if (this.heldMint && !mints.includes(this.heldMint))
345
+ mints.push(this.heldMint);
342
346
  for (const e of this.watchStore.list())
343
347
  if (!mints.includes(e.mint))
344
348
  mints.push(e.mint);
@@ -20,6 +20,8 @@ export class NewPairsLive {
20
20
  dl;
21
21
  now;
22
22
  onChange;
23
+ /** 0.1.5 dev history: called once per row when its creator becomes known (the recorder stores the link). */
24
+ onCreator;
23
25
  limit;
24
26
  polls;
25
27
  rows = new Map();
@@ -84,6 +86,12 @@ export class NewPairsLive {
84
86
  this.mode = 'replay';
85
87
  return this.client.replay(rec, speed);
86
88
  }
89
+ tellCreator(mint, creator, source, createdAt) {
90
+ try {
91
+ this.onCreator?.(mint, creator, source, createdAt);
92
+ }
93
+ catch { /* the recorder must never break the feed */ }
94
+ }
87
95
  addCreate(c) {
88
96
  const rowAt = this.now();
89
97
  const existing = this.rows.get(c.mint);
@@ -105,6 +113,8 @@ export class NewPairsLive {
105
113
  }
106
114
  else {
107
115
  this.rows.set(c.mint, row);
116
+ if (row.creator)
117
+ this.tellCreator(c.mint, row.creator, c.creator ? 'pumpportal' : 'rugcheck', row.createdAt);
108
118
  this.latencies.push(row.latencyMs);
109
119
  if (this.latencies.length > 500)
110
120
  this.latencies.shift();
@@ -227,6 +237,8 @@ export class NewPairsLive {
227
237
  const row = this.rows.get(t.mint);
228
238
  if (!row)
229
239
  continue;
240
+ if (!row.creator && t.creator)
241
+ this.tellCreator(t.mint, t.creator, 'rugcheck', row.createdAt);
230
242
  row.creator = row.creator ?? t.creator;
231
243
  if (t.mintAuthority !== null)
232
244
  row.mintAuth = t.mintAuthority;
@@ -125,7 +125,7 @@ export function renderNewPairsLive(c, state, live) {
125
125
  } },
126
126
  ];
127
127
  if (rows.length)
128
- table(c, box.x + 1, box.y, box.w - 2, box.h, { columns: cols, rows, selected: sel, scroll: 0, bg: box.bg });
128
+ table(c, box.x + 1, box.y, box.w - 2, box.h, { columns: cols, rows, selected: sel, scroll: 0, bg: box.bg, rowKey: (r) => r.mint });
129
129
  else {
130
130
  const s = st.stream;
131
131
  const msg = st.mode === 'off' ? 'push feed off · r to poll jup recent' : s?.state === 'open' ? `connected to pumpportal · waiting for the first create (≈15/min measured)` : `pumpportal ${s?.state ?? 'connecting'}${s?.lastError ? ` · ${s.lastError}` : ''} · polling jup recent`;
@@ -14,10 +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";
19
+ import { devBookOf, devRows } from "../dev-history.js";
18
20
  import { EX_POOL_TAGS } from '../../../../data/src/index.js';
19
21
  import { layoutFor } from "../../custom.js";
20
- import { chartMode, priceModeOf, mcapAt, positionPriceCells } from "../../display.js";
22
+ import { chartMode, chartTitle, priceModeOf, chartAxisOf, mcapAt, positionPriceCells } from "../../display.js";
21
23
  const dash = '—';
22
24
  const EX_POOL_TAG_SET = EX_POOL_TAGS;
23
25
  /** task-1367 holder-class badge colours: venues/escrows accent, the creator/linked warn, the rest muted. */
@@ -86,13 +88,13 @@ export function renderTokenLive(c, state, v) {
86
88
  const shownPx = livePx ?? v.price;
87
89
  // task-1366: mcap mode leads with the market cap (the live price × the polled supply ratio) and keeps the price beside it;
88
90
  // without a supply it shows the price and says so.
89
- const cm = chartMode(priceModeOf(state), v.price, v.mcap);
90
- if (cm.kind === 'mcap' && shownPx !== null) {
91
- 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 });
92
94
  x = c.text(x + 1, y, `(${fmtPrice(shownPx)})`, { fg: t.dim, bg: t.bg });
93
95
  }
94
96
  else
95
- 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 });
96
98
  if (state.live?.fastLane)
97
99
  x = badge(c, x + 1, y, liveBadge, liveBadge === 'LIVE' ? t.up : t.muted, { on: t.bg });
98
100
  const deltas = [['5m', v.chg.m5], ['1h', v.chg.h1], ['6h', v.chg.h6], ['24h', v.chg.h24]];
@@ -130,7 +132,9 @@ export function renderTokenLive(c, state, v) {
130
132
  const chartH = Math.max(10, Math.floor((bottom - y - posH) * lay.chartFrac));
131
133
  const cp = v.panels.candles;
132
134
  const chartCap = clip(cp.state === 'ok' || cp.fetchedAt !== null ? `ohlcv: geckoterminal${cp.stale || cp.state === 'error' ? ' · stale' : ''}` : panelCaption(cp, now, 'ohlcv unavailable'), 40);
133
- 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 });
134
138
  // timeframe tabs sit on the top border, left of the caption (same spot as the fixture screen)
135
139
  let tx = cb.x + cb.w - 2 - (chartCap.length + 4) - INTERVALS.reduce((s, f) => s + f.length + 1, 0);
136
140
  for (const f of INTERVALS)
@@ -141,9 +145,12 @@ export function renderTokenLive(c, state, v) {
141
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 });
142
146
  y += chartH;
143
147
  if (prow && src) {
144
- 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);
145
149
  y += posH;
146
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;
147
154
  // ---- task-1330: the SOCIAL panel (`s`) replaces the ON-CHAIN / HOLDERS band; the chart + position stay.
148
155
  if (state.tokenSocial) {
149
156
  renderSocialPanel(c, 0, y, mainW, bottom - y, state, { mint: v.mint, symbol: v.symbol, now });
@@ -178,6 +185,14 @@ export function renderTokenLive(c, state, v) {
178
185
  const top10Rows = tags.top10
179
186
  ? [['Top 10 ex-pool', `${tags.top10.exPool.toFixed(1)}%`, t10lvl(tags.top10.exPool)], ['Top 10 raw', `${tags.top10.raw.toFixed(1)}%`, 'na']]
180
187
  : [['Top 10 holders', ...(v.top10 === null ? na(v.panels.holders.state === 'needs-key' ? 'needs key' : dash) : [`${v.top10.toFixed(1)}%${v.panels.holders.state === 'ok' ? '' : ' (jup)'}`, t10lvl(v.top10)])]];
188
+ // 0.1.5 dev history (task-1425): the creator's recorded track record from the local recorder (keyless). The rugs
189
+ // row sits with the other rug signals; launches / bag-exit / linked wallets follow the safety list.
190
+ const book = devBookOf(state);
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) : [];
192
+ const devHead = devAll.slice(0, 1);
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']);
181
196
  const sniperRow = [cells.snipers.text, cells.snipers.level];
182
197
  const bundleRow = [cells.bundle.text, cells.bundle.level];
183
198
  const rows = [
@@ -189,10 +204,12 @@ export function renderTokenLive(c, state, v) {
189
204
  ['Mint authority', ...(v.mintAuth === null ? na(dash) : [v.mintAuth ? 'ENABLED' : 'revoked', v.mintAuth ? 'bad' : 'ok'])],
190
205
  ['Freeze authority', ...(v.freezeAuth === null ? na(dash) : [v.freezeAuth ? 'ENABLED' : 'revoked', v.freezeAuth ? 'bad' : 'ok'])],
191
206
  ['Dev history', ...(v.devMints === null ? needsHistory : [`${v.devMints} mints by dev (jup)`, v.devMints > 3 ? 'warn' : 'ok'])],
207
+ ...devHead,
192
208
  ['RugCheck score', ...(v.rug === null || v.rug.score === null ? na(v.panels.rugcheck.state === 'error' ? `unavailable: ${v.panels.rugcheck.reason ?? ''}`.trim() : dash) : [`${Math.round(v.rug.score)}/100`, v.rug.score < 30 ? 'ok' : v.rug.score < 60 ? 'warn' : 'bad'])],
193
209
  ['GoPlus', ...goplusRow(v)],
210
+ ...devTail,
194
211
  ];
195
- for (const [label, val0, lvl] of rows) {
212
+ for (const [label, val0, lvl] of fitRows(rows, sb.y + sb.h - sy)) {
196
213
  if (sy >= sb.y + sb.h)
197
214
  break;
198
215
  const color = lvl === 'ok' ? t.up : lvl === 'warn' ? t.warn : lvl === 'bad' ? t.down : t.dim;
@@ -318,8 +335,15 @@ export function renderTokenLive(c, state, v) {
318
335
  }
319
336
  }
320
337
  }
321
- if (showAi)
322
- 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
+ }
323
347
  const watched = state.live?.watch?.has(v.mint) ? 'unwatch' : 'watch';
324
348
  // task-1311: `i` asks claude about this token (chat), `a` runs the paid dossier on demand, `y`
325
349
  // copies the full contract address.
@@ -393,6 +417,21 @@ function positionPanel(c, x, y, w, r, kind, mode = 'price', supply = null) {
393
417
  function clip(s, max) {
394
418
  return s.length > max ? s.slice(0, Math.max(1, max - 1)) + '…' : s;
395
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
+ }
396
435
  /** Token amounts up to trillions (fmtNum stops at B). */
397
436
  function fmtAmt(n) {
398
437
  if (n >= 1e12)
@@ -435,7 +474,8 @@ function analystPlaceholder(c, x, y, w, h, v, state) {
435
474
  // task-1311 (QA M5): the pre-run state. Opening a token no longer spends a paid dossier; this panel
436
475
  // waits until you ask for one with `a` (dossier) or type a question with `i`. No fake verdicts.
437
476
  const t = c.theme;
438
- const box = c.box(x, y, w, h, { title: '✦ ANALYST · claude', titleFg: t.ai, caption: 'read-only · local claude · on demand', captionFg: t.dim, border: mix(t.border, t.ai, 0.35) });
477
+ const eng = state.analystEngine ?? 'claude'; // 0.1.5: the user's own CLI that will answer
478
+ const box = c.box(x, y, w, h, { title: `✦ ANALYST · ${eng}`, titleFg: t.ai, caption: `read-only · local ${eng} · on demand`, captionFg: t.dim, border: mix(t.border, t.ai, 0.35) });
439
479
  const bg = box.bg;
440
480
  const inner = box.w - 2;
441
481
  let cy = box.y;
@@ -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;
@@ -266,6 +266,7 @@ export class TokenLive {
266
266
  v.top10 = audit?.top10Pct ?? v.top10;
267
267
  v.dev = audit?.devPct ?? null;
268
268
  v.devMints = audit?.devMints ?? null;
269
+ v.devAddress = audit?.devAddress ?? v.devAddress ?? null;
269
270
  v.sources = [...new Set([...v.sources, ...r.sources])];
270
271
  const SHORT = { dexscreener: 'ds', 'jupiter-tokens': 'jup', 'jupiter-price': 'jup-price' };
271
272
  this.ok('market', r.sources.map((x) => SHORT[x] ?? x).join('+'), pair.fetchedAt, r.stale);
@@ -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);