dexterm-cli 0.1.1 → 0.1.3
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/agents/src/bus.js +57 -6
- package/dist/packages/agents/src/executor.js +25 -0
- package/dist/packages/agents/src/follow.js +105 -0
- package/dist/packages/agents/src/gate.js +3 -0
- package/dist/packages/agents/src/identity.js +41 -2
- package/dist/packages/agents/src/index.js +5 -0
- package/dist/packages/agents/src/spectator-server.js +153 -0
- package/dist/packages/agents/src/spectator.js +212 -0
- package/dist/packages/agents/src/tiers.js +5 -0
- package/dist/packages/agents/src/tools.js +3 -0
- package/dist/packages/cli/assets/claude/analyst-system.md +6 -1
- package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +11 -3
- package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +13 -3
- package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +3 -3
- package/dist/packages/cli/src/agent-cmd.js +107 -10
- package/dist/packages/cli/src/agent-spectator.js +37 -0
- package/dist/packages/cli/src/alerts/adapters/desktop.js +40 -0
- package/dist/packages/cli/src/alerts/adapters/discord.js +36 -0
- package/dist/packages/cli/src/alerts/adapters/telegram.js +43 -0
- package/dist/packages/cli/src/alerts/adapters/toast.js +16 -0
- package/dist/packages/cli/src/alerts/creds.js +83 -0
- package/dist/packages/cli/src/alerts/format.js +66 -0
- package/dist/packages/cli/src/alerts/index.js +10 -0
- package/dist/packages/cli/src/alerts/notifier.js +53 -0
- package/dist/packages/cli/src/alerts/route.js +19 -0
- package/dist/packages/cli/src/alerts/types.js +1 -0
- package/dist/packages/cli/src/alerts-cmd.js +212 -0
- package/dist/packages/cli/src/app-help-gen.js +365 -0
- package/dist/packages/cli/src/backtest-cmd.js +62 -3
- package/dist/packages/cli/src/coin-target.js +2 -0
- package/dist/packages/cli/src/doctor.js +21 -6
- package/dist/packages/cli/src/export.js +235 -0
- package/dist/packages/cli/src/follow-cmd.js +107 -1
- package/dist/packages/cli/src/help-cmd.js +48 -0
- package/dist/packages/cli/src/live/index.js +66 -3
- package/dist/packages/cli/src/live/screens/token-live.js +24 -0
- package/dist/packages/cli/src/live/signals.js +109 -0
- package/dist/packages/cli/src/live/trade.js +24 -1
- package/dist/packages/cli/src/main.js +596 -49
- package/dist/packages/cli/src/mcp-cmd.js +321 -0
- package/dist/packages/cli/src/pnl-app.js +221 -0
- package/dist/packages/cli/src/pnl-card.js +615 -0
- package/dist/packages/cli/src/record-cmd.js +455 -0
- package/dist/packages/cli/src/rules.js +69 -2
- package/dist/packages/cli/src/screens/follows.js +91 -2
- package/dist/packages/cli/src/screens/overlay.js +3 -0
- package/dist/packages/cli/src/screens/positions.js +1 -1
- package/dist/packages/cli/src/screens/rules.js +1 -1
- package/dist/packages/cli/src/skill-eval-build.js +109 -0
- package/dist/packages/cli/src/spectate-cmd.js +104 -0
- package/dist/packages/cli/src/state.js +1 -1
- package/dist/packages/cli/src/update.js +294 -0
- package/dist/packages/cli/src/version.js +3 -0
- package/dist/packages/cli/src/voice-cmd.js +194 -0
- package/dist/packages/cli/src/voice-hud.js +1 -1
- package/dist/packages/cli/src/watch-cmd.js +190 -0
- package/dist/packages/custom/src/rules.js +3 -2
- package/dist/packages/custom/src/schema.js +24 -1
- package/dist/packages/data/src/analyst-actions.js +130 -3
- package/dist/packages/data/src/coin-target.js +82 -0
- package/dist/packages/data/src/fast-lane.js +16 -2
- package/dist/packages/data/src/index.js +4 -0
- package/dist/packages/data/src/intel/helius-history.js +12 -1
- package/dist/packages/data/src/intel/live-signals.js +185 -0
- package/dist/packages/data/src/larp/link-baseline.js +196 -0
- package/dist/packages/data/src/sources/dexscreener.js +14 -0
- package/dist/packages/data/src/sources/pumpportal.js +6 -1
- package/dist/packages/data/src/trade/copy/index.js +1 -1
- package/dist/packages/data/src/trade/copy/stats.js +36 -2
- package/dist/packages/data/src/trade/copy/tracker.js +3 -2
- package/dist/packages/mcp/assets/kb/app-help/app-help-command.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/ask-the-analyst.md +43 -0
- package/dist/packages/mcp/assets/kb/app-help/backtest.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/bubble-map.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/calm-mode.md +31 -0
- package/dist/packages/mcp/assets/kb/app-help/change-voice-speed.md +31 -0
- package/dist/packages/mcp/assets/kb/app-help/change-voice.md +30 -0
- package/dist/packages/mcp/assets/kb/app-help/chart-popup-selftest.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/coin-images.md +29 -0
- package/dist/packages/mcp/assets/kb/app-help/configure-rpc-key.md +28 -0
- package/dist/packages/mcp/assets/kb/app-help/connect-an-agent-registry.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/connect-an-agent.md +39 -0
- package/dist/packages/mcp/assets/kb/app-help/custom-keymap.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/edit-kb-or-skill.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/edit-rules.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/export-a-view.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/follow-a-wallet.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/getting-started.md +37 -0
- package/dist/packages/mcp/assets/kb/app-help/kol-lists.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/larp-check.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/link-a-wallet.md +41 -0
- package/dist/packages/mcp/assets/kb/app-help/login-with-x.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/narrative-board.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/open-a-coin.md +43 -0
- package/dist/packages/mcp/assets/kb/app-help/open-chart-popup.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/order-plans.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/paper-trade.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/price-or-marketcap.md +38 -0
- package/dist/packages/mcp/assets/kb/app-help/record-headless.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/recorder-history.md +39 -0
- package/dist/packages/mcp/assets/kb/app-help/rug-check.md +39 -0
- package/dist/packages/mcp/assets/kb/app-help/run-doctor.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/scan-mode.md +37 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-analyst.md +181 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-kol.md +190 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-newpairs.md +181 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +180 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-screener.md +199 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-token.md +240 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-trending.md +192 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-watchlist.md +181 -0
- package/dist/packages/mcp/assets/kb/app-help/set-a-filter.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/setup-neural-voice.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/share-packs.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/sort-the-screener.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/spectate-an-agent.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/standing-mandates.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/switch-theme.md +42 -0
- package/dist/packages/mcp/assets/kb/app-help/turn-on-alerts.md +37 -0
- package/dist/packages/mcp/assets/kb/app-help/update-dexterm.md +30 -0
- package/dist/packages/mcp/assets/kb/app-help/use-presets.md +42 -0
- package/dist/packages/mcp/assets/kb/app-help/use-voice-mode.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/watch-a-coin.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/watch-headless.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/what-dexterm-shows-agents.md +31 -0
- package/dist/packages/mcp/assets/kb/app-help/workspaces.md +38 -0
- package/dist/packages/mcp/src/app-help.js +135 -0
- package/dist/packages/mcp/src/index.js +3 -0
- package/dist/packages/mcp/src/server.js +179 -5
- package/dist/packages/mcp/src/spectator-sink.js +72 -0
- package/dist/packages/packs/src/index.js +3 -0
- package/dist/packages/packs/src/skill-backtest.js +300 -0
- package/dist/packages/packs/src/skill-care.js +206 -0
- package/dist/packages/popup/src/pages/spectator.css +45 -0
- package/dist/packages/popup/src/pages/spectator.html +25 -0
- package/dist/packages/popup/src/pages/spectator.js +74 -0
- package/dist/packages/popup/src/selftest.js +19 -0
- package/dist/packages/popup/src/server.js +7 -1
- package/dist/packages/tui/src/index.js +3 -0
- package/dist/packages/tui/src/keymap.js +12 -0
- package/dist/packages/tui/src/raster-font.js +195 -0
- package/dist/packages/tui/src/raster.js +213 -0
- package/dist/packages/voice/src/controller.js +57 -4
- package/dist/packages/voice/src/doctor.js +96 -11
- package/dist/packages/voice/src/download.js +184 -0
- package/dist/packages/voice/src/engines.js +55 -8
- package/dist/packages/voice/src/index.js +9 -3
- package/dist/packages/voice/src/speakable.js +111 -0
- package/dist/packages/voice/src/stt/whisper.js +14 -4
- package/dist/packages/voice/src/tts/espeak.js +131 -0
- package/dist/packages/voice/src/tts/piper-setup.js +229 -0
- package/dist/packages/voice/src/tts/piper.js +449 -0
- package/dist/packages/voice/src/tts/say.js +72 -35
- package/dist/packages/voice/src/tts/voices.js +209 -0
- package/package.json +5 -9
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// mirror PROPOSAL and Tier 3 the capped auto-mirror, both behind the plan engine's own keypress gates.
|
|
7
7
|
import { Canvas, header, footer, breakpoint, fmtAgeSec } from '../../../tui/src/index.js';
|
|
8
8
|
import { headerModel } from "../state.js";
|
|
9
|
-
import { shortAddr, autoSizeCap } from '../../../data/src/trade/copy/index.js';
|
|
9
|
+
import { shortAddr, autoSizeCap, fmtHoldMs } from '../../../data/src/trade/copy/index.js';
|
|
10
10
|
function fmtSol(n) { const s = n >= 0 ? '+' : '−'; return `${s}${Math.abs(n).toFixed(Math.abs(n) >= 10 ? 1 : 3)} SOL`; }
|
|
11
11
|
function fmtUsdSigned(n) { const s = n >= 0 ? '+' : '−'; const a = Math.abs(n); return `${s}$${a >= 1000 ? (a / 1000).toFixed(1) + 'k' : a.toFixed(a >= 100 ? 0 : 2)}`; }
|
|
12
12
|
function fmtUsdAbs(n) { if (n === null)
|
|
@@ -196,6 +196,95 @@ export function renderFollows(c, state) {
|
|
|
196
196
|
y += 2;
|
|
197
197
|
c.text(mbox.x + 1, y, gateOk ? 'Hold and release [enter] or [y] to turn auto ON (physical keypress) · [esc] leaves it OFF · a pipe/agent cannot' : '[esc] closes · accept the popup, then A again', { fg: t.accent, bg: mbg }, mbox.w - 2);
|
|
198
198
|
}
|
|
199
|
+
// task-1399: the per-wallet DETAIL view (its own trades + positions + metrics, each with source + data age).
|
|
200
|
+
if (ui?.detail)
|
|
201
|
+
renderWalletDetail(c, state, now);
|
|
199
202
|
const autoHint = (() => { const f = follows[Math.max(0, Math.min(follows.length - 1, ui?.selected ?? 0))]; const p = f ? svc?.auto?.policy(f.addr) : null; return p?.enabled ? 'auto OFF' : 'auto-mirror'; })();
|
|
200
|
-
|
|
203
|
+
const foot = ui?.detail
|
|
204
|
+
? [['↑↓', 'scroll'], ['enter/esc', 'close'], ['q', 'quit']]
|
|
205
|
+
: [['↑↓', 'move'], ['enter', 'wallet view'], ['i', 'follow a wallet'], ['d', 'unfollow'], ['m', ui?.mirror ? (ui.mirror.result?.ok ? 'PROPOSE mirror' : 'close') : 'mirror ticket'], ['A', autoHint], ['r', 'poll now'], ['esc', 'back'], ['q', 'quit']];
|
|
206
|
+
footer(c, foot, t.name);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* task-1399 — one followed wallet's DETAIL view: its own recent decoded trades and positions, plus the full metric
|
|
210
|
+
* set (realised PnL, win rate with n, median hold time, early-entry score, open), each labelled with its source
|
|
211
|
+
* (Helius history) and data age (time since the last poll). Read-only: nothing here trades, arms, proposes or signs.
|
|
212
|
+
*/
|
|
213
|
+
function renderWalletDetail(c, state, now) {
|
|
214
|
+
const t = c.theme;
|
|
215
|
+
const ui = state.copyUi;
|
|
216
|
+
const svc = state.copy;
|
|
217
|
+
if (!ui?.detail || !svc)
|
|
218
|
+
return;
|
|
219
|
+
const addr = ui.detail.wallet;
|
|
220
|
+
const f = svc.follows().find((x) => x.addr === addr);
|
|
221
|
+
const s = svc.stats(addr);
|
|
222
|
+
const trades = svc.trades(addr); // newest first
|
|
223
|
+
const top = 2, bottom = c.h - 1;
|
|
224
|
+
const mw = Math.min(c.w - 2, 104);
|
|
225
|
+
const mh = Math.max(10, bottom - top - 1);
|
|
226
|
+
const mx = Math.floor((c.w - mw) / 2), my = top + 1;
|
|
227
|
+
c.noRain(mx, my, mw, mh);
|
|
228
|
+
const label = f?.name ? ` · ${f.name}` : '';
|
|
229
|
+
const box = c.box(mx, my, mw, mh, { title: `WALLET${label} — read-only`, caption: 'its own trades + positions · every number: source + data age · not a recommendation', focus: true, bg: t.panelAlt });
|
|
230
|
+
const bg = box.bg;
|
|
231
|
+
let y = box.y;
|
|
232
|
+
c.text(box.x + 1, y, addr, { fg: t.text, bg }, box.w - 2);
|
|
233
|
+
y += 1;
|
|
234
|
+
const age = s.sample.asOf ? `${Math.max(0, Math.round((now - s.sample.asOf) / 1000))}s ago` : 'not fetched yet';
|
|
235
|
+
c.text(box.x + 1, y, `source ${s.sample.source} · ${age} · ${s.sample.note}`, { fg: t.dim, bg }, box.w - 2);
|
|
236
|
+
y += 1;
|
|
237
|
+
const pnl = `${fmtSol(s.realisedSol)}${s.realisedUsd !== null ? ` ${fmtUsdSigned(s.realisedUsd)}` : ''}`;
|
|
238
|
+
const wr = s.winRate === null ? `win rate n<10 (${s.wins}/${s.closed})` : `win rate ${Math.round(s.winRate * 100)}% (${s.wins}/${s.closed})`;
|
|
239
|
+
c.text(box.x + 1, y, `realised ${pnl} · ${wr} · hold ${fmtHoldMs(s.holdMedianMs)} · EES ${s.earlyEntry.score === null ? '—' : s.earlyEntry.score.toFixed(2)} · open ${s.open} · ${s.buys}b/${s.sells}s`, { fg: t.text, bg }, box.w - 2);
|
|
240
|
+
y += 1;
|
|
241
|
+
c.text(box.x + 1, y, `hold: ${s.holdNote} · EES: ${s.earlyEntry.note}`, { fg: t.dim, bg }, box.w - 2);
|
|
242
|
+
y += 2;
|
|
243
|
+
const footerY = box.y + box.h - 1;
|
|
244
|
+
const posCount = Math.min(s.positions.length, 6);
|
|
245
|
+
const posHeaderY = posCount ? footerY - posCount - 1 : footerY;
|
|
246
|
+
// RECENT TRADES (scrollable) fill the space above the positions block.
|
|
247
|
+
c.text(box.x + 1, y, 'RECENT TRADES', { fg: t.dim, bg });
|
|
248
|
+
y += 1;
|
|
249
|
+
if (!trades.length) {
|
|
250
|
+
c.text(box.x + 1, y, svc.status().state === 'unavailable' ? 'needs a free Helius key' : 'no swaps in fetched history', { fg: t.dim, bg, i: true });
|
|
251
|
+
y += 1;
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
const roomRows = Math.max(1, posHeaderY - 1 - y);
|
|
255
|
+
const scroll = Math.max(0, Math.min(Math.max(0, trades.length - roomRows), ui.detail.scroll));
|
|
256
|
+
for (let i = scroll; i < trades.length && y < posHeaderY - 1; i++) {
|
|
257
|
+
const tr = trades[i];
|
|
258
|
+
const sideFg = tr.side === 'buy' ? t.up : tr.side === 'sell' ? t.down : t.muted;
|
|
259
|
+
let cx = c.text(box.x + 1, y, (tr.tKnown ? new Date(tr.t).toISOString().replace('T', ' ').slice(5, 19) : 'time unknown ').padEnd(15), { fg: t.dim, bg });
|
|
260
|
+
cx = c.text(cx + 1, y, tr.side.toUpperCase().padEnd(9), { fg: sideFg, bg, b: true });
|
|
261
|
+
cx = c.text(cx + 1, y, shortAddr(tr.mint).padEnd(11), { fg: t.text, bg });
|
|
262
|
+
cx = c.text(cx + 1, y, (tr.solDelta !== null ? `${Math.abs(tr.solDelta).toFixed(3)} SOL` : 'unpriced').padStart(11), { fg: t.muted, bg });
|
|
263
|
+
cx = c.text(cx + 1, y, fmtUsdAbs(tr.usd).padStart(8), { fg: t.text, bg });
|
|
264
|
+
if (tr.note)
|
|
265
|
+
c.text(cx + 1, y, `(${tr.note})`, { fg: t.dim, bg }, box.w - (cx + 1 - box.x) - 1);
|
|
266
|
+
y += 1;
|
|
267
|
+
}
|
|
268
|
+
const older = trades.length - scroll - roomRows;
|
|
269
|
+
if (older > 0)
|
|
270
|
+
c.textR(box.x + 1, posHeaderY - 1, box.w - 2, `… ${older} older (↑↓)`, { fg: t.dim, bg, i: true });
|
|
271
|
+
}
|
|
272
|
+
// POSITIONS block (fixed, bottom).
|
|
273
|
+
if (posCount) {
|
|
274
|
+
c.text(box.x + 1, posHeaderY, `POSITIONS · ${s.positions.length}`, { fg: t.dim, bg });
|
|
275
|
+
let py = posHeaderY + 1;
|
|
276
|
+
for (const p of s.positions.slice(0, posCount)) {
|
|
277
|
+
if (py >= footerY)
|
|
278
|
+
break;
|
|
279
|
+
let cx = c.text(box.x + 1, py, shortAddr(p.mint).padEnd(11), { fg: t.text, bg });
|
|
280
|
+
cx = c.text(cx + 1, py, (p.closed ? 'closed' : 'open').padEnd(7), { fg: p.closed ? t.muted : t.accent, bg });
|
|
281
|
+
cx = c.text(cx + 1, py, fmtSol(p.realisedSol).padStart(11), { fg: p.realisedSol > 0 ? t.up : p.realisedSol < 0 ? t.down : t.muted, bg });
|
|
282
|
+
cx = c.text(cx + 1, py, `${p.buys}b/${p.sells}s`.padEnd(8), { fg: t.dim, bg });
|
|
283
|
+
cx = c.text(cx + 1, py, `hold ${fmtHoldMs(p.holdMs)}`.padEnd(12), { fg: t.dim, bg });
|
|
284
|
+
if (p.flagged)
|
|
285
|
+
c.text(cx + 1, py, '(cost-0 lot: airdrop/transfer/unpriced)', { fg: t.dim, bg }, box.w - (cx + 1 - box.x) - 1);
|
|
286
|
+
py += 1;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
c.textR(box.x + 1, footerY, box.w - 2, 'not a recommendation · a wallet\'s past fills say nothing about its next one', { fg: t.dim, bg, i: true });
|
|
201
290
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// after the effects pass, so the card is crisp and no rain/scanline crosses it. Any key closes the
|
|
4
4
|
// overlay; `?` toggles help.
|
|
5
5
|
import { Canvas, DEFAULT_KEYMAP, mix } from '../../../tui/src/index.js';
|
|
6
|
+
import { renderPnlOverlay } from "../pnl-app.js";
|
|
6
7
|
/** Pretty single-key display for a keymap id ('enter' → '⏎', 'pagedown' → 'PgDn', 'ctrl+f' → '^F'). */
|
|
7
8
|
function prettyKey(id) {
|
|
8
9
|
switch (id) {
|
|
@@ -241,6 +242,8 @@ export function renderOverlay(c, state) {
|
|
|
241
242
|
renderHelp(c, state);
|
|
242
243
|
else if (state.overlay === 'workspaces')
|
|
243
244
|
renderWorkspaces(c, state);
|
|
245
|
+
else if (state.overlay === 'pnl')
|
|
246
|
+
renderPnlOverlay(c, state);
|
|
244
247
|
else if (state.overlay === 'links' && state.linkMenu) {
|
|
245
248
|
const menu = state.linkMenu;
|
|
246
249
|
const box = card(c, { w: 100, h: Math.min(24, menu.items.length * 2 + 6) }, 'ADVERTISED LINKS (UNTRUSTED)', 'j/k select · enter open · esc close');
|
|
@@ -172,5 +172,5 @@ export function renderPositions(c, state) {
|
|
|
172
172
|
}
|
|
173
173
|
if (v.history.length === 0)
|
|
174
174
|
c.text(hb.x + 1, hb.y, 'no paper trades yet', { fg: t.dim, bg: hb.bg });
|
|
175
|
-
footer(c, [['esc', 'back'], ['r', 'refresh'], ['1-4', 'screens'], ['q', 'quit']], t.name, isCalm(state.vfx));
|
|
175
|
+
footer(c, [['esc', 'back'], ['r', 'refresh'], ['H', 'PnL card'], ['1-4', 'screens'], ['q', 'quit']], t.name, isCalm(state.vfx));
|
|
176
176
|
}
|
|
@@ -39,7 +39,7 @@ export function renderRules(c, state) {
|
|
|
39
39
|
cy += 1;
|
|
40
40
|
c.text(box.x + 1, cy, 'dexterm rules add "<rule>" the same from a shell · rules list | check | dry-run | remove | enable | disable', { fg: t.dim, bg }, box.w - 2);
|
|
41
41
|
cy += 1;
|
|
42
|
-
c.text(box.x + 1, cy, 'fields: price mcap fdv liq vol5m vol1h vol24 holders org top10 dev lplocked bundled snipers cluster age m5 h1 h6 h24 buys sells bonding', { fg: t.dim, bg }, box.w - 2);
|
|
42
|
+
c.text(box.x + 1, cy, 'fields: price mcap fdv liq vol5m vol1h vol24 holders org top10 dev lplocked bundled snipers cluster fresh mintable freezable age m5 h1 h6 h24 buys sells bonding', { fg: t.dim, bg }, box.w - 2);
|
|
43
43
|
cy += 1;
|
|
44
44
|
c.text(box.x + 1, cy, 'signals: <field>.velocity (× its own pace) · <field>.delta (change in the window) · tags: launchpad: flag: verified: sus:', { fg: t.dim, bg }, box.w - 2);
|
|
45
45
|
cy += 2;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Build the skill backtest eval set from a recorder DB (task-1401). Read-only on the DB, no network.
|
|
2
|
+
//
|
|
3
|
+
// One row per coin. The label is the recorder's own outcome at the horizon (keyless proxy). The link evidence is
|
|
4
|
+
// the DexScreener / Jupiter token record the app's cache already holds (ns `ds:token` / `jup:token`), used ONLY
|
|
5
|
+
// when that record was fetched within `maxLagSec` AFTER the row's t0 anchor: the cache keeps the newest fetch per
|
|
6
|
+
// coin, and a coin that kept trading was re-fetched long after t0 (when it may have bought a DexScreener profile),
|
|
7
|
+
// while a coin that died stopped being fetched near t0. Taking a late record would leak the outcome into the
|
|
8
|
+
// evidence, so a coin with no record inside the window is excluded and counted, never back-filled.
|
|
9
|
+
//
|
|
10
|
+
// The rows keep the mint, t0, the label, URL-free link features and the t0 snapshot numbers. No URL, name or
|
|
11
|
+
// symbol is written (the name and symbol are read only to compute the domain-match boolean).
|
|
12
|
+
import { linkFeatures } from '../../data/src/index.js';
|
|
13
|
+
import { LABELER_VERSION } from '../../recorder/src/index.js';
|
|
14
|
+
import { SKILL_EVAL_SCHEMA } from '../../packs/src/index.js';
|
|
15
|
+
function readCache(db, ns, mint) {
|
|
16
|
+
const row = db.prepare('SELECT json, fetched_at FROM cache WHERE ns = ? AND key = ?').get(ns, mint);
|
|
17
|
+
if (!row)
|
|
18
|
+
return null;
|
|
19
|
+
try {
|
|
20
|
+
const j = JSON.parse(row.json);
|
|
21
|
+
const s = j.token?.socials ?? {};
|
|
22
|
+
return { fetched_at: Number(row.fetched_at), socials: { web: s.web ?? null, x: s.x ?? null, tg: s.tg ?? null }, symbol: j.token?.symbol ?? null, name: j.token?.name ?? null };
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
|
|
29
|
+
const bool = (v) => (v === null || v === undefined ? null : Number(v) !== 0);
|
|
30
|
+
export function buildSkillEvalSet(db, opts = {}) {
|
|
31
|
+
const horizon = opts.horizonSec ?? 259_200;
|
|
32
|
+
const lagMs = (opts.maxLagSec ?? 3_600) * 1000;
|
|
33
|
+
const testFrac = opts.testFrac ?? 0.3;
|
|
34
|
+
const excluded = { no_cached_link_record: 0, no_point_in_time_link_record: 0, no_t0_snapshot: 0, chain_verified_label: 0 };
|
|
35
|
+
const mints = db.prepare("SELECT DISTINCT mint FROM outcomes WHERE horizon_sec = ? AND label != 'unobserved'").all(horizon).map((r) => r.mint);
|
|
36
|
+
const outStmt = db.prepare("SELECT t0, label, label_tier FROM outcomes WHERE mint = ? AND horizon_sec = ? AND label != 'unobserved' ORDER BY t0 DESC");
|
|
37
|
+
const snapStmt = db.prepare('SELECT * FROM token_snapshots WHERE mint = ? AND t = ? AND is_t0 = 1 LIMIT 1');
|
|
38
|
+
const rows = [];
|
|
39
|
+
for (const mint of mints) {
|
|
40
|
+
const ds = readCache(db, 'ds:token', mint);
|
|
41
|
+
const jup = readCache(db, 'jup:token', mint);
|
|
42
|
+
if (!ds && !jup) {
|
|
43
|
+
excluded.no_cached_link_record++;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const outs = outStmt.all(mint, horizon);
|
|
47
|
+
let picked = null;
|
|
48
|
+
for (const o of outs) {
|
|
49
|
+
const t0 = Number(o.t0);
|
|
50
|
+
const srcs = [['jupiter', jup], ['dexscreener', ds]]
|
|
51
|
+
.filter((x) => !!x[1] && x[1].fetched_at >= t0 && x[1].fetched_at <= t0 + lagMs);
|
|
52
|
+
if (srcs.length) {
|
|
53
|
+
picked = { t0, label: o.label, label_tier: o.label_tier, srcs };
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!picked) {
|
|
58
|
+
excluded.no_point_in_time_link_record++;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (picked.label_tier !== 'keyless-proxy') {
|
|
62
|
+
excluded.chain_verified_label++;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const snap = snapStmt.get(mint, picked.t0);
|
|
66
|
+
if (!snap) {
|
|
67
|
+
excluded.no_t0_snapshot++;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
// The store's merge order: Jupiter first, then DexScreener (packages/data/src/store.ts).
|
|
71
|
+
const pick = (k) => picked.srcs.map(([, r]) => r.socials[k]).find((v) => v) ?? null;
|
|
72
|
+
const symbol = picked.srcs.map(([, r]) => r.symbol).find((v) => v) ?? snap.symbol;
|
|
73
|
+
const name = picked.srcs.map(([, r]) => r.name).find((v) => v) ?? snap.name;
|
|
74
|
+
const createdAt = num(snap.created_at);
|
|
75
|
+
rows.push({
|
|
76
|
+
mint, t0: picked.t0, split: 'train',
|
|
77
|
+
outcome: picked.label, bad: picked.label === 'rug' || picked.label === 'dead',
|
|
78
|
+
link: linkFeatures({ web: pick('web'), x: pick('x'), tg: pick('tg'), symbol, name }),
|
|
79
|
+
link_sources: picked.srcs.map(([s]) => s),
|
|
80
|
+
link_lag_sec: Math.round((Math.max(...picked.srcs.map(([, r]) => r.fetched_at)) - picked.t0) / 1000),
|
|
81
|
+
rug: {
|
|
82
|
+
freeze_auth: bool(snap.freeze_auth_enabled), mint_auth: bool(snap.mint_auth_enabled),
|
|
83
|
+
dev_pct: num(snap.dev_pct), top10_pct: num(snap.top_holders_pct), organic_score: num(snap.organic_score),
|
|
84
|
+
liquidity_usd: num(snap.liquidity_usd), price_usd: num(snap.price_usd), lp_locked_pct: num(snap.lp_locked_pct),
|
|
85
|
+
launchpad: snap.launchpad ?? null,
|
|
86
|
+
age_sec: createdAt ? Math.max(0, Math.round((picked.t0 - createdAt) / 1000)) : null,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
rows.sort((a, b) => a.t0 - b.t0 || a.mint.localeCompare(b.mint));
|
|
91
|
+
const cutIdx = Math.min(rows.length, Math.floor(rows.length * (1 - testFrac)));
|
|
92
|
+
const cutoff = rows[cutIdx]?.t0 ?? Number.MAX_SAFE_INTEGER;
|
|
93
|
+
for (const r of rows)
|
|
94
|
+
r.split = r.t0 >= cutoff ? 'test' : 'train';
|
|
95
|
+
const train = rows.filter((r) => r.split === 'train');
|
|
96
|
+
const test = rows.filter((r) => r.split === 'test');
|
|
97
|
+
return {
|
|
98
|
+
schema: SKILL_EVAL_SCHEMA,
|
|
99
|
+
built_at: new Date(opts.now ?? Date.now()).toISOString(),
|
|
100
|
+
source: { kind: 'recorder', labeler_version: LABELER_VERSION, db_note: 'the app history DB on the machine that built it (outcomes + cache + token_snapshots); not shipped' },
|
|
101
|
+
horizon_sec: horizon,
|
|
102
|
+
label_rule: `recorder outcome at ${horizon}s (keyless proxy, labeler ${LABELER_VERSION}); bad = rug ∪ dead, ok = flat ∪ fade ∪ runner; unobserved dropped`,
|
|
103
|
+
evidence_rule: `link features from the cached DexScreener/Jupiter token record only when it was fetched within ${lagMs / 1000}s after the row's t0 (latest such t0 per coin); rug-check features from that t0 snapshot`,
|
|
104
|
+
split_rule: `by time: the latest ${Math.round(testFrac * 100)}% of rows by t0 (t0 ≥ ${new Date(cutoff).toISOString()}) are the held-out TEST split, fixed before tuning`,
|
|
105
|
+
cutoff_t0: cutoff,
|
|
106
|
+
counts: { rows: rows.length, train: train.length, test: test.length, bad_train: train.filter((r) => r.bad).length, bad_test: test.filter((r) => r.bad).length, excluded },
|
|
107
|
+
rows,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// `dexterm spectate …` — watch an agent drive dexterm (task-1406, "watch the agent cook").
|
|
2
|
+
//
|
|
3
|
+
// dexterm spectate --replay FILE [--json] replay a saved session file (demos + bug reports): fold the
|
|
4
|
+
// recorded agent actions into the screen model + activity panel
|
|
5
|
+
// dexterm spectate --info [--json] show the live spectator stream endpoint (loopback + token)
|
|
6
|
+
// from ~/.dexterm/spectator.json, if a session is streaming
|
|
7
|
+
//
|
|
8
|
+
// Replay is deterministic and offline. The live stream itself is started by `dexterm agent serve --spectate`;
|
|
9
|
+
// it is read-only, loopback-only and token-gated, and redacts wallet addresses/keys (see SPECTATOR.md).
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { FollowController, loadSpectatorRecording } from '../../agents/src/index.js';
|
|
13
|
+
function flag(rest, name) { const i = rest.indexOf(name); return i >= 0 ? rest[i + 1] : undefined; }
|
|
14
|
+
function has(rest, name) { return rest.includes(name); }
|
|
15
|
+
const USAGE = `dexterm spectate <command>
|
|
16
|
+
--replay FILE [--json] replay a saved session (fold agent actions into the screen + activity panel)
|
|
17
|
+
--info [--json] show the live stream endpoint from ~/.dexterm/spectator.json (if streaming)
|
|
18
|
+
read-only · redacts wallet addresses/keys · the live stream is started by \`dexterm agent serve --spectate\``;
|
|
19
|
+
function ago(t) { const s = Math.max(0, Math.round((Date.now() - t) / 1000)); return s < 60 ? `${s}s` : s < 3600 ? `${Math.floor(s / 60)}m` : `${Math.floor(s / 3600)}h`; }
|
|
20
|
+
function age(s) { if (s == null)
|
|
21
|
+
return ''; return s < 60 ? ` · ${Math.round(s)}s old` : s < 3600 ? ` · ${Math.floor(s / 60)}m old` : ` · ${Math.floor(s / 3600)}h old`; }
|
|
22
|
+
function renderView(view, meta, out) {
|
|
23
|
+
const s = view.screen;
|
|
24
|
+
out('SCREEN');
|
|
25
|
+
const bits = [`screen ${s.screen}`];
|
|
26
|
+
if (s.selectedMint)
|
|
27
|
+
bits.push(`token ${s.selectedMint}`);
|
|
28
|
+
if (s.feed)
|
|
29
|
+
bits.push(`feed ${s.feed}`);
|
|
30
|
+
if (s.filter)
|
|
31
|
+
bits.push(`filter ${s.filter}`);
|
|
32
|
+
if (s.highlight)
|
|
33
|
+
bits.push(`★ ${s.highlight}`);
|
|
34
|
+
if (s.watchlist.length)
|
|
35
|
+
bits.push(`watchlist ${s.watchlist.length}`);
|
|
36
|
+
out(` ${bits.join(' · ')}`);
|
|
37
|
+
if (view.control === 'user')
|
|
38
|
+
out(' YOU HAVE TAKEN OVER (agent frozen)');
|
|
39
|
+
out('');
|
|
40
|
+
out('AGENTS');
|
|
41
|
+
if (!view.agents.length)
|
|
42
|
+
out(' (none)');
|
|
43
|
+
for (const a of view.agents)
|
|
44
|
+
out(` [agent] ${a.name} ${a.tier ?? '—'} ${a.status} · ${ago(a.lastAt)}`);
|
|
45
|
+
out('');
|
|
46
|
+
out(`ACTIVITY (${view.activity.length}${meta.redacted ? ' · redacted' : ' · full'})`);
|
|
47
|
+
for (const ev of view.activity) {
|
|
48
|
+
const ms = ev.ms != null ? ` ${Math.round(ev.ms)}ms` : '';
|
|
49
|
+
out(` ${ago(ev.t).padStart(4)} [agent] ${ev.agent} ${ev.phase.padEnd(8)} ${(ev.tool ?? '').padEnd(16)} ${ev.summary ?? ''}${ms}${age(ev.dataAgeSec)}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export async function runSpectate(rest, io) {
|
|
53
|
+
const out = (s) => io.stdout.write(s + '\n');
|
|
54
|
+
const errOut = (s) => io.stderr.write(s + '\n');
|
|
55
|
+
const replayFile = flag(rest, '--replay');
|
|
56
|
+
if (replayFile) {
|
|
57
|
+
let recording;
|
|
58
|
+
try {
|
|
59
|
+
recording = loadSpectatorRecording(replayFile);
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
errOut(`dexterm spectate: cannot read ${replayFile}: ${e.message}`);
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
const follow = new FollowController({ activityCap: 500 });
|
|
66
|
+
for (const ev of recording.events)
|
|
67
|
+
follow.apply(ev);
|
|
68
|
+
if (has(rest, '--json')) {
|
|
69
|
+
out(JSON.stringify({ meta: recording.meta, view: follow.view() }));
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
out(`replaying ${recording.events.length} event(s) from ${replayFile}${recording.meta.redacted ? ' (redacted)' : ''}`);
|
|
73
|
+
out('');
|
|
74
|
+
renderView(follow.view(), recording.meta, out);
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
if (has(rest, '--info')) {
|
|
78
|
+
const home = io.env.DEXTERM_HOME || join(io.env.HOME || '', '.dexterm');
|
|
79
|
+
const path = join(home, 'spectator.json');
|
|
80
|
+
let info;
|
|
81
|
+
try {
|
|
82
|
+
info = JSON.parse(readFileSync(path, 'utf8'));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
errOut('no live spectator stream (start one with `dexterm agent serve --spectate`).');
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
if (has(rest, '--json')) {
|
|
89
|
+
out(JSON.stringify(info));
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
out(`live spectator stream on ws://127.0.0.1:${info.port} (loopback only).`);
|
|
93
|
+
out(`token is in ${path} (mode 0600) — pass it in the viewer's hello; never on a command line.`);
|
|
94
|
+
out('the stream is read-only and redacts wallet addresses/keys.');
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
if (rest.length && !rest[0].startsWith('--')) {
|
|
98
|
+
errOut(`dexterm spectate: unknown subcommand ${JSON.stringify(rest[0])}`);
|
|
99
|
+
errOut(USAGE);
|
|
100
|
+
return 2;
|
|
101
|
+
}
|
|
102
|
+
out(USAGE);
|
|
103
|
+
return rest.length ? 2 : 0;
|
|
104
|
+
}
|
|
@@ -120,7 +120,7 @@ export function plansRows(state) {
|
|
|
120
120
|
return [];
|
|
121
121
|
return state.plansUi?.tab === 'mandates' ? svc.listMandates() : svc.listPlans();
|
|
122
122
|
}
|
|
123
|
-
export function newFollowsUi() { return { selected: 0, input: null, notice: null, mirror: null, autoModal: null }; }
|
|
123
|
+
export function newFollowsUi() { return { selected: 0, input: null, notice: null, mirror: null, autoModal: null, detail: null }; }
|
|
124
124
|
/**
|
|
125
125
|
* Tier 3: the auto-mirror service for this run, attached to the copy binding on first use. It sits on the plan
|
|
126
126
|
* service (the signer trust domain) and the copy binding's market resolver; its journal is ~/.dexterm/copy.journal.
|