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
@@ -12,6 +12,7 @@
12
12
  <div id="hud" class="hud">
13
13
  <div id="drag" class="drag"><span class="mark">✦</span><span id="title" class="title">dexterm · WATCH THE AGENT COOK</span><span id="status" class="status">connecting…</span><button id="close" class="close" type="button" aria-label="close">✕</button></div>
14
14
  <div class="stage"><canvas id="rain"></canvas><div class="body">
15
+ <div id="story" class="spec-story"></div>
15
16
  <div id="screen" class="spec-screen"></div>
16
17
  <div id="agents" class="spec-agents"></div>
17
18
  <div class="spec-activity-head">agent activity</div>
@@ -14,8 +14,77 @@
14
14
 
15
15
  let vm = null;
16
16
 
17
+ const pctS = (p) => (p == null ? '—' : `${p >= 0 ? '+' : '−'}${Math.abs(p * 100).toFixed(1)}%`);
18
+ const px = (n) => (n == null ? '—' : n >= 1 ? `$${n.toFixed(4)}` : `$${Number(n.toPrecision(3))}`);
19
+ const fmtMs = (ms) => (ms == null ? '·' : ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`);
20
+ const CHECKS = ['price', 'tape', 'chart', 'holders', 'rug', 'dev', 'larp', 'verdict'];
21
+
22
+ // task-1466 spectator v2: the agent story above the activity list (textContent only; strings redacted TUI-side).
23
+ function renderStory(st) {
24
+ const host = $('story'); host.textContent = '';
25
+ if (!st) return;
26
+ const rail = el('div', 'spec-rail');
27
+ const heart = el('span', 'spec-heart' + (st.control === 'killed' ? ' dead' : st.control !== 'running' ? ' still' : ''));
28
+ if (st.bpm > 0) heart.style.animationDuration = `${(60 / st.bpm).toFixed(2)}s`;
29
+ rail.appendChild(heart);
30
+ rail.appendChild(el('span', 'spec-bpm', st.control === 'killed' ? '— bpm' : `${st.bpm} bpm`));
31
+ const ai = st.rail.indexOf(st.phase);
32
+ st.rail.forEach((p, i) => {
33
+ if (i) rail.appendChild(el('span', 'spec-sep', '─'));
34
+ const on = i === ai && st.control !== 'killed';
35
+ rail.appendChild(el('span', 'spec-phase p-' + p + (on ? ' on' : i < ai ? ' done' : ''), (on ? '▸' : '') + p.toUpperCase() + (on && st.sub ? ` · ${st.sub}` : '')));
36
+ });
37
+ if (st.control !== 'running') rail.appendChild(el('span', 'spec-ctlstate ' + st.control, st.control === 'killed' ? '■ KILLED' : st.control === 'paused' ? '❚❚ PAUSED' : 'YOU ARE DRIVING'));
38
+ rail.appendChild(el('span', 'spec-paper', 'PAPER'));
39
+ host.appendChild(rail);
40
+ const th = (st.thoughts || []).slice().reverse();
41
+ const agentT = th.find((x) => x.source === 'agent');
42
+ const ours = th.find((x) => x.source === 'dexterm');
43
+ const pick = agentT && (!ours || agentT.t >= ours.t - 1500 || Date.now() - agentT.t < 9000) ? agentT : ours || agentT;
44
+ const rib = el('div', 'spec-ribbon' + (pick && pick.source === 'dexterm' ? ' ours' : ''));
45
+ if (pick) { if (pick.source === 'agent') rib.appendChild(el('span', 'spec-ribbon-who', '❯')); rib.appendChild(el('span', '', pick.text)); }
46
+ else rib.appendChild(el('span', 'spec-dim', 'the agent has not said anything yet'));
47
+ host.appendChild(rib);
48
+ const tl = el('div', 'spec-timeline');
49
+ (st.nodes || []).slice(-14).forEach((n, i) => {
50
+ if (i) tl.appendChild(el('span', 'spec-link', '──'));
51
+ const node = el('div', 'spec-node' + (n.paper ? ' paper' : ''));
52
+ node.appendChild(el('span', 'spec-node-l', `${n.status === 'refused' ? '✗' : n.paper ? '■' : '●'} ${n.label}${n.count > 1 ? ` ×${n.count}` : ''}`));
53
+ node.appendChild(el('span', 'spec-chip ' + n.status, n.status === 'refused' ? 'refused' : fmtMs(n.ms)));
54
+ tl.appendChild(node);
55
+ });
56
+ host.appendChild(tl);
57
+ const cards = el('div', 'spec-cards');
58
+ if (st.focus) {
59
+ const c = el('div', 'spec-card');
60
+ c.appendChild(el('span', 'spec-card-h', `VETTING $${st.focus.symbol}`));
61
+ const row = el('div');
62
+ for (const k of CHECKS) row.appendChild(el('span', 'spec-check' + (st.focus.checks.includes(k) ? ' ok' : ''), `${st.focus.checks.includes(k) ? '✓' : '·'}${k}`));
63
+ c.appendChild(row);
64
+ if (st.focus.larpFlags != null) c.appendChild(el('div', 'spec-kv', `LARP evidence: ${st.focus.larpFlags} flag${st.focus.larpFlags === 1 ? '' : 's'} (not a verdict)`));
65
+ if (st.focus.verdict) c.appendChild(el('div', 'spec-kv', `verdict ${st.focus.verdict}${st.focus.rug != null ? ` · rug heuristic ${Math.round(st.focus.rug * 100)}%` : ''}`));
66
+ cards.appendChild(c);
67
+ }
68
+ if (st.arc) {
69
+ const a = st.arc;
70
+ const c = el('div', 'spec-card trade');
71
+ c.appendChild(el('span', 'spec-card-h', `PAPER · $${a.symbol} · ${a.stage === 'closed' ? 'RESULT' : a.stage === 'open' ? 'HOLDING' : a.stage === 'refused' ? 'REFUSED' : 'TICKET'}`));
72
+ const big = a.stage === 'closed' ? a.resultPct : a.pnlPct;
73
+ if (big != null) c.appendChild(el('div', 'spec-big ' + (big >= 0 ? 'up' : 'down'), pctS(big)));
74
+ const kv = el('div', 'spec-kv');
75
+ kv.textContent = [a.sizeUsd != null ? `size $${a.sizeUsd.toFixed(2)}` : null, a.entryUsd != null ? `entry ${px(a.entryUsd)}` : null, a.markUsd != null && a.stage !== 'closed' ? `mark ${px(a.markUsd)}` : null,
76
+ a.peakPct != null ? `peak ${pctS(a.peakPct)}` : null, a.exits ? `exits ${a.exits}` : null, a.realizedSol != null ? `realized ${a.realizedSol >= 0 ? '+' : ''}${a.realizedSol.toFixed(4)} SOL` : null,
77
+ a.refusal ? `refused: ${a.refusal}` : null].filter(Boolean).join(' · ');
78
+ c.appendChild(kv);
79
+ c.appendChild(el('div', 'spec-dim', 'simulated paper trade · not a result'));
80
+ cards.appendChild(c);
81
+ }
82
+ if (cards.childNodes.length) host.appendChild(cards);
83
+ }
84
+
17
85
  function render() {
18
86
  if (!vm) return;
87
+ renderStory(vm.story || null);
19
88
 
20
89
  const scr = $('screen'); scr.textContent = '';
21
90
  const s = vm.screen || {};
@@ -28,4 +28,6 @@ export const CHART_INDICATORS = ['ema9', 'ema21', 'vwap', 'volma', 'volume', 'ta
28
28
  export const CHART_STYLES = ['candles', 'line', 'area'];
29
29
  /** Price-scale mode: auto (normal, autoscaled), logarithmic, percent (from the first visible bar). */
30
30
  export const CHART_SCALES = ['auto', 'log', 'percent'];
31
+ /** task-1472: what the chart's Y axis shows (the popup's MCAP | PRICE control / `A` ask the terminal to switch). */
32
+ export const CHART_AXES = ['mcap', 'price'];
31
33
  export const PROTOCOL_VERSION = 'dexterm-popup/1';
@@ -26,7 +26,7 @@ import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
26
26
  import { readFileSync, existsSync } from 'node:fs';
27
27
  import { fileURLToPath } from 'node:url';
28
28
  import path from 'node:path';
29
- import { PROTOCOL_VERSION, CHART_INTERVALS, CHART_INDICATORS, CHART_STYLES, CHART_SCALES, WIDGET_TOPICS, WIDGET_HTML_MAX } from "./protocol.js";
29
+ import { PROTOCOL_VERSION, CHART_INTERVALS, CHART_INDICATORS, CHART_STYLES, CHART_SCALES, CHART_AXES, WIDGET_TOPICS, WIDGET_HTML_MAX } from "./protocol.js";
30
30
  import { auditWidgetHtml, inlineHashes, normalizeWidgetHtml } from "./widget-html.js";
31
31
  const here = path.dirname(fileURLToPath(import.meta.url));
32
32
  const PAGES_DIR = path.join(here, 'pages');
@@ -48,6 +48,8 @@ export const POPUP_EVENTS = new Set([
48
48
  'event:popup.close-request', 'event:popup.focus', 'event:popup.drag',
49
49
  // task-1325 chart v2 requests (payloads validated by validateEventPayload below)
50
50
  'event:chart.interval', 'event:chart.indicator', 'event:chart.style', 'event:chart.scale', 'event:chart.painted',
51
+ // task-1472: the chart's Y axis (market cap ⇄ price): a display preference the terminal saves, nothing else
52
+ 'event:chart.axis',
51
53
  // task-1345 agent roster: per-agent pause / kick, by agent name (the only writes this popup can send)
52
54
  'event:roster.pause', 'event:roster.resume', 'event:roster.kick',
53
55
  // task-1355 order plans: cancel a plan by id (the ONLY write the plans popup can send — never arm/cap/sign)
@@ -94,6 +96,8 @@ export function validateEventPayload(name, payload) {
94
96
  return oneOf(CHART_STYLES, p.style) ? { style: p.style } : null;
95
97
  case 'chart.scale':
96
98
  return oneOf(CHART_SCALES, p.scale) ? { scale: p.scale } : null;
99
+ case 'chart.axis':
100
+ return oneOf(CHART_AXES, p.axis) ? { axis: p.axis } : null;
97
101
  case 'chart.painted':
98
102
  return (p.kind === 'first' || p.kind === 'data') && finiteIn(p.ms, 0, 600_000) && finiteIn(p.sentAt, 0, 1e15)
99
103
  ? { kind: p.kind, ms: p.ms, sentAt: p.sentAt } : null;
@@ -0,0 +1,133 @@
1
+ // DEV HISTORY (0.1.5, task-1425): "has this deployer rugged before?" from dexterm's OWN recorded history, keyless.
2
+ // The creator links come from the `creators` table (a keyless source naming the dev when dexterm saw the coin), the
3
+ // outcomes from the recorder's code-only labeler (labels.ts: keyless-proxy tier), the dev-exit timing from the
4
+ // recorded `dev_pct` series. Every number carries its source, sample size and age; no recorded prior launch is
5
+ // `unknown`, never "clean". It is a risk flag from observed history, never a certificate.
6
+ //
7
+ // Point in time: with `asOf`, only links recorded before it and labels already known by then count (a label is known
8
+ // once its observation t1, or t0 + horizon, has passed). The backtest uses this so a coin is never judged on the
9
+ // future (the same leakage rule as export.ts).
10
+ /** Label horizons in preference order: the 72 h label when known, else the longest one known. */
11
+ const PREF_HORIZONS = [259200, 86400, 21600, 7200, 3600];
12
+ /** Dev-exit rule: the dev held more than this % of supply, then this little. */
13
+ export const DEV_EXIT = { heldPct: 0.5, exitPct: 0.1 };
14
+ /** Prior rugs at/over this many = the `serial_rugger` flag (same threshold as the token-dossier pack). */
15
+ export const SERIAL_RUGGER_MIN = 2;
16
+ export const DEV_HISTORY_SOURCE = 'dexterm recorder (keyless creator links + code-only outcome labels)';
17
+ const median = (xs) => {
18
+ if (!xs.length)
19
+ return null;
20
+ const s = [...xs].sort((a, b) => a - b);
21
+ const m = Math.floor(s.length / 2);
22
+ return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
23
+ };
24
+ /** The creator dexterm recorded for `mint`, or null. */
25
+ export function creatorOf(db, mint) {
26
+ try {
27
+ const r = db.prepare('SELECT mint, creator, source, first_t, created_at FROM creators WHERE mint = ?').get(mint);
28
+ return r ? { mint: r.mint, creator: r.creator, source: r.source, firstT: Number(r.first_t), createdAt: r.created_at === null ? null : Number(r.created_at) } : null;
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ /** The creator's recorded track record. Never throws: a DB without the tables reads as `unknown`. */
35
+ export function queryDevHistory(db, q) {
36
+ const asOf = q.asOf ?? Date.now();
37
+ const empty = {
38
+ creator: q.creator, status: 'unknown', asOf, source: DEV_HISTORY_SOURCE,
39
+ launches: { n: 0, newestT: null, ageSec: null },
40
+ labelled: { n: 0, rug: 0, dead: 0, fade: 0, flat: 0, runner: 0, horizon: '72h preferred', tier: 'keyless-proxy', newestT: null, ageSec: null },
41
+ rugRate: null, badRate: null, devExit: { n: 0, medianMin: null, within60m: 0, newestT: null, ageSec: null }, serialRugger: false, recent: [],
42
+ };
43
+ let links;
44
+ try {
45
+ links = db.prepare('SELECT mint, first_t, created_at FROM creators WHERE creator = ? AND first_t <= ? AND mint != ? ORDER BY first_t DESC LIMIT ?')
46
+ .all(q.creator, asOf, q.mint ?? '', q.limit ?? 300);
47
+ }
48
+ catch {
49
+ return empty;
50
+ }
51
+ if (!links.length)
52
+ return empty;
53
+ const outStmt = db.prepare("SELECT t0, horizon_sec, t1, label FROM outcomes WHERE mint = ? AND label != 'unobserved' ORDER BY t0 ASC");
54
+ const symStmt = db.prepare('SELECT symbol FROM token_snapshots WHERE mint = ? AND symbol IS NOT NULL ORDER BY t ASC LIMIT 1');
55
+ const devStmt = db.prepare('SELECT t, dev_pct FROM token_snapshots WHERE mint = ? AND dev_pct IS NOT NULL AND t <= ? ORDER BY t ASC LIMIT 400');
56
+ const counts = { rug: 0, dead: 0, fade: 0, flat: 0, runner: 0 };
57
+ let labelledN = 0, labelledNewest = null;
58
+ const exits = [];
59
+ let exitNewest = null;
60
+ const launches = [];
61
+ for (const l of links) {
62
+ const firstT = Number(l.first_t);
63
+ const createdAt = l.created_at === null ? null : Number(l.created_at);
64
+ const rows = outStmt.all(l.mint);
65
+ // the label of the EARLIEST anchor (closest to launch) at the longest horizon already known as of asOf
66
+ const known = rows.filter((r) => (r.t1 !== null ? Number(r.t1) : Number(r.t0) + Number(r.horizon_sec) * 1000) <= asOf);
67
+ let label = null, horizonSec = null, knownAt = null;
68
+ if (known.length) {
69
+ const t0 = Math.min(...known.map((r) => Number(r.t0)));
70
+ const at = known.filter((r) => Number(r.t0) === t0);
71
+ for (const h of PREF_HORIZONS) {
72
+ const hit = at.find((r) => Number(r.horizon_sec) === h);
73
+ if (hit) {
74
+ label = hit.label;
75
+ horizonSec = h;
76
+ knownAt = hit.t1 !== null ? Number(hit.t1) : t0 + h * 1000;
77
+ break;
78
+ }
79
+ }
80
+ }
81
+ if (label) {
82
+ labelledN++;
83
+ counts[label]++;
84
+ if (knownAt !== null)
85
+ labelledNewest = Math.max(labelledNewest ?? 0, knownAt);
86
+ }
87
+ let devExitMin = null;
88
+ if (q.devExit !== false) {
89
+ const series = devStmt.all(l.mint, asOf);
90
+ const held = series.findIndex((r) => r.dev_pct > DEV_EXIT.heldPct);
91
+ if (held >= 0) {
92
+ const out = series.slice(held + 1).find((r) => r.dev_pct <= DEV_EXIT.exitPct);
93
+ if (out) {
94
+ const from = createdAt ?? Number(series[held].t);
95
+ devExitMin = Math.max(0, (Number(out.t) - from) / 60_000);
96
+ exits.push(devExitMin);
97
+ exitNewest = Math.max(exitNewest ?? 0, Number(out.t));
98
+ }
99
+ }
100
+ }
101
+ if (launches.length < 10) {
102
+ const sym = symStmt.get(l.mint)?.symbol ?? null;
103
+ launches.push({ mint: l.mint, symbol: sym, firstT, createdAt, label, horizonSec, devExitMin });
104
+ }
105
+ }
106
+ const newestT = Number(links[0].first_t);
107
+ return {
108
+ ...empty,
109
+ status: 'ok',
110
+ launches: { n: links.length, newestT, ageSec: Math.max(0, Math.round((asOf - newestT) / 1000)) },
111
+ labelled: { ...empty.labelled, n: labelledN, ...counts, newestT: labelledNewest, ageSec: labelledNewest === null ? null : Math.max(0, Math.round((asOf - labelledNewest) / 1000)) },
112
+ rugRate: labelledN ? counts.rug / labelledN : null,
113
+ badRate: labelledN ? (counts.rug + counts.dead) / labelledN : null,
114
+ devExit: { n: exits.length, medianMin: median(exits), within60m: exits.filter((m) => m <= 60).length, newestT: exitNewest, ageSec: exitNewest === null ? null : Math.max(0, Math.round((asOf - exitNewest) / 1000)) },
115
+ serialRugger: counts.rug >= SERIAL_RUGGER_MIN,
116
+ recent: launches,
117
+ };
118
+ }
119
+ /** Compact age for a panel cell: `5m`, `3h`, `40d`; `age ?` when unknown. */
120
+ export const ageShort = (sec) => sec === null ? 'age ?' : sec < 3600 ? `${Math.max(1, Math.round(sec / 60))}m` : sec < 172800 ? `${Math.round(sec / 3600)}h` : `${Math.round(sec / 86400)}d`;
121
+ const ago = (sec) => sec === null ? 'age unknown' : `${ageShort(sec)} ago`;
122
+ /**
123
+ * The one-line summary every surface shows (DEV panel, rug-signal row, MCP note): counts with the sample size and
124
+ * the age, or `unknown` with why. Plain wording: a risk flag from observed history, never "rug confirmed" or "safe".
125
+ */
126
+ export function devHistoryLine(r) {
127
+ if (r.status === 'unknown')
128
+ return 'unknown: no prior launch by this wallet in dexterm\'s record';
129
+ const L = r.labelled;
130
+ if (!L.n)
131
+ return `${r.launches.n} prior launch${r.launches.n === 1 ? '' : 'es'} recorded, none labelled yet (newest ${ago(r.launches.ageSec)})`;
132
+ return `rugged ${L.rug} of ${L.n} labelled prior launches${L.dead ? ` (+${L.dead} dead)` : ''} · ${r.launches.n} recorded · labels ${ago(L.ageSec)}`;
133
+ }
@@ -8,6 +8,7 @@ export * from "./export.js";
8
8
  export * from "./open.js";
9
9
  export * from "./verdicts.js";
10
10
  export * from "./schedule.js";
11
+ export * from "./dev-history.js";
11
12
  export { recorderCli, RECORDER_HELP } from "./cli.js";
12
13
  /** `--no-recorder` / `DEXTERM_RECORDER=0` resolution: the recorder is on in live mode unless either says no. */
13
14
  export function recorderEnabled(env, flagOff) {
@@ -30,6 +30,7 @@ export const LABEL_RETRY_MS = 300_000;
30
30
  export const LABEL_BATCH = 300;
31
31
  /** In-memory last-row cache: entries not observed for this long are dropped (re-read from the DB on return). */
32
32
  export const SEEN_EVICT_MS = 2 * 3_600_000;
33
+ const BASE58 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
33
34
  export class Recorder {
34
35
  enabled;
35
36
  path;
@@ -58,6 +59,8 @@ export class Recorder {
58
59
  /** Counters for the handoff / --verbose. */
59
60
  counters = { observed: 0, written: 0, heartbeat: 0, members: 0, sweepCalls: 0, labelled: 0 };
60
61
  stmts;
62
+ /** Mints whose creator link is already stored (this run), so a refresh costs no write. */
63
+ creatorKnown = new Set();
61
64
  constructor(opts = {}) {
62
65
  this.enabled = opts.enabled ?? true;
63
66
  this.path = opts.path ?? ':memory:';
@@ -87,6 +90,7 @@ export class Recorder {
87
90
  insertMember: this.db.prepare('INSERT OR IGNORE INTO feed_members (t, feed, rank, mint) VALUES (?, ?, ?, ?)'),
88
91
  lastIdentity: this.db.prepare('SELECT symbol, name, launchpad, created_at FROM token_snapshots WHERE mint = ? ORDER BY t DESC LIMIT 1'),
89
92
  lastLive: this.db.prepare(`SELECT t, hash, sources, ${cols} FROM token_snapshots WHERE mint = ? AND kind = 'live' ORDER BY t DESC LIMIT 1`),
93
+ insertCreator: this.db.prepare('INSERT OR IGNORE INTO creators (mint, creator, source, first_t, created_at) VALUES (?, ?, ?, ?, ?)'),
90
94
  };
91
95
  }
92
96
  /** Attach as the data layer's store listener (and detach any previous one). */
@@ -120,6 +124,10 @@ export class Recorder {
120
124
  const pending = [];
121
125
  for (const r of rows) {
122
126
  this.counters.observed++;
127
+ // 0.1.5 dev history: the creator the row's source names (Jupiter `dev`), stored once per mint.
128
+ const dev = r.audit?.devAddress;
129
+ if (r.mint && dev)
130
+ this.recordCreator(r.mint, dev, r.audit?.source ?? 'jupiter-tokens', now, r.token?.createdAt ?? null);
123
131
  if (!r.mint || !r.pair) {
124
132
  res.skipped++;
125
133
  continue;
@@ -173,6 +181,21 @@ export class Recorder {
173
181
  this.counters.heartbeat += res.heartbeat;
174
182
  return res;
175
183
  }
184
+ /**
185
+ * Record which wallet created `mint`, as `source` named it (0.1.5 dev history). First writer wins; a mint or
186
+ * creator that is not a base58 address is ignored. Returns true when a new link was stored.
187
+ */
188
+ recordCreator(mint, creator, source, now = this.clock.now(), createdAt = null) {
189
+ if (!this.enabled || this.closed || this.creatorKnown.has(mint))
190
+ return false;
191
+ if (!BASE58.test(mint) || !BASE58.test(creator) || mint === creator)
192
+ return false;
193
+ this.creatorKnown.add(mint);
194
+ if (this.creatorKnown.size > 200_000)
195
+ this.creatorKnown.clear();
196
+ const r = this.stmts.insertCreator.run(mint, creator, String(source).slice(0, 24), now, createdAt !== null && Number.isFinite(createdAt) ? Math.round(createdAt) : null);
197
+ return Number(r.changes) > 0;
198
+ }
176
199
  /** The mint's last stored live row (one indexed read; only paid the first time a mint is seen this run). */
177
200
  lastStored(mint) {
178
201
  const row = this.stmts.lastLive.get(mint);
@@ -132,4 +132,15 @@ CREATE TABLE IF NOT EXISTS verdicts (
132
132
  run_file TEXT
133
133
  );
134
134
  CREATE INDEX IF NOT EXISTS verdicts_mint_t ON verdicts (mint, t);
135
+
136
+ -- 0.1.5 dev history: which wallet created a coin, as a keyless source named it when dexterm saw the coin
137
+ -- (Jupiter tokens v2 \`dev\`, a PumpPortal create, RugCheck new_tokens). First writer wins; never a guess.
138
+ CREATE TABLE IF NOT EXISTS creators (
139
+ mint TEXT PRIMARY KEY,
140
+ creator TEXT NOT NULL,
141
+ source TEXT NOT NULL, -- 'jupiter-tokens' | 'pumpportal' | 'rugcheck' | 'goplus'
142
+ first_t INTEGER NOT NULL, -- unix ms dexterm first recorded the link
143
+ created_at INTEGER -- the coin's creation time when the source gave one
144
+ ) WITHOUT ROWID;
145
+ CREATE INDEX IF NOT EXISTS creators_creator ON creators (creator, first_t);
135
146
  `;
@@ -66,6 +66,11 @@ export class Canvas {
66
66
  */
67
67
  images;
68
68
  imageMode;
69
+ /**
70
+ * task-1466: where each keyed list row was drawn (a table with `rowKey`, e.g. a screener row by mint), so an
71
+ * overlay drawn later (the agent HUD's row glow / cursor) can find the row on screen. Not emitted anywhere.
72
+ */
73
+ rowMarks;
69
74
  constructor(w, h, theme) {
70
75
  this.w = w;
71
76
  this.h = h;
@@ -82,6 +87,7 @@ export class Canvas {
82
87
  this.shields = [];
83
88
  this.images = [];
84
89
  this.imageMode = 'cells';
90
+ this.rowMarks = [];
85
91
  }
86
92
  idx(x, y) { return y * this.w + x; }
87
93
  /** Copy every buffer of `src` (same size) into this canvas. */
@@ -97,6 +103,7 @@ export class Canvas {
97
103
  this.shields = src.shields.slice();
98
104
  this.images = src.images.slice();
99
105
  this.imageMode = src.imageMode;
106
+ this.rowMarks = src.rowMarks.slice();
100
107
  }
101
108
  put(x, y, ch, st = {}) {
102
109
  if (x < 0 || y < 0 || x >= this.w || y >= this.h)
@@ -119,6 +119,59 @@ export function fmtMcap(n) {
119
119
  return bare;
120
120
  return bare.startsWith('-') ? `-$${bare.slice(1)}` : `$${bare}`;
121
121
  }
122
+ /**
123
+ * task-1472: THE chart number formatter (Y axis, crosshair/legend, last-value tag, volume): compact with a LOWERCASE k
124
+ * for thousands, then M, B, T. One decimal while the value is below 10 of its unit, none after, trailing `.0` dropped:
125
+ * 4930 → 4.9k, 42100 → 42k, 487000 → 487k, 1.23e6 → 1.2M, 4.8e7 → 48M, 1.3e9 → 1.3B, 999 → 999. The unit is promoted
126
+ * AFTER rounding, so 999 900 reads 1M (never 1000k). Below 1 it is the subscript price form (0.0000123 → 0.0₄123):
127
+ * never scientific notation, never a long run of zeros. `extra` (0..3) adds decimals when an axis needs them to keep
128
+ * two neighbouring ticks apart (see compactDecimalsFor). No currency symbol; fmtCompactUsd adds the `$`.
129
+ */
130
+ export function fmtCompact(n, extra = 0) {
131
+ if (n == null || !Number.isFinite(n))
132
+ return '—';
133
+ if (n === 0)
134
+ return '0';
135
+ const sign = n < 0 ? '-' : '';
136
+ const a = Math.abs(n);
137
+ if (a < 1)
138
+ return sign + trimZeros(fmtPriceBare(a));
139
+ const units = ['', 'k', 'M', 'B', 'T'];
140
+ const x = Math.max(0, Math.min(3, Math.floor(extra)));
141
+ const round = (q) => Number(q.toFixed((q < 10 ? 1 : 0) + x));
142
+ let u = 0, v = a;
143
+ while (u < units.length - 1 && v >= 1000) {
144
+ v /= 1000;
145
+ u++;
146
+ }
147
+ let r = round(v);
148
+ if (r >= 1000 && u < units.length - 1) {
149
+ u++;
150
+ r = round(r / 1000);
151
+ }
152
+ return `${sign}${trimZeros(r.toFixed((r < 10 ? 1 : 0) + x))}${units[u]}`;
153
+ }
154
+ /** USD form of fmtCompact: `$4.9k`, `$48M`, `-$1.2k`. */
155
+ export function fmtCompactUsd(n, extra = 0) {
156
+ const bare = fmtCompact(n, extra);
157
+ if (bare === '—')
158
+ return bare;
159
+ return bare.startsWith('-') ? `-$${bare.slice(1)}` : `$${bare}`;
160
+ }
161
+ /**
162
+ * task-1472: the extra decimals (0..3) fmtCompact needs so that axis ticks `step` apart around `around` never print
163
+ * the same label (a 48.1M–48.6M range would otherwise read 48M on every tick). 0 for a normal memecoin range.
164
+ */
165
+ export function compactDecimalsFor(around, step) {
166
+ if (!Number.isFinite(around) || !Number.isFinite(step) || step <= 0 || Math.abs(around) < 1)
167
+ return 0;
168
+ for (let x = 0; x < 3; x++) {
169
+ const a = fmtCompact(around, x);
170
+ if (a !== fmtCompact(around + step, x) && a !== fmtCompact(around - step, x))
171
+ return x;
172
+ }
173
+ return 3;
174
+ }
122
175
  export function fmtPct(n, { sign = true, digits = 1 } = {}) {
123
176
  if (n == null || Number.isNaN(n))
124
177
  return '—';
@@ -268,6 +268,10 @@ export const DEFAULT_KEYMAP = new Keymap([
268
268
  // the open coin. (`P` is already the global positions key, so chart uses `C`.) Collision-free with the
269
269
  // token screen's other capitals (B/S trade, W/X/T/L links, V vet).
270
270
  { keys: ['C'], action: 'popup.chart', label: 'chart popup', screen: 'token' },
271
+ // task-1472: `A` (axis) on the token screen flips the chart Y axis MARKET CAP ⇄ PRICE (both charts; saved as
272
+ // display.chartAxis). Free capital on the token screen (A is only auto-mirror on the follows screen); deliberately
273
+ // not Y, which confirms inside the PAPER ticket and the modals. The chart popup uses the same `A`.
274
+ { keys: ['A'], action: 'display.chartAxis', label: 'chart axis mcap ⇄ price', screen: 'token' },
271
275
  // task-1359: `I` on the token screen opens/toggles the TOKEN-DETAIL HUD (identity, market, risk, snipers/bundled
272
276
  // from the wallet-intel scan); `M` on the trending screen opens/toggles the FEEDS HUD (the keyless social feed +
273
277
  // official news, newest first; ⏎ in the window opens that coin here). Both are read-only windows. `I` is unused
@@ -5,7 +5,7 @@
5
5
  // background (hard cells); glow is only ever applied to neighbouring blank cells or to bg tints.
6
6
  import { Canvas } from "./canvas.js";
7
7
  import { mix, luminance } from "./color.js";
8
- import { fmtUsd, fmtPct, fmtNum, fmtPrice, fmtAgeSec, shortAddr, clamp } from "./fmt.js";
8
+ import { fmtUsd, fmtPct, fmtNum, fmtPrice, fmtPriceBare, fmtCompactUsd, compactDecimalsFor, fmtAgeSec, shortAddr, clamp } from "./fmt.js";
9
9
  import { launchpadColor } from "./theme.js";
10
10
  export const BLOCKS = '▁▂▃▄▅▆▇█';
11
11
  // braille dot bits: column 0 rows 0..3, column 1 rows 0..3 (row 0 = top)
@@ -107,6 +107,7 @@ export function brailleLine(c, x, y, w, h, values, opts = {}) {
107
107
  c.put(x + col, y + r, String.fromCharCode(0x2800 + bits), { fg: neon(color, col / (w - 1), bg), bg, soft: true });
108
108
  }
109
109
  }
110
+ /** task-1472: the price axis never prints scientific notation: subscript zeros below 0.01 (0.0₅493), like fmtPrice. */
110
111
  function fmtPriceAxis(p) {
111
112
  if (p >= 1000)
112
113
  return p.toFixed(0);
@@ -114,7 +115,7 @@ function fmtPriceAxis(p) {
114
115
  return p.toFixed(3);
115
116
  if (p >= 0.001)
116
117
  return p.toFixed(5);
117
- return p.toExponential(2);
118
+ return fmtPriceBare(p);
118
119
  }
119
120
  /**
120
121
  * Candlestick chart with right-hand price axis, last-price tag, volume strip and time axis.
@@ -137,7 +138,6 @@ export function candleChart(c, x, y, w, h, candles, opts = {}) {
137
138
  // task-1366: the bars are laid out in price units either way (identical glyphs); only the labels are scaled
138
139
  const s = opts.scale && Number.isFinite(opts.scale) && opts.scale > 0 ? opts.scale : 1;
139
140
  const baseFmt = opts.axisFmt ?? fmtPriceAxis;
140
- const fmtAxis = s === 1 ? baseFmt : (p) => baseFmt(p * s);
141
141
  let lo = Infinity, hi = -Infinity, vmax = 0;
142
142
  for (const k of data) {
143
143
  lo = Math.min(lo, k.l);
@@ -152,6 +152,9 @@ export function candleChart(c, x, y, w, h, candles, opts = {}) {
152
152
  const gridStyle = { fg: mix(bg, t.border, 0.9), bg, soft: true };
153
153
  const labelStyle = { fg: t.muted, bg };
154
154
  const ticks = Math.max(2, Math.min(6, Math.floor(plotH / 3)));
155
+ // task-1472: one more decimal only when two neighbouring ticks would otherwise print the same compact label
156
+ const extra = opts.axisFmt ? compactDecimalsFor(((hi + lo) / 2) * s, (range / ticks) * s) : 0;
157
+ const fmtAxis = (p) => baseFmt(p * s, extra);
155
158
  for (let i = 0; i <= ticks; i++) {
156
159
  const row = Math.round((i / ticks) * (plotH - 1));
157
160
  const price = hi - (row / (plotH - 1)) * range;
@@ -245,7 +248,7 @@ export function candleChart(c, x, y, w, h, candles, opts = {}) {
245
248
  }
246
249
  }
247
250
  c.text(x + plotW + 1, vy, 'vol', labelStyle);
248
- c.text(x + plotW + 1, vy + 1, fmtUsd(last.v, { digits: 1 }), { fg: t.text, bg });
251
+ c.text(x + plotW + 1, vy + 1, fmtCompactUsd(last.v), { fg: t.text, bg }); // task-1472: volume stays USD, compact k/M
249
252
  }
250
253
  const ty = y + h - 1;
251
254
  const labels = Math.max(2, Math.floor(plotW / 18));
@@ -386,6 +389,8 @@ export function table(c, x, y, w, h, spec) {
386
389
  const selected = ri === spec.selected;
387
390
  const rowBg = selected ? t.selBg : (r % 2 === 1 && spec.zebra !== false ? mix(bg, t.panelAlt, 1) : bg);
388
391
  c.fill(x, ry, w, 1, ' ', { bg: rowBg });
392
+ if (spec.rowKey)
393
+ c.rowMarks.push({ key: spec.rowKey(row), x, y: ry, w });
389
394
  if (selected)
390
395
  c.put(x - 1 >= 0 ? x - 1 : x, ry, '▎', { fg: t.accent, bg: rowBg, soft: true });
391
396
  cx = x;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dexterm-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "mcpName": "io.github.dexterm-xyz/dexterm",
5
5
  "description": "dexterm: terminal DEX screener with a local Claude CLI analyst",
6
6
  "author": "The dexterm authors",