dexterm-cli 0.1.4 → 0.1.5

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 (63) hide show
  1. package/dist/packages/cli/src/agent-paper.js +60 -0
  2. package/dist/packages/cli/src/analyst/backend.js +183 -0
  3. package/dist/packages/cli/src/analyst/codex.js +301 -0
  4. package/dist/packages/cli/src/analyst/session.js +9 -2
  5. package/dist/packages/cli/src/app-help-gen.js +15 -6
  6. package/dist/packages/cli/src/doctor.js +58 -2
  7. package/dist/packages/cli/src/live/dev-history.js +99 -0
  8. package/dist/packages/cli/src/live/newpairs.js +12 -0
  9. package/dist/packages/cli/src/live/screens/token-live.js +11 -1
  10. package/dist/packages/cli/src/live/token.js +1 -0
  11. package/dist/packages/cli/src/main.js +122 -8
  12. package/dist/packages/cli/src/mcp-follow.js +37 -7
  13. package/dist/packages/cli/src/record-cmd.js +2 -0
  14. package/dist/packages/cli/src/rules.js +25 -2
  15. package/dist/packages/cli/src/screens/analyst-session.js +5 -3
  16. package/dist/packages/cli/src/screens/analyst.js +4 -3
  17. package/dist/packages/cli/src/version.js +1 -1
  18. package/dist/packages/custom/src/rules.js +2 -2
  19. package/dist/packages/custom/src/schema.js +5 -1
  20. package/dist/packages/data/src/fixtures.js +28 -6
  21. package/dist/packages/data/src/index.js +3 -0
  22. package/dist/packages/data/src/sources/jupiter.js +1 -0
  23. package/dist/packages/data/src/trade/agent/paper-desk.js +336 -0
  24. package/dist/packages/data/src/trade/agent/rendezvous.js +161 -0
  25. package/dist/packages/mcp/assets/kb/app-help/agent-paper-trade.md +38 -0
  26. package/dist/packages/mcp/assets/kb/app-help/ask-the-analyst.md +1 -1
  27. package/dist/packages/mcp/assets/kb/app-help/connect-an-agent.md +1 -1
  28. package/dist/packages/mcp/assets/kb/app-help/dev-history.md +32 -0
  29. package/dist/packages/mcp/assets/kb/app-help/paper-trade.md +1 -1
  30. package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +4 -1
  31. package/dist/packages/mcp/assets/kb/app-help/screen-token.md +1 -0
  32. package/dist/packages/mcp/assets/kb/app-help/use-codex-analyst.md +31 -0
  33. package/dist/packages/mcp/assets/kb/docs/docs-agents-agent-paper-trading.md +50 -0
  34. package/dist/packages/mcp/assets/kb/docs/docs-agents-directory-metadata-prepared-not-submitted.md +36 -0
  35. package/dist/packages/mcp/assets/kb/docs/docs-agents-install-in-one-command.md +2 -0
  36. package/dist/packages/mcp/assets/kb/docs/docs-analyst-if-claude-is-missing.md +1 -1
  37. package/dist/packages/mcp/assets/kb/docs/docs-analyst-use-codex-instead-of-claude.md +35 -0
  38. package/dist/packages/mcp/assets/kb/docs/docs-beginners-macos-or-linux-npm.md +1 -1
  39. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +1 -1
  40. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-powershell-experimental.md +1 -1
  41. package/dist/packages/mcp/assets/kb/docs/docs-configuration-environment-variables.md +2 -0
  42. package/dist/packages/mcp/assets/kb/docs/docs-configuration-rules-engine.md +1 -0
  43. package/dist/packages/mcp/assets/kb/docs/docs-configuration-the-analyst-s-launch.md +2 -0
  44. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +13 -3
  45. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +1 -1
  46. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +2 -2
  47. package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +0 -2
  48. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-letting-an-agent-paper-trade-experimental.md +32 -0
  49. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-what-the-live-path-keeps.md +1 -1
  50. package/dist/packages/mcp/assets/kb/docs/docs-screens-dev-history.md +44 -0
  51. package/dist/packages/mcp/assets/kb/docs/docs-screens-how-well-it-predicts-measured-held-out.md +46 -0
  52. package/dist/packages/mcp/assets/kb/docs/docs-spectator-follow-over-mcp-in-the-running-terminal.md +1 -1
  53. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-doctor-lines.md +2 -0
  54. package/dist/packages/mcp/src/index.js +5 -2
  55. package/dist/packages/mcp/src/paper-agent.js +92 -0
  56. package/dist/packages/mcp/src/server.js +206 -12
  57. package/dist/packages/mcp/src/spectator-sink.js +73 -4
  58. package/dist/packages/mcp/src/user-content.js +1 -0
  59. package/dist/packages/recorder/src/dev-history.js +129 -0
  60. package/dist/packages/recorder/src/index.js +1 -0
  61. package/dist/packages/recorder/src/recorder.js +23 -0
  62. package/dist/packages/recorder/src/schema.js +11 -0
  63. package/package.json +1 -1
