dexterm-cli 0.1.2 → 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 +37 -1
- package/dist/packages/agents/src/follow.js +105 -0
- 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/cli/assets/claude/analyst-system.md +6 -1
- package/dist/packages/cli/src/agent-cmd.js +71 -7
- package/dist/packages/cli/src/agent-spectator.js +37 -0
- package/dist/packages/cli/src/alerts/format.js +25 -0
- package/dist/packages/cli/src/app-help-gen.js +365 -0
- package/dist/packages/cli/src/doctor.js +6 -5
- 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 +64 -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 +355 -10
- package/dist/packages/cli/src/mcp-cmd.js +2 -7
- 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 +50 -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/spectate-cmd.js +104 -0
- package/dist/packages/cli/src/state.js +1 -1
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/cli/src/voice-cmd.js +86 -11
- package/dist/packages/cli/src/voice-hud.js +1 -1
- package/dist/packages/custom/src/rules.js +2 -1
- package/dist/packages/data/src/analyst-actions.js +130 -3
- package/dist/packages/data/src/fast-lane.js +16 -2
- package/dist/packages/data/src/index.js +1 -0
- package/dist/packages/data/src/intel/live-signals.js +185 -0
- 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 +90 -3
- package/dist/packages/mcp/src/spectator-sink.js +72 -0
- package/dist/packages/packs/src/index.js +2 -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/keymap.js +10 -0
- package/dist/packages/voice/src/controller.js +42 -1
- package/dist/packages/voice/src/doctor.js +25 -8
- package/dist/packages/voice/src/engines.js +8 -1
- package/dist/packages/voice/src/index.js +3 -1
- package/dist/packages/voice/src/speakable.js +111 -0
- package/dist/packages/voice/src/tts/piper-setup.js +229 -0
- package/dist/packages/voice/src/tts/piper.js +309 -66
- package/dist/packages/voice/src/tts/say.js +3 -2
- package/dist/packages/voice/src/tts/voices.js +8 -5
- package/package.json +1 -1
|
@@ -15,8 +15,11 @@
|
|
|
15
15
|
// Helius key (keyless prints the fix) and reads public history. Secrets never print: the RPC URL stays inside the
|
|
16
16
|
// data layer.
|
|
17
17
|
import { createDataLayer, resolveRpcUrl, HeliusHistory } from '../../data/src/index.js';
|
|
18
|
-
import { FollowStore, WalletTracker, defaultFollowsPath, isWalletAddress, shortAddr, CopyAuto, defaultCopyJournalPath, AUTO_HARD, autoSizeCap } from '../../data/src/trade/copy/index.js';
|
|
18
|
+
import { FollowStore, WalletTracker, defaultFollowsPath, isWalletAddress, shortAddr, fmtHoldMs, CopyAuto, defaultCopyJournalPath, AUTO_HARD, autoSizeCap, buildMirror, leaderSoldSince } from '../../data/src/trade/copy/index.js';
|
|
19
19
|
import { PlanService, defaultPlansJournalPath } from '../../data/src/trade/plans/index.js';
|
|
20
|
+
import { CopyLive } from "./live/copy.js";
|
|
21
|
+
/** Default per-mirror cap for the headless path (same as the TUI's MIRROR_DEFAULT_CAP_USD); overridable with --cap. */
|
|
22
|
+
export const FOLLOW_MIRROR_DEFAULT_CAP_USD = 20;
|
|
20
23
|
function flag(rest, name) { const i = rest.indexOf(name); return i >= 0 ? rest[i + 1] : undefined; }
|
|
21
24
|
function has(rest, name) { return rest.includes(name); }
|
|
22
25
|
const USAGE = `dexterm follow <command>
|
|
@@ -25,6 +28,9 @@ const USAGE = `dexterm follow <command>
|
|
|
25
28
|
list [--json]
|
|
26
29
|
stats <addr> [--json] one Helius history fetch: trades, FIFO realised PnL, win rate (hidden under n=10)
|
|
27
30
|
trades <addr> [--limit N] the decoded swaps behind the stats
|
|
31
|
+
mirror <addr> [--cap USD] [--k X] [--propose] [--json]
|
|
32
|
+
preview a MIRROR TICKET from this wallet's latest BUY (checks + size);
|
|
33
|
+
--propose writes it as an INERT order plan (arm only in the app, keypress/Phantom gate)
|
|
28
34
|
purge --yes delete every follow and label
|
|
29
35
|
auto <addr> [--per-trade USD] [--daily USD] [--max-open N] [--cooldown SEC] [--k X] [--disarm N] [--off]
|
|
30
36
|
draft the auto-mirror caps (always OFF here: enabling is a keypress in the app, U then A,
|
|
@@ -35,7 +41,9 @@ function fmtStats(s) {
|
|
|
35
41
|
`${shortAddr(s.wallet)} ${s.trades} trades (${s.buys} buys · ${s.sells} sells · ${s.transfers} transfers) over ${s.mints} mints · ${s.sample.note}`,
|
|
36
42
|
`realised ${s.realisedSol >= 0 ? '+' : ''}${s.realisedSol.toFixed(4)} SOL${s.realisedUsd !== null ? ` (${s.realisedUsd >= 0 ? '+' : ''}$${s.realisedUsd.toFixed(2)} at $${s.solUsdAt}/SOL now)` : ' (no SOL price: USD not shown)'} · FIFO over fetched history, fees and tips included`,
|
|
37
43
|
`win rate ${s.winRate === null ? '—' : `${(s.winRate * 100).toFixed(0)}%`} · ${s.winRateNote} · open positions ${s.open}`,
|
|
44
|
+
`hold time ${fmtHoldMs(s.holdMedianMs)} · ${s.holdNote}`,
|
|
38
45
|
`early-entry score ${s.earlyEntry.score === null ? '—' : s.earlyEntry.score.toFixed(2)} · ${s.earlyEntry.note}`,
|
|
46
|
+
`source ${s.sample.source}${s.sample.asOf ? ` · as of ${new Date(s.sample.asOf).toISOString().replace('T', ' ').slice(0, 19)} (${Math.max(0, Math.round((Date.now() - s.sample.asOf) / 1000))}s ago)` : ' · not fetched yet'}`,
|
|
39
47
|
];
|
|
40
48
|
for (const p of s.positions.slice(0, 12))
|
|
41
49
|
out.push(` ${shortAddr(p.mint)} ${p.closed ? 'closed' : 'open '} realised ${p.realisedSol >= 0 ? '+' : ''}${p.realisedSol.toFixed(4)} SOL ${p.buys}b/${p.sells}s${p.flagged ? ' (cost-0 lot: airdrop/transfer/unpriced leg)' : ''}`);
|
|
@@ -209,6 +217,104 @@ export async function runFollow(rest, io, over = {}) {
|
|
|
209
217
|
dl?.close();
|
|
210
218
|
}
|
|
211
219
|
}
|
|
220
|
+
// task-1400 Tier 2 (headless): the one-key MIRROR TICKET from a shell. Reuses the exact TUI path — poll the
|
|
221
|
+
// tracker, take the wallet's latest BUY alert, resolve its market, run buildMirror. --propose writes the result
|
|
222
|
+
// as an INERT `proposed` order plan through PlanService; it is armable ONLY through the app's keypress / Phantom
|
|
223
|
+
// gate. There is no arm/sign/execute path in this command (the plan engine refuses to arm without a gesture and
|
|
224
|
+
// an armed mandate), so a pipe or an agent calling `follow mirror --propose` can never trade.
|
|
225
|
+
case 'mirror': {
|
|
226
|
+
const addr = rest[1];
|
|
227
|
+
if (!isWalletAddress(addr)) {
|
|
228
|
+
errOut('usage: dexterm follow mirror <addr> [--cap USD] [--k X] [--propose] [--json]');
|
|
229
|
+
return 2;
|
|
230
|
+
}
|
|
231
|
+
if (!store.has(addr)) {
|
|
232
|
+
errOut(`dexterm follow mirror: ${shortAddr(addr)} is not followed (dexterm follow add <addr> first)`);
|
|
233
|
+
return 1;
|
|
234
|
+
}
|
|
235
|
+
const capUsd = flag(rest, '--cap') ? Number(flag(rest, '--cap')) : FOLLOW_MIRROR_DEFAULT_CAP_USD;
|
|
236
|
+
if (!(capUsd > 0)) {
|
|
237
|
+
errOut('dexterm follow mirror: --cap needs a USD number > 0');
|
|
238
|
+
return 2;
|
|
239
|
+
}
|
|
240
|
+
const kRaw = flag(rest, '--k');
|
|
241
|
+
const k = kRaw ? Number(kRaw) : undefined;
|
|
242
|
+
if (kRaw !== undefined && !(k > 0)) {
|
|
243
|
+
errOut('dexterm follow mirror: --k needs a number > 0');
|
|
244
|
+
return 2;
|
|
245
|
+
}
|
|
246
|
+
let dl = null;
|
|
247
|
+
let history;
|
|
248
|
+
if (over.history !== undefined)
|
|
249
|
+
history = over.history;
|
|
250
|
+
else {
|
|
251
|
+
const rpc = resolveRpcUrl({ env: io.env });
|
|
252
|
+
if (rpc.provider !== 'helius')
|
|
253
|
+
history = null;
|
|
254
|
+
else {
|
|
255
|
+
dl = createDataLayer({ rpcUrl: rpc.url, rpcFallbackUrl: rpc.fallbackUrl });
|
|
256
|
+
history = dl.rpcPool.heliusUrl ? new HeliusHistory(dl.http, dl.rpcPool.heliusUrl) : null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
const copy = new CopyLive(dl, { followsPath: null, history, solUsd: () => over.solUsd ?? null, ...(over.clock ? { clock: over.clock } : {}) });
|
|
261
|
+
copy.store.add(addr);
|
|
262
|
+
if (copy.status().state === 'unavailable') {
|
|
263
|
+
errOut(`dexterm follow mirror: ${copy.status().why}`);
|
|
264
|
+
return 1;
|
|
265
|
+
}
|
|
266
|
+
await copy.tracker.pollOnce();
|
|
267
|
+
const after = copy.status();
|
|
268
|
+
if (after.state === 'degraded') {
|
|
269
|
+
errOut(`dexterm follow mirror: ${after.why}`);
|
|
270
|
+
return 1;
|
|
271
|
+
}
|
|
272
|
+
// Prefer a live "news" buy alert (a buy the tracker saw AFTER priming). A one-shot shell call primes and
|
|
273
|
+
// sees the history in a single poll, so there is usually no news alert yet; fall back to the latest BUY in
|
|
274
|
+
// the fetched history, synthesising the same CopyAlert shape and captioning it as history, not live news.
|
|
275
|
+
let alert = copy.alerts(200).find((a) => a.wallet === addr && a.trade.side === 'buy');
|
|
276
|
+
let fromHistory = false;
|
|
277
|
+
if (!alert) {
|
|
278
|
+
const lastBuy = copy.trades(addr).filter((t) => t.side === 'buy').sort((a, b) => b.slot - a.slot || b.sig.localeCompare(a.sig))[0];
|
|
279
|
+
if (!lastBuy) {
|
|
280
|
+
out(`no BUY from ${shortAddr(addr)} in the fetched history (nothing to mirror)`);
|
|
281
|
+
return 0;
|
|
282
|
+
}
|
|
283
|
+
alert = { id: `copy_hist_${lastBuy.sig.slice(0, 12)}`, t: copy.now(), wallet: addr, name: store.get(addr)?.name ?? null, trade: lastBuy, lagMs: lastBuy.tKnown ? Math.max(0, copy.now() - lastBuy.t) : null };
|
|
284
|
+
fromHistory = true;
|
|
285
|
+
}
|
|
286
|
+
const market = over.mirrorMarket ? await over.mirrorMarket(alert.trade.mint) : await copy.mirrorMarket(alert.trade.mint);
|
|
287
|
+
const soldSince = leaderSoldSince(alert, copy.trades(addr));
|
|
288
|
+
// No wallet-intel scan from a shell: the leader's dev-cluster / holdings stay unknown (shown as such).
|
|
289
|
+
const result = buildMirror(alert, { now: copy.now(), capUsd, k, market, leaderSoldSince: soldSince, leaderIntel: null });
|
|
290
|
+
if (has(rest, '--json')) {
|
|
291
|
+
out(JSON.stringify(result));
|
|
292
|
+
return result.ok ? 0 : 1;
|
|
293
|
+
}
|
|
294
|
+
out(`mirror of ${shortAddr(addr)}'s latest BUY — ${shortAddr(alert.trade.mint)}${market.symbol ? ` (${market.symbol})` : ''}${fromHistory ? ' · from fetched history (not a live alert)' : ' · live alert'}`);
|
|
295
|
+
for (const c of result.checks)
|
|
296
|
+
out(` ${c.ok === true ? 'ok ' : c.ok === false ? 'NO ' : '? '} ${c.name.padEnd(12)} ${c.detail}`);
|
|
297
|
+
if (!result.ok) {
|
|
298
|
+
errOut(`refused: ${result.reason} — ${result.message}`);
|
|
299
|
+
return 1;
|
|
300
|
+
}
|
|
301
|
+
out(` size $${result.sizeUsd.toFixed(2)} = ${result.sizeWhy}`);
|
|
302
|
+
if (!has(rest, '--propose')) {
|
|
303
|
+
out('preview only · add --propose to write the inert plan (then arm it in the app: O screen, hold a — keypress/Phantom gate)');
|
|
304
|
+
out('not a recommendation · a wallet\'s past fills say nothing about its next one');
|
|
305
|
+
return 0;
|
|
306
|
+
}
|
|
307
|
+
const plans = new PlanService({ journalPath: over.plansJournalPath === undefined ? defaultPlansJournalPath(io.env) : over.plansJournalPath });
|
|
308
|
+
const plan = plans.propose(result.input, { supply: result.supply, by: 'copy', ref: result.ref });
|
|
309
|
+
out(`proposed ${plan.id} · ${plan.status.toUpperCase()} (${plan.side} ${plan.symbol} ≈ $${plan.sizeUsd.toFixed(2)}) · INERT — nothing is armed`);
|
|
310
|
+
out('arm it in the app: U → O (the plans screen), select it, hold a (keypress) or approve in Phantom · the gate stays on');
|
|
311
|
+
out('not a recommendation · a wallet\'s past fills say nothing about its next one');
|
|
312
|
+
return 0;
|
|
313
|
+
}
|
|
314
|
+
finally {
|
|
315
|
+
dl?.close();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
212
318
|
default:
|
|
213
319
|
if (cmd) {
|
|
214
320
|
errOut(`dexterm follow: unknown subcommand ${JSON.stringify(cmd)}`);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// `dexterm help [topic]` (task-1413) — the app-help knowledge base from the command line, backed by the
|
|
2
|
+
// same generated entries as the analyst's `app_help` MCP tool. No network, no model.
|
|
3
|
+
// dexterm help list the topics
|
|
4
|
+
// dexterm help export show the best-matching topic (exact key/command + how-to)
|
|
5
|
+
// dexterm help screen token multi-word query
|
|
6
|
+
import { loadAppHelp, searchAppHelp, getAppHelp } from '../../mcp/src/index.js';
|
|
7
|
+
function show(e, io) {
|
|
8
|
+
io.stdout.write(`\n${e.title} (${e.id})\n`);
|
|
9
|
+
io.stdout.write(`${'─'.repeat(Math.min(60, e.title.length + e.id.length + 4))}\n`);
|
|
10
|
+
if (e.keys.length)
|
|
11
|
+
io.stdout.write(`Keys: ${e.keys.join(' ')}\n`);
|
|
12
|
+
if (e.commands.length)
|
|
13
|
+
io.stdout.write(`Commands: ${e.commands.map((c) => `dexterm ${c}`).join(' ')}\n`);
|
|
14
|
+
if (e.flags.length)
|
|
15
|
+
io.stdout.write(`Flags: ${e.flags.join(' ')}\n`);
|
|
16
|
+
io.stdout.write(`\n${e.body}\n`);
|
|
17
|
+
}
|
|
18
|
+
export function helpCommand(argv, io) {
|
|
19
|
+
const entries = loadAppHelp();
|
|
20
|
+
if (!entries.length) {
|
|
21
|
+
io.stderr.write('no app-help entries found\n');
|
|
22
|
+
return 1;
|
|
23
|
+
}
|
|
24
|
+
const query = argv.join(' ').trim();
|
|
25
|
+
if (!query) {
|
|
26
|
+
io.stdout.write('dexterm help <topic> — how the app works. Topics:\n\n');
|
|
27
|
+
for (const e of entries)
|
|
28
|
+
io.stdout.write(` ${e.id.padEnd(26)} ${e.title}\n`);
|
|
29
|
+
io.stdout.write('\nExamples: dexterm help export · dexterm help voice speed · dexterm help screener\n');
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
// Exact id wins; otherwise the best retrieval match, with near-misses listed.
|
|
33
|
+
const exact = getAppHelp(query.replace(/\s+/g, '-'), entries) ?? getAppHelp(query, entries);
|
|
34
|
+
if (exact) {
|
|
35
|
+
show(exact, io);
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
const results = searchAppHelp(query, 5, entries);
|
|
39
|
+
if (!results.length) {
|
|
40
|
+
io.stdout.write(`No app-help topic matches "${query}". Run \`dexterm help\` to list topics.\n`);
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
show(results[0], io);
|
|
44
|
+
if (results.length > 1) {
|
|
45
|
+
io.stdout.write(`\nRelated: ${results.slice(1).map((r) => r.id).join(', ')}\n`);
|
|
46
|
+
}
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
@@ -10,15 +10,24 @@ import { NewPairsLive } from "./newpairs.js";
|
|
|
10
10
|
import { WatchlistLive } from "./watchlist.js";
|
|
11
11
|
import { TradeTicketLive, PositionsLive, LivePositions, CombinedPositions, POSITIONS_REFRESH_MS } from "./trade.js";
|
|
12
12
|
import { IntelLive } from "./intel.js";
|
|
13
|
+
import { LiveSignalsLive } from "./signals.js";
|
|
13
14
|
import { CopyLive } from "./copy.js";
|
|
14
15
|
export { TokenLive, INTERVALS, panelCaption, emptyView } from "./token.js";
|
|
15
16
|
export { NewPairsLive, latencyStats, percentile, RECENT_POLL_MS, NEW_TOKENS_POLL_MS, ENRICH_POLL_MS } from "./newpairs.js";
|
|
16
17
|
export { WatchlistLive, WATCH_REFRESH_MS } from "./watchlist.js";
|
|
17
18
|
export { TradeTicketLive, PositionsLive, LivePositions, CombinedPositions, POSITIONS_REFRESH_MS } from "./trade.js";
|
|
18
19
|
export { IntelLive, popupBubbleVM, unavailableText, INTEL_AUTOSCAN_DELAY_MS } from "./intel.js";
|
|
20
|
+
export { LiveSignalsLive, buildObservation, SIGNALS_LOG_MAX } from "./signals.js";
|
|
19
21
|
export { renderTokenLive } from "./screens/token-live.js";
|
|
20
22
|
export { renderNewPairsLive } from "./screens/newpairs-live.js";
|
|
21
23
|
export { renderWatchlistLive } from "./screens/watchlist-live.js";
|
|
24
|
+
/** task-1404: how often the open token's credit-metered wallet-intel leg (holders/bundle/sniper/fresh) is re-scanned. */
|
|
25
|
+
export const INTEL_RESCAN_MS = 45_000;
|
|
26
|
+
/** A transient one-liner for a fired live signal (⚑ SYMBOL-less; the token screen shows the full strip). */
|
|
27
|
+
function signalNotice(s) {
|
|
28
|
+
const age = s.dataAgeMs >= 1000 ? ` · ${Math.round(s.dataAgeMs / 1000)}s` : '';
|
|
29
|
+
return `⚑ ${s.title}: ${s.text} (${s.source}${age})`;
|
|
30
|
+
}
|
|
22
31
|
export class LiveContext {
|
|
23
32
|
dl;
|
|
24
33
|
mode;
|
|
@@ -54,6 +63,8 @@ export class LiveContext {
|
|
|
54
63
|
token = null;
|
|
55
64
|
/** task-1340 wallet intel: the holder scan per mint (light on open, deep for watched coins) behind the bubble map. */
|
|
56
65
|
intel;
|
|
66
|
+
/** task-1404 LIVE holder/rug signals: turns the open token's repeated holder/safety reads into signal events. */
|
|
67
|
+
signals;
|
|
57
68
|
/** task-1360 copy trading Tier 1: the follow store + wallet tracker (alerts on a followed wallet's buys/sells). */
|
|
58
69
|
copy;
|
|
59
70
|
/**
|
|
@@ -86,6 +97,8 @@ export class LiveContext {
|
|
|
86
97
|
onLog;
|
|
87
98
|
lastFastLogAt = 0;
|
|
88
99
|
lastFastLog = '';
|
|
100
|
+
/** task-1404: last time the open token's wallet-intel leg was re-scanned (slow cadence; see refreshOpenIntel). */
|
|
101
|
+
lastIntelRescanAt = 0;
|
|
89
102
|
constructor(dl, opts) {
|
|
90
103
|
this.dl = dl;
|
|
91
104
|
this.mode = opts.mode;
|
|
@@ -124,7 +137,7 @@ export class LiveContext {
|
|
|
124
137
|
endpoints: opts.fastEndpoints,
|
|
125
138
|
solUsd: () => this.solUsd,
|
|
126
139
|
ws: opts.ws,
|
|
127
|
-
onPrice: (_p) => emit('fastprice'),
|
|
140
|
+
onPrice: (_p) => { this.observeSignals(); emit('fastprice'); },
|
|
128
141
|
onStatus: () => { emit('fastlane:status'); this.logFast(); },
|
|
129
142
|
})
|
|
130
143
|
: null;
|
|
@@ -166,7 +179,10 @@ export class LiveContext {
|
|
|
166
179
|
? new LivePositions(this.trade.paths?.liveLedger ?? defaultLiveLedgerPath(), { now: this.now, mark: (m) => this.positions.markOf(m), solUsd: () => this.positions.solUsd() ?? this.solUsd })
|
|
167
180
|
: null;
|
|
168
181
|
this.positionSource = new CombinedPositions(this.positions, this.livePositions);
|
|
169
|
-
this.intel = new IntelLive(dl, { now: this.now, onChange: () => { this.syncIntelRows(); emit('intel'); }, isWatched: (mint) => this.watch.has(mint) });
|
|
182
|
+
this.intel = new IntelLive(dl, { now: this.now, onChange: () => { this.syncIntelRows(); this.observeSignals(); emit('intel'); }, isWatched: (mint) => this.watch.has(mint) });
|
|
183
|
+
// task-1404: the live holder/rug signals engine. A fired signal sets a transient notice and redraws; the
|
|
184
|
+
// token screen shows the recent signals, and the rules engine reads `signals.fields()` so each is an alert trigger.
|
|
185
|
+
this.signals = new LiveSignalsLive({ now: this.now, onSignal: (s) => { this.notice = signalNotice(s); emit('signals'); } });
|
|
170
186
|
// task-1360: followed wallets are polled only in a real live run (replay never spends credits); the store is always readable.
|
|
171
187
|
this.copy = new CopyLive(opts.mode === 'live' ? dl : null, { onChange: (r) => emit(r), solUsd: () => this.solUsd, env: opts.copy?.env, followsPath: opts.copy?.followsPath, history: opts.copy?.history, pollMs: opts.copy?.pollMs });
|
|
172
188
|
}
|
|
@@ -376,7 +392,7 @@ export class LiveContext {
|
|
|
376
392
|
}
|
|
377
393
|
/** Open a token: a fresh loader, first load kicked off; the view is readable immediately (row fields). */
|
|
378
394
|
openToken(row) {
|
|
379
|
-
const t = new TokenLive(this.dl, row, { interval: this.token?.view.interval ?? this.interval, now: this.now, onChange: () => { this.syncChainRows(t); this.onChange('token'); } });
|
|
395
|
+
const t = new TokenLive(this.dl, row, { interval: this.token?.view.interval ?? this.interval, now: this.now, onChange: () => { this.syncChainRows(t); this.observeSignals(); this.onChange('token'); } });
|
|
380
396
|
this.token = t;
|
|
381
397
|
this.syncFastSet(); // promote the opened token to the fast lane (slow→fast, no seam)
|
|
382
398
|
this.x?.setFocus({ mint: row.mint, symbol: row.symbol }); // task-1357: the open token's X search (60 s, cached 15 min)
|
|
@@ -423,6 +439,51 @@ export class LiveContext {
|
|
|
423
439
|
if (sigs.length)
|
|
424
440
|
this.social.ingest(sigs.map((s) => chainSignalToRaw(s, this.now())));
|
|
425
441
|
}
|
|
442
|
+
/** The current inputs for the live-signals engine: the open token's view + its settled intel scan + fast-lane curve. */
|
|
443
|
+
signalInputs() {
|
|
444
|
+
if (!this.token)
|
|
445
|
+
return null;
|
|
446
|
+
const mint = this.token.view.mint;
|
|
447
|
+
const result = this.intel.entry(mint)?.result;
|
|
448
|
+
const ok = result && result.state === 'ok' ? result : null;
|
|
449
|
+
return {
|
|
450
|
+
view: this.token.view,
|
|
451
|
+
intel: ok ? { analysis: ok.analysis, fetchedAt: ok.fetchedAt, via: ok.via.history ? 'helius' : ok.via.holders } : null,
|
|
452
|
+
curve: this.fastLane?.curve(mint) ?? null,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* task-1404: feed one moment of the open token's holder/rug state to the live-signals engine. Called whenever the
|
|
457
|
+
* token, the intel scan or a fast-lane push changed something; the engine only emits on a real threshold-crossing
|
|
458
|
+
* change, so this is cheap to call on every redraw. Nothing fires until a second observation of the same field.
|
|
459
|
+
*/
|
|
460
|
+
observeSignals() {
|
|
461
|
+
if (this.closed)
|
|
462
|
+
return;
|
|
463
|
+
const inputs = this.signalInputs();
|
|
464
|
+
if (inputs)
|
|
465
|
+
this.signals.observe(inputs);
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* task-1404: re-run the open token's wallet-intel scan on a slow cadence so holder/concentration/bundle/sniper
|
|
469
|
+
* signals stay LIVE, not computed once on open. Gated: live mode, the history leg can actually run (else a scan
|
|
470
|
+
* would only spend public-RPC calls to still print a dash), not already in flight, and at most once per
|
|
471
|
+
* INTEL_RESCAN_MS. The cheap on-chain reads (authorities, LP, liq, price) already refresh with token.load()
|
|
472
|
+
* every 10 s and bonding streams sub-second; this covers the credit-metered Helius leg.
|
|
473
|
+
*/
|
|
474
|
+
refreshOpenIntel(now) {
|
|
475
|
+
if (this.mode !== 'live' || !this.token)
|
|
476
|
+
return;
|
|
477
|
+
if (now - this.lastIntelRescanAt < INTEL_RESCAN_MS)
|
|
478
|
+
return;
|
|
479
|
+
if (!this.intel.historyAvailable().ok)
|
|
480
|
+
return;
|
|
481
|
+
const v = this.token.view;
|
|
482
|
+
if (this.intel.entry(v.mint)?.state === 'loading')
|
|
483
|
+
return;
|
|
484
|
+
this.lastIntelRescanAt = now;
|
|
485
|
+
void this.intel.scan({ mint: v.mint, symbol: v.symbol, name: v.name, holderCount: v.holders ?? null }, { force: true }).catch(() => { });
|
|
486
|
+
}
|
|
426
487
|
/** The token screen's `1`..`6` keys. */
|
|
427
488
|
setTimeframe(n) {
|
|
428
489
|
const iv = INTERVALS[n - 1];
|
|
@@ -214,6 +214,30 @@ export function renderTokenLive(c, state, v) {
|
|
|
214
214
|
sy += 1;
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
|
+
// task-1404: LIVE holder/rug signals — the changes seen since the token was opened (dev sell, concentration
|
|
218
|
+
// shift, LP unlock, authority flip, new bundle/sniper, fresh inflow, bonding, migration). Each is a risk flag,
|
|
219
|
+
// not a verdict, and carries its SOURCE and the AGE of the data it read; the `stale` mark shows honestly.
|
|
220
|
+
const sigs = state.live?.signals?.recent(v.mint, 4) ?? [];
|
|
221
|
+
if (sigs.length && sy < sb.y + sb.h) {
|
|
222
|
+
sy += sy + 1 < sb.y + sb.h ? 1 : 0;
|
|
223
|
+
if (sy < sb.y + sb.h) {
|
|
224
|
+
c.text(sb.x + 1, sy, 'LIVE SIGNALS', { fg: t.dim, bg: sb.bg });
|
|
225
|
+
sy += 1;
|
|
226
|
+
}
|
|
227
|
+
const staleAfter = 60_000;
|
|
228
|
+
for (const s of sigs) {
|
|
229
|
+
if (sy >= sb.y + sb.h)
|
|
230
|
+
break;
|
|
231
|
+
const color = s.level === 'warn' ? t.warn : s.level === 'down' ? t.down : s.level === 'up' ? t.up : t.muted;
|
|
232
|
+
const ageMs = Math.max(0, now - s.at);
|
|
233
|
+
const stale = ageMs > staleAfter;
|
|
234
|
+
const meta = `${s.source} · ${ageMs >= 1000 ? `${Math.round(ageMs / 1000)}s` : 'now'}${stale ? ' · stale' : ''}`;
|
|
235
|
+
const metaClip = clip(meta, Math.max(8, Math.floor((sb.w - 2) / 2)));
|
|
236
|
+
c.text(sb.x + 1, sy, `⚑ ${s.title}`, { fg: color, bg: sb.bg, b: s.level === 'warn' || s.level === 'down' }, sb.w - 2 - metaClip.length - 1);
|
|
237
|
+
c.textR(sb.x + 1, sy, sb.w - 2, metaClip, { fg: stale ? t.warn : t.dim, bg: sb.bg, i: true });
|
|
238
|
+
sy += 1;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
217
241
|
if (bp !== 'compact') {
|
|
218
242
|
const hx = statsW;
|
|
219
243
|
const hw = mainW - statsW;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// dexterm — LIVE holder/rug SIGNALS binding (task-1404). Turns the open token's repeatedly-refreshed holder and
|
|
2
|
+
// safety reads (market/authorities/rugcheck panels + the wallet-intel scan + the fast-lane bonding curve) into
|
|
3
|
+
// SIGNAL events through the pure @dexterm/data LiveSignalsEngine, keeps a small per-mint log for the token screen,
|
|
4
|
+
// and exposes the latest numeric values as RULE FIELDS so every signal is an alert trigger (task-1398 pattern).
|
|
5
|
+
//
|
|
6
|
+
// It owns no timers and does no I/O: the UI loop / LiveContext calls observe() whenever the token, the intel scan
|
|
7
|
+
// or a fast-lane push changed something. The engine dedups (only a threshold-crossing change emits), so calling it
|
|
8
|
+
// on every redraw is cheap. Read-only: nothing here trades or signs.
|
|
9
|
+
import { LiveSignalsEngine, bondingProgress, } from '../../../data/src/index.js';
|
|
10
|
+
/** Max signals kept per mint for the token-screen strip (newest first). */
|
|
11
|
+
export const SIGNALS_LOG_MAX = 40;
|
|
12
|
+
function fromPanel(p, value, fallbackSource) {
|
|
13
|
+
if (!p || value == null || !Number.isFinite(value) || p.fetchedAt == null)
|
|
14
|
+
return null;
|
|
15
|
+
return { value, source: p.via ?? fallbackSource, fetchedAt: p.fetchedAt, stale: p.stale || p.state === 'error' };
|
|
16
|
+
}
|
|
17
|
+
function authFromPanel(p, present) {
|
|
18
|
+
if (!p || present == null || p.fetchedAt == null)
|
|
19
|
+
return null;
|
|
20
|
+
return { present, source: p.via ?? 'authorities', fetchedAt: p.fetchedAt, stale: p.stale || p.state === 'error' };
|
|
21
|
+
}
|
|
22
|
+
function fromIntel(value, intel) {
|
|
23
|
+
if (value == null || !Number.isFinite(value) || !intel || intel.fetchedAt == null)
|
|
24
|
+
return null;
|
|
25
|
+
return { value, source: intel.via ?? 'helius', fetchedAt: intel.fetchedAt, stale: false };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Build one observation from the token view + intel scan + fast-lane curve. Each field keeps its OWN source and
|
|
29
|
+
* fetch time so a signal is honest about where it came from and how old the data is. The richer wallet-intel
|
|
30
|
+
* figures (ex-pool top-10, creator %, bundled, snipers, fresh, cluster) are preferred when the scan has landed;
|
|
31
|
+
* otherwise the panel reads (Jupiter audit top-10/dev) stand in. Pure + exported for the tests.
|
|
32
|
+
*/
|
|
33
|
+
export function buildObservation(inputs, now) {
|
|
34
|
+
const v = inputs.view;
|
|
35
|
+
const I = inputs.intel?.analysis?.insights ?? null;
|
|
36
|
+
const panels = v.panels ?? {};
|
|
37
|
+
const obs = { mint: v.mint, at: now };
|
|
38
|
+
// top-10 concentration (pools excluded): the scan's ex-pool figure first, else the holders panel's top-10.
|
|
39
|
+
obs.top10 = (I && typeof I.top10.exPool === 'number' ? fromIntel(I.top10.exPool, inputs.intel) : null) ?? fromPanel(panels.holders, v.top10, 'holders');
|
|
40
|
+
// creator / dev share: the scan's creatorPctNow first, else the market panel's Jupiter audit devPct.
|
|
41
|
+
obs.dev = (I ? fromIntel(I.dev.creatorPctNow, inputs.intel) : null) ?? fromPanel(panels.market, v.dev, 'jupiter');
|
|
42
|
+
// bundled / snipers / fresh / cluster only exist once the launch window / funders were read (Helius).
|
|
43
|
+
if (I && I.coverage.launchTxs > 0) {
|
|
44
|
+
obs.bundled = fromIntel(I.bundle.bundlePctOfTopSample, inputs.intel);
|
|
45
|
+
obs.snipers = fromIntel(I.snipers.count, inputs.intel);
|
|
46
|
+
}
|
|
47
|
+
if (I && I.coverage.fundingLookups > 0)
|
|
48
|
+
obs.fresh = fromIntel(I.fresh.freshAtBuyPctOfTopSample, inputs.intel);
|
|
49
|
+
const cluster = inputs.intel?.analysis?.clusters?.[0]?.pct ?? null;
|
|
50
|
+
obs.cluster = fromIntel(cluster, inputs.intel);
|
|
51
|
+
// LP locked (RugCheck), liquidity (market), mint/freeze authority (authorities panel).
|
|
52
|
+
obs.lpLocked = fromPanel(panels.rugcheck, v.lpLocked, 'rugcheck');
|
|
53
|
+
obs.liq = fromPanel(panels.market, v.liq, 'market');
|
|
54
|
+
obs.mintAuth = authFromPanel(panels.authorities, v.mintAuth);
|
|
55
|
+
obs.freezeAuth = authFromPanel(panels.authorities, v.freezeAuth);
|
|
56
|
+
// bonding-curve progress + migration from the on-chain fast-lane push (sub-second), when streaming.
|
|
57
|
+
if (inputs.curve) {
|
|
58
|
+
const bp = bondingProgress(inputs.curve.curve.vToken);
|
|
59
|
+
if (bp != null)
|
|
60
|
+
obs.bonding = { value: bp, source: 'onchain-curve', fetchedAt: inputs.curve.at, stale: false };
|
|
61
|
+
obs.migrated = { value: inputs.curve.curve.complete, source: 'onchain-curve', fetchedAt: inputs.curve.at, stale: false };
|
|
62
|
+
}
|
|
63
|
+
return obs;
|
|
64
|
+
}
|
|
65
|
+
export class LiveSignalsLive {
|
|
66
|
+
engine;
|
|
67
|
+
now;
|
|
68
|
+
onSignal;
|
|
69
|
+
logByMint = new Map();
|
|
70
|
+
/** Signals emitted this session (all mints), newest last — a cheap counter/feed for the smoke script + tests. */
|
|
71
|
+
emitted = [];
|
|
72
|
+
constructor(opts = {}) {
|
|
73
|
+
this.engine = new LiveSignalsEngine({ thresholds: opts.thresholds });
|
|
74
|
+
this.now = opts.now ?? (() => Date.now());
|
|
75
|
+
this.onSignal = opts.onSignal;
|
|
76
|
+
}
|
|
77
|
+
/** Observe one moment of a token's state; records and returns any new signals. */
|
|
78
|
+
observe(inputs) {
|
|
79
|
+
const obs = buildObservation(inputs, this.now());
|
|
80
|
+
const sigs = this.engine.observe(obs);
|
|
81
|
+
for (const s of sigs) {
|
|
82
|
+
const arr = this.logByMint.get(s.mint) ?? [];
|
|
83
|
+
arr.unshift(s);
|
|
84
|
+
if (arr.length > SIGNALS_LOG_MAX)
|
|
85
|
+
arr.length = SIGNALS_LOG_MAX;
|
|
86
|
+
this.logByMint.set(s.mint, arr);
|
|
87
|
+
this.emitted.push(s);
|
|
88
|
+
this.onSignal?.(s);
|
|
89
|
+
}
|
|
90
|
+
return sigs;
|
|
91
|
+
}
|
|
92
|
+
/** Recent signals for a mint, newest first (token-screen LIVE SIGNALS strip). */
|
|
93
|
+
recent(mint, limit = 8) {
|
|
94
|
+
return (this.logByMint.get(mint) ?? []).slice(0, limit);
|
|
95
|
+
}
|
|
96
|
+
/** Latest numeric fields for the rules engine (merged into the open-token RuleRow in ruleRowsOf). */
|
|
97
|
+
fields(mint) {
|
|
98
|
+
return this.engine.fields(mint);
|
|
99
|
+
}
|
|
100
|
+
/** Per-field snapshot (value + source + age + stale) for the strip header. */
|
|
101
|
+
snapshot(mint, now = this.now()) {
|
|
102
|
+
return this.engine.snapshot(mint, now);
|
|
103
|
+
}
|
|
104
|
+
/** Drop a mint the UI no longer tracks. */
|
|
105
|
+
forget(mint) {
|
|
106
|
+
this.engine.forget(mint);
|
|
107
|
+
this.logByMint.delete(mint);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -680,6 +680,8 @@ export class PositionsLive {
|
|
|
680
680
|
now;
|
|
681
681
|
/** mint → current price in SOL per whole token. */
|
|
682
682
|
marks = new Map();
|
|
683
|
+
/** task-1407: when each mark was taken (ms), so a PnL card can print the mark's age. */
|
|
684
|
+
markTimes = new Map();
|
|
683
685
|
solPriceUsd = null;
|
|
684
686
|
loading = false;
|
|
685
687
|
heldMemo = new Map();
|
|
@@ -703,6 +705,10 @@ export class PositionsLive {
|
|
|
703
705
|
markOf(mint) {
|
|
704
706
|
return this.marks.get(mint) ?? null;
|
|
705
707
|
}
|
|
708
|
+
/** task-1407: when the mark for a mint was taken (ms), or null when there is none. */
|
|
709
|
+
markAt(mint) {
|
|
710
|
+
return this.marks.has(mint) ? this.markTimes.get(mint) ?? null : null;
|
|
711
|
+
}
|
|
706
712
|
/**
|
|
707
713
|
* PositionSource: the open paper position for a mint, or null. One indexed SQLite read, reused for
|
|
708
714
|
* HELD_MEMO_MS so a 30 fps render loop does not hit the ledger every frame. A closed wallet reads as flat.
|
|
@@ -758,8 +764,10 @@ export class PositionsLive {
|
|
|
758
764
|
continue;
|
|
759
765
|
}
|
|
760
766
|
const usd = byMint.get(m)?.pair.priceUsd ?? null;
|
|
761
|
-
if (usd && usd > 0)
|
|
767
|
+
if (usd && usd > 0) {
|
|
762
768
|
this.marks.set(m, usd / this.solPriceUsd);
|
|
769
|
+
this.markTimes.set(m, this.now());
|
|
770
|
+
}
|
|
763
771
|
}
|
|
764
772
|
}
|
|
765
773
|
}
|
|
@@ -865,6 +873,21 @@ export class LivePositions {
|
|
|
865
873
|
this.rowsMemo = { at: now, rows };
|
|
866
874
|
return rows;
|
|
867
875
|
}
|
|
876
|
+
/** task-1407: the raw ledger rows (newest first, swaps only) with network + signature, for the LIVE PnL card. */
|
|
877
|
+
fills(mint, limit = 500) {
|
|
878
|
+
try {
|
|
879
|
+
const l = new LiveLedger({ path: this.path, readOnly: true });
|
|
880
|
+
try {
|
|
881
|
+
return l.trades(limit * 2).filter((r) => r.kind === 'swap' && r.mint === mint).slice(0, limit);
|
|
882
|
+
}
|
|
883
|
+
finally {
|
|
884
|
+
l.close();
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
catch {
|
|
888
|
+
return [];
|
|
889
|
+
}
|
|
890
|
+
}
|
|
868
891
|
/** Confirmed live fills (newest first) for the popup markers / history; probes excluded. */
|
|
869
892
|
records(limit = 100, mint) {
|
|
870
893
|
try {
|