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.
- package/dist/packages/cli/src/agent-paper.js +60 -0
- package/dist/packages/cli/src/analyst/backend.js +183 -0
- package/dist/packages/cli/src/analyst/codex.js +301 -0
- package/dist/packages/cli/src/analyst/session.js +9 -2
- package/dist/packages/cli/src/app-help-gen.js +15 -6
- package/dist/packages/cli/src/doctor.js +58 -2
- package/dist/packages/cli/src/live/dev-history.js +99 -0
- package/dist/packages/cli/src/live/newpairs.js +12 -0
- package/dist/packages/cli/src/live/screens/token-live.js +11 -1
- package/dist/packages/cli/src/live/token.js +1 -0
- package/dist/packages/cli/src/main.js +122 -8
- package/dist/packages/cli/src/mcp-follow.js +37 -7
- package/dist/packages/cli/src/record-cmd.js +2 -0
- package/dist/packages/cli/src/rules.js +25 -2
- package/dist/packages/cli/src/screens/analyst-session.js +5 -3
- package/dist/packages/cli/src/screens/analyst.js +4 -3
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/custom/src/rules.js +2 -2
- package/dist/packages/custom/src/schema.js +5 -1
- package/dist/packages/data/src/fixtures.js +28 -6
- package/dist/packages/data/src/index.js +3 -0
- package/dist/packages/data/src/sources/jupiter.js +1 -0
- package/dist/packages/data/src/trade/agent/paper-desk.js +336 -0
- package/dist/packages/data/src/trade/agent/rendezvous.js +161 -0
- package/dist/packages/mcp/assets/kb/app-help/agent-paper-trade.md +38 -0
- package/dist/packages/mcp/assets/kb/app-help/ask-the-analyst.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/connect-an-agent.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/dev-history.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/paper-trade.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +4 -1
- package/dist/packages/mcp/assets/kb/app-help/screen-token.md +1 -0
- package/dist/packages/mcp/assets/kb/app-help/use-codex-analyst.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-agents-agent-paper-trading.md +50 -0
- package/dist/packages/mcp/assets/kb/docs/docs-agents-directory-metadata-prepared-not-submitted.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-agents-install-in-one-command.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-if-claude-is-missing.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-use-codex-instead-of-claude.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-macos-or-linux-npm.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-powershell-experimental.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-environment-variables.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-rules-engine.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-the-analyst-s-launch.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +13 -3
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +0 -2
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-letting-an-agent-paper-trade-experimental.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-what-the-live-path-keeps.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-screens-dev-history.md +44 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-how-well-it-predicts-measured-held-out.md +46 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-follow-over-mcp-in-the-running-terminal.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-doctor-lines.md +2 -0
- package/dist/packages/mcp/src/index.js +5 -2
- package/dist/packages/mcp/src/paper-agent.js +92 -0
- package/dist/packages/mcp/src/server.js +206 -12
- package/dist/packages/mcp/src/spectator-sink.js +73 -4
- package/dist/packages/mcp/src/user-content.js +1 -0
- package/dist/packages/recorder/src/dev-history.js +129 -0
- package/dist/packages/recorder/src/index.js +1 -0
- package/dist/packages/recorder/src/recorder.js +23 -0
- package/dist/packages/recorder/src/schema.js +11 -0
- 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
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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' :
|
|
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.
|
|
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
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
//
|
|
75
|
-
|
|
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,
|
|
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,
|