@@ -18,7 +18,7 @@ import { writeFileSync, readFileSync, rmSync, mkdirSync, statSync } from 'node:f
18
18
  import { dirname } from 'node:path';
19
19
  import { randomBytes } from 'node:crypto';
20
20
  import { SpectatorHub, FollowController, SpectatorServer, } from '../../agents/src/index.js';
21
- import { rendezvousPath } from '../../mcp/src/index.js';
21
+ import { rendezvousPath, writeFollowScreen, clearFollowScreen } from '../../mcp/src/index.js';
22
22
  function navLabel(agent, nav) {
23
23
  const coin = nav.mint ? ` ${nav.mint.length > 10 ? nav.mint.slice(0, 4) + '…' + nav.mint.slice(-4) : nav.mint}` : '';
24
24
  if (nav.screen === 'token')
@@ -56,6 +56,7 @@ export class McpFollowSession {
56
56
  stream = null;
57
57
  armed = false;
58
58
  paused = false;
59
+ lastSnapshot = 0;
59
60
  constructor(opts) {
60
61
  this.opts = opts;
61
62
  this.agentLabel = opts.agentLabel || 'mcp-agent';
@@ -85,10 +86,23 @@ export class McpFollowSession {
85
86
  }
86
87
  }
87
88
  if (!this.timer) {
88
- this.timer = setInterval(() => this.pump(), Math.max(30, this.opts.pollMs ?? 150));
89
+ this.timer = setInterval(() => { this.pump(); this.publish(); }, Math.max(30, this.opts.pollMs ?? 150));
89
90
  this.timer.unref?.();
90
91
  }
91
92
  }
93
+ /** Publish the follow-mode screen snapshot (throttled to ~1/s; only while armed). Never throws. */
94
+ publish(force = false) {
95
+ if (!this.opts.snapshot || !this.armed)
96
+ return;
97
+ const now = (this.opts.now ?? Date.now)();
98
+ if (!force && now - this.lastSnapshot < 1000)
99
+ return;
100
+ this.lastSnapshot = now;
101
+ try {
102
+ writeFollowScreen(this.opts.env, this.opts.snapshot());
103
+ }
104
+ catch { /* fail closed: stale → nav refused */ }
105
+ }
92
106
  /** Write (or re-write) the channel + rendezvous so the server emits here. */
93
107
  rearm() {
94
108
  try {
@@ -108,8 +122,11 @@ export class McpFollowSession {
108
122
  // A second follow surface may have replaced the rendezvous after this session started. Never remove
109
123
  // another surface's live handle when this one pauses, is killed, or exits.
110
124
  const current = JSON.parse(readFileSync(this.rvPath, 'utf8'));
111
- if (current.token === this.token)
125
+ if (current.token === this.token) {
112
126
  rmSync(this.rvPath, { force: true });
127
+ if (this.opts.snapshot)
128
+ clearFollowScreen(this.opts.env);
129
+ }
113
130
  }
114
131
  catch { /* absent/malformed/another surface owns it */ }
115
132
  this.armed = false;
@@ -182,6 +199,15 @@ export class McpFollowSession {
182
199
  }
183
200
  catch { /* ignore */ }
184
201
  }
202
+ // task-1443: every agent PAPER write (fill, close, size, or a refusal) is toasted in the terminal, whatever the
203
+ // follow state, so the watcher always sees what the agent did to the simulated ledger.
204
+ if (typeof ev.tool === 'string' && /^paper_(open_ticket|size_ticket|confirm|close)$/.test(ev.tool)) {
205
+ const what = ev.ok === false ? `PAPER ${ev.tool.slice(6).replace('_', ' ')} refused · ${String(ev.summary ?? '').replace(/^error: /, '').slice(0, 70)}` : String(ev.summary ?? ev.tool);
206
+ try {
207
+ this.opts.onStatus?.(`agent ${this.currentAgent}: ${what}`);
208
+ }
209
+ catch { /* ignore */ }
210
+ }
185
211
  try {
186
212
  this.opts.onEvent?.();
187
213
  }
@@ -195,10 +221,14 @@ export class McpFollowSession {
195
221
  return !st || st.status === 'active';
196
222
  }
197
223
  // ----- the owner's controls. Each returns a short line to toast. -----
198
- pause() { const l = this.follow.pause(this.currentAgent); this.paused = l.startsWith('paused '); this.opts.onStatus?.(l); return l; }
199
- takeOver() { const l = this.follow.takeOver(this.currentAgent); this.paused = false; this.opts.onStatus?.(l); return l; }
200
- resume() { const l = this.follow.resume(this.currentAgent); this.paused = false; this.opts.onStatus?.(l); return l; }
201
- kill() { const l = this.follow.kill(this.currentAgent); this.paused = false; this.opts.onStatus?.(l); return l; }
224
+ pause() { const l = this.follow.pause(this.currentAgent); this.paused = l.startsWith('paused '); this.control('paused'); this.opts.onStatus?.(l); return l; }
225
+ takeOver() { const l = this.follow.takeOver(this.currentAgent); this.paused = false; this.control('takeover'); this.opts.onStatus?.(l); return l; }
226
+ resume() { const l = this.follow.resume(this.currentAgent); this.paused = false; this.control('running'); this.opts.onStatus?.(l); return l; }
227
+ kill() { const l = this.follow.kill(this.currentAgent); this.paused = false; this.control('stopped'); this.opts.onStatus?.(l); return l; }
228
+ control(state) { try {
229
+ this.opts.onControl?.(state);
230
+ }
231
+ catch { /* never breaks the key loop */ } }
202
232
  /** Any physical keypress while the agent is driving: the human takes over (no fight over the screen). */
203
233
  userKey() { if (this.follow.controlState === 'agent')
204
234
  this.takeOver(); }
@@ -46,6 +46,8 @@ export function buildLiveEngine(env, opts) {
46
46
  let newpairs = null;
47
47
  if (!opts.noNewpairs) {
48
48
  newpairs = new NewPairsLive(feed.dl, {});
49
+ // 0.1.5 dev history: keep the creator of every new pair in the recorder's creator index.
50
+ newpairs.onCreator = (mint, creator, source, createdAt) => { recorder.recordCreator(mint, creator, source, undefined, createdAt); };
49
51
  const client = new PumpPortalClient({ health: feed.dl.health, clock: feed.dl.clock, onEvent: (ev) => newpairs.handleEvent(ev), onStatus: () => { } });
50
52
  newpairs.attach(client, 'live');
51
53
  newpairs.start();
@@ -1,3 +1,4 @@
1
+ import { devBookOf } from "./live/dev-history.js";
1
2
  import { RulesEngine, ruleToJson, ruleToText, slug, applyAndJournal, describeActions, } from '../../custom/src/index.js';
2
3
  import { alertToDelivery, proposalToDelivery } from "./alerts/format.js";
3
4
  export const RULE_INPUT_MAX = 400;
@@ -66,11 +67,14 @@ export function ruleRowsOf(state) {
66
67
  return c ? c.pct : null;
67
68
  };
68
69
  const openMint = state.live?.token?.view.mint ?? null;
70
+ // 0.1.5 dev history: the creator's recorded rugs as the `devrugs` field (local recorder only; null = unknown).
71
+ const book = devBookOf(state);
72
+ const devRugsOf = (mint) => book ? book.rugsFor(mint) : null;
69
73
  for (const r of state.screener.rows) {
70
74
  if (!r.mint || r.skeleton || seen.has(r.mint))
71
75
  continue;
72
76
  seen.add(r.mint);
73
- const row = Object.assign(Object.create(null), r, { cluster: clusterOf(r.mint), larp: larpGrade(state.larpByMint?.get(r.mint)?.verdict) });
77
+ const row = Object.assign(Object.create(null), r, { cluster: clusterOf(r.mint), larp: larpGrade(state.larpByMint?.get(r.mint)?.verdict), devRugs: devRugsOf(r.mint) });
74
78
  if (r.mint === openMint)
75
79
  overlaySignalFields(row, state, r.mint);
76
80
  out.push(row);
@@ -81,12 +85,31 @@ export function ruleRowsOf(state) {
81
85
  const row = Object.assign(Object.create(null), {
82
86
  mint: v.mint, symbol: v.symbol, price: v.price, mcap: v.mcap, fdv: v.fdv, liq: v.liq, vol24: v.vol24,
83
87
  holders: v.holders, org: v.org, top10: v.top10, dev: v.dev, lpLocked: v.lpLocked,
84
- cluster: clusterOf(v.mint), larp: larpGrade(state.larpByMint?.get(v.mint)?.verdict),
88
+ cluster: clusterOf(v.mint), larp: larpGrade(state.larpByMint?.get(v.mint)?.verdict), devRugs: devRugsOf(v.mint),
85
89
  ageMin: v.ageMin, chg: v.chg, launchpad: v.launchpad, flags: v.flags, verified: v.verified, sus: v.sus,
86
90
  });
87
91
  overlaySignalFields(row, state, v.mint);
88
92
  out.push(row);
89
93
  }
94
+ // 0.1.5 dev history: a brand-new pair (PumpPortal / RugCheck) whose creator has a labelled record becomes a rule
95
+ // row too, so `devrugs >= N scope newpairs` can alert on a new coin from a wallet that rugged before. Only pairs
96
+ // with a known record are added (no flood of unknowns); every other field is what the new-pairs feed knows.
97
+ const np = state.live?.newpairs;
98
+ if (book && np) {
99
+ const now = Date.now();
100
+ for (const p of np.rows.values()) {
101
+ if (seen.has(p.mint) || !p.creator)
102
+ continue;
103
+ const devRugs = devRugsOf(p.mint);
104
+ if (devRugs === null)
105
+ continue;
106
+ seen.add(p.mint);
107
+ out.push(Object.assign(Object.create(null), {
108
+ mint: p.mint, symbol: p.symbol, mcap: p.mcap, liq: p.liq, holders: p.holders, launchpad: p.launchpad,
109
+ ageMin: p.createdAt ? Math.max(0, (now - p.createdAt) / 60_000) : null, bonding: p.bonding, devRugs,
110
+ }));
111
+ }
112
+ }
90
113
  return out;
91
114
  }
92
115
  /** Overlay the live-signals engine's latest values (non-null wins) onto a rule row (task-1404). */
@@ -18,9 +18,11 @@ export function renderAnalystSession(c, x, y, w, h, s, opts) {
18
18
  const bc = mix(t.border, t.ai, 0.55);
19
19
  // Narrow panels (token screen) drop the model so cost · latency · tools still fits beside the title.
20
20
  const full = s.status === 'error' ? 'analyst unavailable' : s.footer;
21
- const caption = (opts.title ?? '✦ ANALYST · claude').length + full.length + 10 <= w ? full : full.split(' · ').slice(0, 3).join(' · ');
21
+ // 0.1.5: a codex conversation says so in the title; claude panels keep the original default.
22
+ const title = opts.title ?? (s.engine === 'codex' ? '✦ ANALYST · codex' : '✦ ANALYST · claude');
23
+ const caption = title.length + full.length + 10 <= w ? full : full.split(' · ').slice(0, 3).join(' · ');
22
24
  const capFg = s.status === 'error' ? t.warn : t.dim;
23
- const box = c.box(x, y, w, h, { title: opts.title ?? '✦ ANALYST · claude', titleFg: t.ai, caption, captionFg: capFg, border: bc });
25
+ const box = c.box(x, y, w, h, { title, titleFg: t.ai, caption, captionFg: capFg, border: bc });
24
26
  const bg = box.bg;
25
27
  const inner = box.w - 2;
26
28
  const frame = opts.frame;
@@ -47,7 +49,7 @@ export function renderAnalystSession(c, x, y, w, h, s, opts) {
47
49
  const drawTail = () => drawInput(c, box, bc, inputY, opts, s);
48
50
  // earlier exchanges on this warm session (compact marker; the current turn is drawn in full below)
49
51
  if (s.history.length && cy < inputY - 1) {
50
- c.text(box.x + 1, cy, `⟲ ${s.history.length} earlier ${s.history.length === 1 ? 'exchange' : 'exchanges'} · one warm claude session`, { fg: t.dim, bg, i: true }, inner - 1);
52
+ c.text(box.x + 1, cy, `⟲ ${s.history.length} earlier ${s.history.length === 1 ? 'exchange' : 'exchanges'} · one warm ${s.engine} session`, { fg: t.dim, bg, i: true }, inner - 1);
51
53
  cy += 1;
52
54
  const last = s.history[s.history.length - 1];
53
55
  const prev = last.error ? `last: ${last.error}` : last.verdict ? `last: ${last.verdict}` : last.summary[0] ? `last: ${last.summary[0]}` : '';
@@ -42,7 +42,8 @@ export function renderAnalyst(c, state) {
42
42
  // next to live data — in live mode the empty tab says how to start a real conversation instead.
43
43
  if (state.screener.source !== 'fixtures') {
44
44
  const live = state.aiFocused === true;
45
- const b = c.box(0, top, chatW, bottom - top, { title: '✦ ANALYST · claude', titleFg: t.ai, caption: 'read-only · local claude CLI · on demand', captionFg: t.dim, border: bc });
45
+ const eng = state.analystEngine ?? 'claude'; // 0.1.5: the user's own CLI that will answer (claude | codex)
46
+ const b = c.box(0, top, chatW, bottom - top, { title: `✦ ANALYST · ${eng}`, titleFg: t.ai, caption: `read-only · local ${eng} CLI · on demand`, captionFg: t.dim, border: bc });
46
47
  const bbg = b.bg;
47
48
  const bin = b.w - 2;
48
49
  let by = b.y + 1;
@@ -54,7 +55,7 @@ export function renderAnalyst(c, state) {
54
55
  } };
55
56
  say2('No conversation yet.');
56
57
  by += 1;
57
- say2("Press i to ask claude about the market, a token, or your watchlist. It can read the live screener, a token's holders / tape / RugCheck, and your watchlist — read-only, never trades.");
58
+ say2(`Press i to ask ${eng} about the market, a token, or your watchlist. It can read the live screener, a token's holders / tape / RugCheck, and your watchlist — read-only, never trades.`);
58
59
  const iy = b.y + b.h - 2;
59
60
  c.hline(b.x, iy - 1, b.w, { fg: bc, bg: bbg, soft: true });
60
61
  c.put(b.x, iy - 1, '├', { fg: bc, bg: bbg, soft: true });
@@ -64,7 +65,7 @@ export function renderAnalyst(c, state) {
64
65
  if (typed)
65
66
  c.text(b.x + 2, iy, typed + (live ? '█' : ''), { fg: t.text, bg: bbg }, bin - 2);
66
67
  else
67
- c.text(b.x + 2, iy, live ? '█ type your question… ⏎ send · esc blur' : 'press i to ask claude · ⏎ to focus', { fg: t.dim, bg: bbg, i: true }, bin - 2);
68
+ c.text(b.x + 2, iy, live ? '█ type your question… ⏎ send · esc blur' : `press i to ask ${eng} · ⏎ to focus`, { fg: t.dim, bg: bbg, i: true }, bin - 2);
68
69
  if (railW)
69
70
  renderCoinList(c, chatW, top, railW, bottom - top, state);
70
71
  footer(c, analystFooter(state, live), state.theme.name, isCalm(state.vfx));
@@ -1,3 +1,3 @@
1
1
  // The one place the app's version lives (task-1397). --version, the help banner and `dexterm update` read it;
2
2
  // cli.test.ts keeps it equal to packages/cli/package.json (release.yml refuses a tag that differs).
3
- export const VERSION = '0.1.4';
3
+ export const VERSION = '0.1.5';
@@ -3,13 +3,13 @@ export const FIELDS = {
3
3
  vol5m: (r) => r.vol5m, vol1h: (r) => r.vol1h, vol24: (r) => r.vol24,
4
4
  holders: (r) => r.holders, org: (r) => r.org, top10: (r) => r.top10, dev: (r) => r.dev,
5
5
  lplocked: (r) => r.lpLocked, bundled: (r) => r.bundled, snipers: (r) => r.snipers,
6
- cluster: (r) => r.cluster, larp: (r) => r.larp,
6
+ cluster: (r) => r.cluster, larp: (r) => r.larp, devrugs: (r) => r.devRugs,
7
7
  fresh: (r) => r.fresh, mintable: (r) => r.mintable, freezable: (r) => r.freezable,
8
8
  age: (r) => r.ageMin, m5: (r) => r.chg?.m5, h1: (r) => r.chg?.h1, h6: (r) => r.chg?.h6, h24: (r) => r.chg?.h24,
9
9
  buys: (r) => r.buys, sells: (r) => r.sells, bonding: (r) => r.bonding,
10
10
  };
11
11
  export const FIELD_NAMES = Object.keys(FIELDS);
12
- const ALIASES = { vol: 'vol24', volume: 'vol24', liquidity: 'liq', cap: 'mcap', mc: 'mcap', holder: 'holders', organic: 'org', top: 'top10', locked: 'lplocked', 'cluster%': 'cluster', clusterpct: 'cluster', freshwallets: 'fresh', mintauth: 'mintable', freezeauth: 'freezable' };
12
+ const ALIASES = { vol: 'vol24', volume: 'vol24', liquidity: 'liq', cap: 'mcap', mc: 'mcap', holder: 'holders', organic: 'org', top: 'top10', locked: 'lplocked', 'cluster%': 'cluster', clusterpct: 'cluster', freshwallets: 'fresh', mintauth: 'mintable', freezeauth: 'freezable', devrug: 'devrugs', serialrugger: 'devrugs' };
13
13
  const OPS = { '>': (a, b) => a > b, '>=': (a, b) => a >= b, '<': (a, b) => a < b, '<=': (a, b) => a <= b, '=': (a, b) => a === b, '==': (a, b) => a === b, '!=': (a, b) => a !== b };
14
14
  const TAGS = {
15
15
  launchpad: (r, v) => (r.launchpad ?? '').toLowerCase().replace('.fun', '') === v.toLowerCase().replace('.fun', ''),
@@ -261,6 +261,10 @@ export const SCHEMAS = {
261
261
  /** Reserved: the trading gate is NOT here (signer policy owns it); listed so a proposal that tries gets a precise refusal. */
262
262
  gate: { type: 'string', enum: ['keypress'] },
263
263
  } },
264
+ /** 0.1.5: which of the user's own CLIs runs the analyst. `/analyst` is user-only (DENIED_FOR_MODELS): a model never picks its own engine. */
265
+ analyst: { type: 'object', additional: false, props: {
266
+ engine: { type: 'string', enum: ['auto', 'claude', 'codex'] },
267
+ } },
264
268
  /** task-1393: voice-out preferences. `/voice` is user-only (DENIED_FOR_MODELS), so only the keyboard sets it. */
265
269
  voice: { type: 'object', additional: false, props: {
266
270
  name: { type: 'string', max: 64 },
@@ -426,7 +430,7 @@ export function applyOps(doc, ops) {
426
430
  export const DENIED_FOR_MODELS = [
427
431
  /^\/signer(\/|$)/, /^\/trading(\/|$)/, /^\/keys(\/|$)/, /^\/caps(\/|$)/, /^\/mandates?(\/|$)/, /^\/wallet(\/|$)/,
428
432
  /^\/packs(\/|$)/, /^\/plugins\/[^/]+\/(capabilities|enabled)(\/|$)/, /^\/plugins\/[^/]+$/, /^\/plugins$/,
429
- /^\/jarvis\/gate(\/|$)/, /^\/jarvis\/configAssistant$/, /^\/voice(\/|$)/, /^\/alerts(\/|$)/, /^\/agents(\/|$)/, /^\/version$/,
433
+ /^\/jarvis\/gate(\/|$)/, /^\/jarvis\/configAssistant$/, /^\/voice(\/|$)/, /^\/analyst(\/|$)/, /^\/alerts(\/|$)/, /^\/agents(\/|$)/, /^\/version$/,
430
434
  /^\/rules\/(\d+|-)\/then\/(\d+|-)\/args\/(arm|armed|auto|autoArm|confirm|execute|sign|send)(\/|$)/,
431
435
  ];
432
436
  /** Keys a model-authored VALUE may never contain at any depth (money/secret-shaped), whatever the path. */
@@ -62,21 +62,43 @@ export class FixtureStore {
62
62
  }
63
63
  save(path, note) {
64
64
  mkdirSync(dirname(path), { recursive: true });
65
- const file = {
65
+ const raw = {
66
66
  version: 1,
67
67
  recordedAt: new Date().toISOString(),
68
68
  note: note ?? this.note,
69
69
  entries: this.list(),
70
70
  };
71
71
  if (this.streams.length)
72
- file.streams = this.streams;
73
- const text = JSON.stringify(file, null, 1) + '\n';
74
- // Belt and braces: the key is redacted at capture time; refuse to write a file that still carries one.
75
- if (containsSecret(text))
72
+ raw.streams = this.streams;
73
+ // Request URLs are redacted at capture time, but live response bodies echo third-party URLs with key-shaped
74
+ // params (DexScreener image URLs carry `&key=<cache id>`); refusing those threw away whole live sessions at
75
+ // quit (task-1443). Redact every string instead. Replay still matches: requestKey() redacts the same way.
76
+ const file = scrubSecrets(raw);
77
+ // Belt and braces: never write a file that still carries a key value (checked per string, so escaping is moot).
78
+ if (anyString(file, containsSecret))
76
79
  throw new Error('refusing to save a recording that contains an api-key value');
77
- writeFileSync(path, text);
80
+ writeFileSync(path, JSON.stringify(file, null, 1) + '\n');
78
81
  }
79
82
  }
83
+ /** A deep copy of `v` with redactSecrets() applied to every string. */
84
+ function scrubSecrets(v) {
85
+ if (typeof v === 'string')
86
+ return redactSecrets(v);
87
+ if (Array.isArray(v))
88
+ return v.map(scrubSecrets);
89
+ if (v && typeof v === 'object')
90
+ return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, scrubSecrets(x)]));
91
+ return v;
92
+ }
93
+ function anyString(v, pred) {
94
+ if (typeof v === 'string')
95
+ return pred(v);
96
+ if (Array.isArray(v))
97
+ return v.some((x) => anyString(x, pred));
98
+ if (v && typeof v === 'object')
99
+ return Object.values(v).some((x) => anyString(x, pred));
100
+ return false;
101
+ }
80
102
  export function toEntry(req, res, recordedAt = new Date().toISOString()) {
81
103
  let body = res.body;
82
104
  let bodyIsJson = false;
@@ -38,6 +38,9 @@ export * from "./trade/tx-decode.js";
38
38
  export * from "./trade/sol-usd.js";
39
39
  export * from "./trade/kill-switch.js";
40
40
  export * from "./trade/token-delta.js";
41
+ // task-1443: the paper agent desk + its rendezvous (agents paper-trade inside an armed mandate; never sign/send).
42
+ export * from "./trade/agent/paper-desk.js";
43
+ export * from "./trade/agent/rendezvous.js";
41
44
  export * from "./trade/signer/protocol.js";
42
45
  export * from "./trade/signer/service.js";
43
46
  export * from "./trade/signer/host.js";
@@ -106,6 +106,7 @@ export function normalizeJupToken(raw, fetchedAt) {
106
106
  top10Pct: numOrNull(raw.audit?.topHoldersPercentage),
107
107
  devPct: numOrNull(raw.audit?.devBalancePercentage),
108
108
  devMints: numOrNull(raw.audit?.devMints),
109
+ devAddress: typeof raw.dev === 'string' && /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(raw.dev) ? raw.dev : null,
109
110
  lpLockedPct: null,
110
111
  rugRisks: [],
111
112
  rugScore: null,