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
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// dexterm — `dexterm watch` (task-1398): run the rules engine against the live feed with NO TUI and PUSH
|
|
2
|
+
// every fired alert (and, when alerts.proposals is on, a one-line proposal summary) through the delivery
|
|
3
|
+
// layer — so a price / holder / LARP / new-pair alert reaches you when the terminal is closed.
|
|
4
|
+
//
|
|
5
|
+
// dexterm watch [--interval S] [--ticks N | --once] [--replay FILE] [--db PATH] [--verbose]
|
|
6
|
+
//
|
|
7
|
+
// SAFETY: this builds NO signer and passes no `trade` option; a rule can only alert / annotate / open an
|
|
8
|
+
// inert PROPOSAL (RulesEngine's money-safety invariant). The notifier only reads an AlertDelivery. There is
|
|
9
|
+
// no arm/sign/confirm/cap path anywhere under here (asserted by alerts-1398.test.ts).
|
|
10
|
+
import { dextermHome, loadConfig, RulesEngine } from '../../custom/src/index.js';
|
|
11
|
+
import { openHistoryReadOnly, LARP_PACK } from '../../recorder/src/index.js';
|
|
12
|
+
import { buildNotifier } from "./alerts/notifier.js";
|
|
13
|
+
import { alertToDelivery, proposalToDelivery } from "./alerts/format.js";
|
|
14
|
+
import { redactCreds } from "./alerts/creds.js";
|
|
15
|
+
import { larpGrade } from "./rules.js";
|
|
16
|
+
export const WATCH_USAGE = `usage: dexterm watch [--interval S] [--ticks N | --once] [--replay FILE] [--db PATH] [--verbose]
|
|
17
|
+
|
|
18
|
+
Runs your rules headless and pushes alerts to the channels you enabled with \`dexterm alerts\`
|
|
19
|
+
(terminal line + desktop notification + your Telegram bot / Discord webhook). Ctrl-C to stop.
|
|
20
|
+
`;
|
|
21
|
+
function parseArgs(argv) {
|
|
22
|
+
const a = { intervalMs: 15_000, ticks: Infinity, replay: null, db: null, verbose: false, help: false };
|
|
23
|
+
for (let i = 0; i < argv.length; i++) {
|
|
24
|
+
const t = argv[i];
|
|
25
|
+
if (t === '--help' || t === '-h')
|
|
26
|
+
a.help = true;
|
|
27
|
+
else if (t === '--verbose' || t === '-v')
|
|
28
|
+
a.verbose = true;
|
|
29
|
+
else if (t === '--once')
|
|
30
|
+
a.ticks = 1;
|
|
31
|
+
else if (t === '--interval') {
|
|
32
|
+
const n = Number(argv[++i]);
|
|
33
|
+
if (!Number.isFinite(n) || n < 1)
|
|
34
|
+
return { error: '--interval needs seconds ≥ 1' };
|
|
35
|
+
a.intervalMs = Math.round(n * 1000);
|
|
36
|
+
}
|
|
37
|
+
else if (t === '--ticks') {
|
|
38
|
+
const n = Number(argv[++i]);
|
|
39
|
+
if (!Number.isInteger(n) || n < 1)
|
|
40
|
+
return { error: '--ticks needs an integer ≥ 1' };
|
|
41
|
+
a.ticks = n;
|
|
42
|
+
}
|
|
43
|
+
else if (t === '--replay') {
|
|
44
|
+
a.replay = argv[++i] ?? null;
|
|
45
|
+
if (!a.replay)
|
|
46
|
+
return { error: '--replay needs a FILE' };
|
|
47
|
+
}
|
|
48
|
+
else if (t === '--db') {
|
|
49
|
+
a.db = argv[++i] ?? null;
|
|
50
|
+
if (!a.db)
|
|
51
|
+
return { error: '--db needs a PATH' };
|
|
52
|
+
}
|
|
53
|
+
else
|
|
54
|
+
return { error: `unknown option "${t}"` };
|
|
55
|
+
}
|
|
56
|
+
return a;
|
|
57
|
+
}
|
|
58
|
+
/** Latest LARP verdict per mint from a read-only recorder DB (best effort; empty map on any problem). */
|
|
59
|
+
function larpMapFromDb(path) {
|
|
60
|
+
const map = new Map();
|
|
61
|
+
if (!path || path === ':memory:')
|
|
62
|
+
return map;
|
|
63
|
+
const db = openHistoryReadOnly(path);
|
|
64
|
+
if (!db)
|
|
65
|
+
return map;
|
|
66
|
+
try {
|
|
67
|
+
const rows = db.prepare('SELECT mint, verdict FROM verdicts WHERE pack = ? AND verdict IS NOT NULL ORDER BY t').all(LARP_PACK);
|
|
68
|
+
for (const r of rows)
|
|
69
|
+
map.set(r.mint, r.verdict); // ORDER BY t → last write per mint wins
|
|
70
|
+
}
|
|
71
|
+
catch { /* table may not exist yet */ }
|
|
72
|
+
finally {
|
|
73
|
+
try {
|
|
74
|
+
db.close();
|
|
75
|
+
}
|
|
76
|
+
catch { /* ignore */ }
|
|
77
|
+
}
|
|
78
|
+
return map;
|
|
79
|
+
}
|
|
80
|
+
/** Map a screener row to a rule row (cluster is only known with the wallet-intel scan, absent headless; larp from the DB). */
|
|
81
|
+
export function toRuleRow(r, larp) {
|
|
82
|
+
return Object.assign(Object.create(null), r, { cluster: null, larp: larpGrade(larp.get(r.mint)) });
|
|
83
|
+
}
|
|
84
|
+
/** The production source: refresh the live (or replay) feed, map rows, yield a snapshot. Replay yields once. */
|
|
85
|
+
function createFeedSource(opts) {
|
|
86
|
+
let feed = null;
|
|
87
|
+
let yielded = 0;
|
|
88
|
+
const larp = larpMapFromDb(opts.db);
|
|
89
|
+
return {
|
|
90
|
+
async next() {
|
|
91
|
+
if (opts.stop.aborted)
|
|
92
|
+
return null;
|
|
93
|
+
const { createLiveFeed, createReplayFeed } = await import("./feed.js");
|
|
94
|
+
if (!feed)
|
|
95
|
+
feed = opts.replay ? createReplayFeed(opts.replay, {}) : createLiveFeed({ dbPath: opts.db ?? undefined });
|
|
96
|
+
else {
|
|
97
|
+
if (opts.replay)
|
|
98
|
+
return null; // a recording is a single snapshot: one evaluate, then stop
|
|
99
|
+
await opts.stop.wait(opts.intervalMs);
|
|
100
|
+
if (opts.stop.aborted)
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const rows = await feed.refresh();
|
|
104
|
+
yielded++;
|
|
105
|
+
if (opts.verbose)
|
|
106
|
+
opts.io.stderr.write(`watch: tick ${yielded}, ${rows.length} rows\n`);
|
|
107
|
+
return { rows: rows.map((r) => toRuleRow(r, larp)), now: opts.now() };
|
|
108
|
+
},
|
|
109
|
+
close() { try {
|
|
110
|
+
feed?.close();
|
|
111
|
+
}
|
|
112
|
+
catch { /* ignore */ } },
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export async function watchCommand(argv, io, deps = {}) {
|
|
116
|
+
const parsed = parseArgs(argv);
|
|
117
|
+
if ('error' in parsed) {
|
|
118
|
+
io.stderr.write(`dexterm watch: ${parsed.error}\n\n${WATCH_USAGE}`);
|
|
119
|
+
return 2;
|
|
120
|
+
}
|
|
121
|
+
if (parsed.help) {
|
|
122
|
+
io.stdout.write(WATCH_USAGE);
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
const now = deps.now ?? (() => Date.now());
|
|
126
|
+
const home = dextermHome(io.env);
|
|
127
|
+
const loaded = loadConfig(home);
|
|
128
|
+
for (const e of loaded.errors)
|
|
129
|
+
io.stderr.write(`dexterm watch: config: ${e}\n`);
|
|
130
|
+
const rules = loaded.config.rules ?? [];
|
|
131
|
+
const engine = new RulesEngine({ rules, now: now() });
|
|
132
|
+
for (const e of engine.errors)
|
|
133
|
+
io.stderr.write(`dexterm watch: rule ${e.id}: ${e.msg}\n`);
|
|
134
|
+
const enabled = engine.rules.filter((r) => r.enabled).length;
|
|
135
|
+
if (enabled === 0)
|
|
136
|
+
io.stderr.write('dexterm watch: no enabled rules (add some with `dexterm rules add`); nothing will fire\n');
|
|
137
|
+
// SIGINT-aware sleeper for the production loop.
|
|
138
|
+
const stop = (() => {
|
|
139
|
+
let aborted = false;
|
|
140
|
+
let wake = () => { };
|
|
141
|
+
return {
|
|
142
|
+
get aborted() { return aborted; },
|
|
143
|
+
abort() { aborted = true; wake(); },
|
|
144
|
+
wake() { wake(); },
|
|
145
|
+
wait(ms) { return new Promise((res) => { const t = setTimeout(res, ms); wake = () => { clearTimeout(t); res(); }; }); },
|
|
146
|
+
};
|
|
147
|
+
})();
|
|
148
|
+
const notifier = deps.notifier ?? buildNotifier(home, loaded.config.alerts, {
|
|
149
|
+
toastSink: (line) => io.stdout.write(`${new Date(now()).toISOString().slice(11, 19)} ${line}\n`),
|
|
150
|
+
});
|
|
151
|
+
const source = deps.source ?? createFeedSource({ replay: parsed.replay, db: parsed.db, intervalMs: parsed.intervalMs, verbose: parsed.verbose, io, now, stop });
|
|
152
|
+
const ready = notifier.readyChannels();
|
|
153
|
+
if (parsed.verbose || !deps.notifier)
|
|
154
|
+
io.stderr.write(`dexterm watch: ${enabled} rule(s), channels: ${ready.length ? ready.join(', ') : 'toast'}\n`);
|
|
155
|
+
const onSigint = () => { io.stderr.write('\ndexterm watch: stopping…\n'); stop.abort(); };
|
|
156
|
+
const addSig = !deps.source; // only own SIGINT for the real loop (tests drive a finite source)
|
|
157
|
+
if (addSig)
|
|
158
|
+
process.on('SIGINT', onSigint);
|
|
159
|
+
let delivered = 0;
|
|
160
|
+
try {
|
|
161
|
+
for (let tick = 0; tick < parsed.ticks; tick++) {
|
|
162
|
+
const snap = await source.next();
|
|
163
|
+
if (!snap)
|
|
164
|
+
break;
|
|
165
|
+
engine.observe(snap.rows, snap.now);
|
|
166
|
+
const res = engine.evaluate(snap.rows, snap.now, { watchlist: snap.watchlist, positions: snap.positions });
|
|
167
|
+
for (const a of res.alerts) {
|
|
168
|
+
await notifier.deliver(alertToDelivery(a));
|
|
169
|
+
delivered++;
|
|
170
|
+
}
|
|
171
|
+
if (loaded.config.alerts?.proposals)
|
|
172
|
+
for (const p of res.proposals) {
|
|
173
|
+
await notifier.deliver(proposalToDelivery(p));
|
|
174
|
+
delivered++;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
io.stderr.write(`dexterm watch: ${redactCreds(e.message)}\n`);
|
|
180
|
+
return 1;
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
if (addSig)
|
|
184
|
+
process.removeListener('SIGINT', onSigint);
|
|
185
|
+
await source.close?.();
|
|
186
|
+
}
|
|
187
|
+
if (parsed.verbose || !deps.notifier)
|
|
188
|
+
io.stderr.write(`dexterm watch: delivered ${delivered} alert(s)\n`);
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
@@ -3,12 +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,
|
|
6
|
+
cluster: (r) => r.cluster, larp: (r) => r.larp,
|
|
7
|
+
fresh: (r) => r.fresh, mintable: (r) => r.mintable, freezable: (r) => r.freezable,
|
|
7
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,
|
|
8
9
|
buys: (r) => r.buys, sells: (r) => r.sells, bonding: (r) => r.bonding,
|
|
9
10
|
};
|
|
10
11
|
export const FIELD_NAMES = Object.keys(FIELDS);
|
|
11
|
-
const ALIASES = { vol: 'vol24', volume: 'vol24', liquidity: 'liq', cap: 'mcap', mc: 'mcap', holder: 'holders', organic: 'org', top: 'top10', locked: 'lplocked', 'cluster%': 'cluster', clusterpct: 'cluster' };
|
|
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
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 };
|
|
13
14
|
const TAGS = {
|
|
14
15
|
launchpad: (r, v) => (r.launchpad ?? '').toLowerCase().replace('.fun', '') === v.toLowerCase().replace('.fun', ''),
|
|
@@ -199,6 +199,9 @@ export const SCHEMAS = {
|
|
|
199
199
|
token: { type: 'object', additional: false, props: { tape: { type: 'boolean' }, social: { type: 'boolean' }, analyst: { type: 'boolean' } } },
|
|
200
200
|
} },
|
|
201
201
|
screenLayout: { oneOf: ['layoutNode', { type: 'object', additional: false, props: { wide: 'layoutNode', standard: 'layoutNode', compact: 'layoutNode' } }] },
|
|
202
|
+
/** task-1398: one alert-delivery channel. A `levels` filter (subset of up|down|info|warn); absent = every level. */
|
|
203
|
+
alertLevels: { type: 'array', max: 4, items: { type: 'string', enum: ['up', 'down', 'info', 'warn'] } },
|
|
204
|
+
alertChannel: { type: 'object', additional: false, props: { enabled: { type: 'boolean' }, levels: 'alertLevels' } },
|
|
202
205
|
ruleJson: { type: 'object', additional: false, required: ['id', 'when', 'then'], props: {
|
|
203
206
|
id: { type: 'string', pattern: NAME },
|
|
204
207
|
name: { type: 'string', max: 80 },
|
|
@@ -258,6 +261,26 @@ export const SCHEMAS = {
|
|
|
258
261
|
/** Reserved: the trading gate is NOT here (signer policy owns it); listed so a proposal that tries gets a precise refusal. */
|
|
259
262
|
gate: { type: 'string', enum: ['keypress'] },
|
|
260
263
|
} },
|
|
264
|
+
/** task-1393: voice-out preferences. `/voice` is user-only (DENIED_FOR_MODELS), so only the keyboard sets it. */
|
|
265
|
+
voice: { type: 'object', additional: false, props: {
|
|
266
|
+
name: { type: 'string', max: 64 },
|
|
267
|
+
rate: { type: 'number', int: true, min: 80, max: 400 },
|
|
268
|
+
pitch: { type: 'number', int: true, min: 0, max: 100 },
|
|
269
|
+
} },
|
|
270
|
+
/**
|
|
271
|
+
* task-1398: alert delivery routing. Enable flags + a per-channel `levels` filter ONLY; NO credentials here
|
|
272
|
+
* (bot tokens / webhook URLs live in ~/.dexterm/telegram.json and discord.json, 0600, like every other key).
|
|
273
|
+
* `/alerts` is user-only (DENIED_FOR_MODELS) so a model can never redirect your alerts to its own channel.
|
|
274
|
+
*/
|
|
275
|
+
alerts: { type: 'object', additional: false, props: {
|
|
276
|
+
channels: { type: 'object', additional: false, props: {
|
|
277
|
+
toast: 'alertChannel', desktop: 'alertChannel',
|
|
278
|
+
telegram: { type: 'object', additional: false, props: { enabled: { type: 'boolean' }, levels: 'alertLevels', chatId: { type: 'string', pattern: '^-?[0-9]{1,19}$' } } },
|
|
279
|
+
discord: 'alertChannel',
|
|
280
|
+
} },
|
|
281
|
+
/** Deliver a one-line summary of ticket/ask PROPOSALS too (never the ticket itself; off by default). */
|
|
282
|
+
proposals: { type: 'boolean' },
|
|
283
|
+
} },
|
|
261
284
|
scan: { type: 'object', additional: true },
|
|
262
285
|
popups: { type: 'object', additional: false, props: { rain: { type: 'object', additional: false, props: { enabled: { type: 'boolean' }, speedScale: { type: 'number', min: 0, max: 4 }, density: { type: 'number', min: 0, max: 1 } } } } },
|
|
263
286
|
/** Forward-compat bag: unknown sections from a newer dexterm survive a downgrade here. */
|
|
@@ -403,7 +426,7 @@ export function applyOps(doc, ops) {
|
|
|
403
426
|
export const DENIED_FOR_MODELS = [
|
|
404
427
|
/^\/signer(\/|$)/, /^\/trading(\/|$)/, /^\/keys(\/|$)/, /^\/caps(\/|$)/, /^\/mandates?(\/|$)/, /^\/wallet(\/|$)/,
|
|
405
428
|
/^\/packs(\/|$)/, /^\/plugins\/[^/]+\/(capabilities|enabled)(\/|$)/, /^\/plugins\/[^/]+$/, /^\/plugins$/,
|
|
406
|
-
/^\/jarvis\/gate(\/|$)/, /^\/jarvis\/configAssistant$/, /^\/voice(\/|$)/, /^\/agents(\/|$)/, /^\/version$/,
|
|
429
|
+
/^\/jarvis\/gate(\/|$)/, /^\/jarvis\/configAssistant$/, /^\/voice(\/|$)/, /^\/alerts(\/|$)/, /^\/agents(\/|$)/, /^\/version$/,
|
|
407
430
|
/^\/rules\/(\d+|-)\/then\/(\d+|-)\/args\/(arm|armed|auto|autoArm|confirm|execute|sign|send)(\/|$)/,
|
|
408
431
|
];
|
|
409
432
|
/** Keys a model-authored VALUE may never contain at any depth (money/secret-shaped), whatever the path. */
|
|
@@ -18,8 +18,53 @@ export const BASE58_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
|
18
18
|
export const ACTION_FILE_ENV = 'DEXTERM_ACTION_FILE';
|
|
19
19
|
/** Env var carrying the per-run random token that authenticates channel lines. */
|
|
20
20
|
export const ACTION_TOKEN_ENV = 'DEXTERM_ACTION_TOKEN';
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The non-financial UI actions the analyst may request (task-1313, extended in task-1413).
|
|
23
|
+
*
|
|
24
|
+
* Two tiers (see `ACTION_TIER`):
|
|
25
|
+
* - `view` — local, reversible runtime changes applied immediately with an "analyst did X" note.
|
|
26
|
+
* - `config` — a persisted change to `config.json`. It is NOT applied by the model; the consumer turns
|
|
27
|
+
* it into a one-line PROPOSAL the user accepts with a keypress, then writes it through the
|
|
28
|
+
* journaled config path (`applyAndJournal`, actor `analyst`, so `ctrl+z` undoes it).
|
|
29
|
+
*
|
|
30
|
+
* FORBIDDEN forever (task-1413 tier c): there is deliberately NO action kind that touches wallets, keys,
|
|
31
|
+
* credentials, alert-channel secrets, signing/arming/caps/mandates, trade tickets beyond the existing
|
|
32
|
+
* PAPER `open_ticket`, network/RPC settings, or software install/update. Such a request fails here as an
|
|
33
|
+
* "unknown action type". The journaled config paths a `config` action may write are a closed list
|
|
34
|
+
* (`CONFIG_ACTION_PATHS`), AND the consumer re-runs `policyCheck(..., non-user actor)` on the journal op, so
|
|
35
|
+
* no `config` action may ever write a `DENIED_FOR_MODELS` path — none of the paths below is denied or a
|
|
36
|
+
* money/secret path.
|
|
37
|
+
*
|
|
38
|
+
* NOTE (task-1413 r2): voice rate / voice name are NOT analyst actions. `/voice` is `DENIED_FOR_MODELS`
|
|
39
|
+
* ("only the keyboard sets it", schema.ts), and the gate stays the gate: a model may never write it, even
|
|
40
|
+
* behind a keypress proposal. The analyst still EXPLAINS how to change the voice (app-help: edit config.json
|
|
41
|
+
* or the `,` key); it just cannot write it. Re-enabling it would need a maintainer-approved DENIED_FOR_MODELS carve-out.
|
|
42
|
+
*/
|
|
43
|
+
export const ANALYST_ACTION_TYPES = [
|
|
44
|
+
// task-1313 (unchanged):
|
|
45
|
+
'open_token', 'show_in_screener', 'add_to_watchlist', 'switch_feed', 'open_ticket',
|
|
46
|
+
// task-1413 view-only:
|
|
47
|
+
'switch_screen', 'switch_workspace', 'chart_popup', 'toggle_calm', 'set_scan', 'export_view',
|
|
48
|
+
// task-1413 persisted config (user-confirmed, journaled; non-denied display paths only):
|
|
49
|
+
'set_theme', 'set_price_mode', 'set_intensity',
|
|
50
|
+
];
|
|
51
|
+
/** Tier of each action: `view` = immediate runtime, `config` = persisted via a user-confirmed journal write. */
|
|
52
|
+
export const ACTION_TIER = {
|
|
53
|
+
open_token: 'view', show_in_screener: 'view', add_to_watchlist: 'view', switch_feed: 'view', open_ticket: 'view',
|
|
54
|
+
switch_screen: 'view', switch_workspace: 'view', chart_popup: 'view', toggle_calm: 'view', set_scan: 'view', export_view: 'view',
|
|
55
|
+
set_theme: 'config', set_price_mode: 'config', set_intensity: 'config',
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* The exact `config.json` JSON-Pointer each `config` action writes — a closed allow-list. Every path here
|
|
59
|
+
* is a local, reversible, non-financial DISPLAY setting and NONE is a `DENIED_FOR_MODELS` path (the journal
|
|
60
|
+
* write re-checks that with `policyCheck`). NOTHING here is matched by `DENIED_VALUE_KEYS` / the money-safety
|
|
61
|
+
* sections; the consumer additionally confirms each with a keypress.
|
|
62
|
+
*/
|
|
63
|
+
export const CONFIG_ACTION_PATHS = {
|
|
64
|
+
set_theme: '/theme/name',
|
|
65
|
+
set_price_mode: '/display/priceMode',
|
|
66
|
+
set_intensity: '/fx/intensity',
|
|
67
|
+
};
|
|
23
68
|
/** Max actions the consumer should apply per rolling window, and the window length. Advisory shared constants. */
|
|
24
69
|
export const ACTION_RATE = { max: 6, windowMs: 10_000 };
|
|
25
70
|
const FILTER_MAX = 120;
|
|
@@ -27,6 +72,10 @@ const FILTER_MAX = 120;
|
|
|
27
72
|
// `!` is included so tag negation (`!flag:mint`, `!launchpad:pump`) — a documented part of the grammar —
|
|
28
73
|
// survives the boundary instead of being rejected as malformed (task-1313 part E).
|
|
29
74
|
const FILTER_SAFE_RE = /^[\w\s<>=:.%$,+!\-|()]*$/;
|
|
75
|
+
// task-1413 value shapes. The consumer re-validates against config schema (`validateConfig`) too.
|
|
76
|
+
const THEME_NAME_RE = /^[a-z][a-z0-9-]{0,39}$/; // mirrors schema.ts NAME
|
|
77
|
+
const PRICE_MODES = ['price', 'mcap'];
|
|
78
|
+
const INTENSITIES = ['off', 'normal', 'fever'];
|
|
30
79
|
function isObj(x) {
|
|
31
80
|
return typeof x === 'object' && x !== null && !Array.isArray(x);
|
|
32
81
|
}
|
|
@@ -94,14 +143,92 @@ export function validateAnalystAction(raw, allow) {
|
|
|
94
143
|
out.highlight = raw.highlight;
|
|
95
144
|
bits.push(`highlight ${short(raw.highlight)}`);
|
|
96
145
|
}
|
|
146
|
+
if (raw.preset !== undefined && raw.preset !== null && raw.preset !== '') {
|
|
147
|
+
if (typeof raw.preset !== 'string' || !allow.feeds.includes(raw.preset)) {
|
|
148
|
+
return { ok: false, error: `show_in_screener: preset must be one of ${allow.feeds.join(', ') || '(none)'}` };
|
|
149
|
+
}
|
|
150
|
+
out.preset = raw.preset;
|
|
151
|
+
bits.push(`preset ${out.preset}`);
|
|
152
|
+
}
|
|
97
153
|
if (!bits.length)
|
|
98
|
-
return { ok: false, error: 'show_in_screener: nothing to do (give filter, sort, or highlight)' };
|
|
154
|
+
return { ok: false, error: 'show_in_screener: nothing to do (give filter, sort, preset, or highlight)' };
|
|
99
155
|
return { ok: true, action: out, label: `showed the screener (${bits.join(', ')})` };
|
|
100
156
|
}
|
|
157
|
+
case 'switch_screen': {
|
|
158
|
+
const screens = allow.screens ?? [];
|
|
159
|
+
if (typeof raw.screen !== 'string' || !screens.includes(raw.screen)) {
|
|
160
|
+
return { ok: false, error: `switch_screen: screen must be one of ${screens.join(', ') || '(none)'}` };
|
|
161
|
+
}
|
|
162
|
+
return { ok: true, action: { type, screen: raw.screen }, label: `switched to the ${raw.screen} screen` };
|
|
163
|
+
}
|
|
164
|
+
case 'switch_workspace': {
|
|
165
|
+
const workspaces = allow.workspaces ?? [];
|
|
166
|
+
if (typeof raw.name !== 'string' || !workspaces.includes(raw.name)) {
|
|
167
|
+
return { ok: false, error: `switch_workspace: name must be one of ${workspaces.join(', ') || '(none)'}` };
|
|
168
|
+
}
|
|
169
|
+
return { ok: true, action: { type, name: raw.name }, label: `opened the "${raw.name}" workspace` };
|
|
170
|
+
}
|
|
171
|
+
case 'chart_popup': {
|
|
172
|
+
if (typeof raw.open !== 'boolean')
|
|
173
|
+
return { ok: false, error: 'chart_popup: open must be true or false' };
|
|
174
|
+
return { ok: true, action: { type, open: raw.open }, label: raw.open ? 'opened the chart popup' : 'closed the chart popup' };
|
|
175
|
+
}
|
|
176
|
+
case 'toggle_calm': {
|
|
177
|
+
if (typeof raw.on !== 'boolean')
|
|
178
|
+
return { ok: false, error: 'toggle_calm: on must be true or false' };
|
|
179
|
+
return { ok: true, action: { type, on: raw.on }, label: raw.on ? 'turned on calm mode' : 'turned off calm mode' };
|
|
180
|
+
}
|
|
181
|
+
case 'set_scan': {
|
|
182
|
+
if (typeof raw.on !== 'boolean')
|
|
183
|
+
return { ok: false, error: 'set_scan: on must be true or false' };
|
|
184
|
+
return { ok: true, action: { type, on: raw.on }, label: raw.on ? 'turned scan mode on' : 'turned scan mode off' };
|
|
185
|
+
}
|
|
186
|
+
case 'export_view': {
|
|
187
|
+
return { ok: true, action: { type }, label: 'exported the current view' };
|
|
188
|
+
}
|
|
189
|
+
case 'set_theme': {
|
|
190
|
+
if (typeof raw.name !== 'string' || !THEME_NAME_RE.test(raw.name))
|
|
191
|
+
return { ok: false, error: 'set_theme: invalid theme name' };
|
|
192
|
+
const themes = allow.themes ?? [];
|
|
193
|
+
if (!themes.includes(raw.name))
|
|
194
|
+
return { ok: false, error: `set_theme: theme must be one of ${themes.join(', ') || '(none)'}` };
|
|
195
|
+
return { ok: true, action: { type, name: raw.name }, label: `set the theme to ${raw.name}` };
|
|
196
|
+
}
|
|
197
|
+
case 'set_price_mode': {
|
|
198
|
+
if (raw.mode !== 'price' && raw.mode !== 'mcap')
|
|
199
|
+
return { ok: false, error: `set_price_mode: mode must be one of ${PRICE_MODES.join(', ')}` };
|
|
200
|
+
return { ok: true, action: { type, mode: raw.mode }, label: `set the price display to ${raw.mode === 'mcap' ? 'market cap' : 'price'}` };
|
|
201
|
+
}
|
|
202
|
+
case 'set_intensity': {
|
|
203
|
+
if (typeof raw.intensity !== 'string' || !INTENSITIES.includes(raw.intensity)) {
|
|
204
|
+
return { ok: false, error: `set_intensity: intensity must be one of ${INTENSITIES.join(', ')}` };
|
|
205
|
+
}
|
|
206
|
+
return { ok: true, action: { type, intensity: raw.intensity }, label: `set the visual intensity to ${raw.intensity}` };
|
|
207
|
+
}
|
|
101
208
|
default:
|
|
102
209
|
return { ok: false, error: `unknown action type ${type}` };
|
|
103
210
|
}
|
|
104
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Map a validated `config`-tier action to a `{section, key, value}` descriptor of the `config.json`
|
|
214
|
+
* change it requests. Returns `null` for a `view`-tier action (nothing to persist). Pure and
|
|
215
|
+
* dependency-free: the consumer builds the actual `PatchOp` (creating the parent section when it is
|
|
216
|
+
* absent, exactly like the `dexterm display` / `,` voice handlers), runs the config schema validator,
|
|
217
|
+
* and writes through `applyAndJournal` (actor `analyst`). `section`/`key` are fixed, never taken from
|
|
218
|
+
* model input — only `value` comes from the (already-validated) action.
|
|
219
|
+
*/
|
|
220
|
+
export function configChangeForAction(action) {
|
|
221
|
+
const at = (path, value) => {
|
|
222
|
+
const [, section, key] = path.split('/');
|
|
223
|
+
return { section, key, value, path };
|
|
224
|
+
};
|
|
225
|
+
switch (action.type) {
|
|
226
|
+
case 'set_theme': return at(CONFIG_ACTION_PATHS.set_theme, action.name);
|
|
227
|
+
case 'set_price_mode': return at(CONFIG_ACTION_PATHS.set_price_mode, action.mode);
|
|
228
|
+
case 'set_intensity': return at(CONFIG_ACTION_PATHS.set_intensity, action.intensity);
|
|
229
|
+
default: return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
105
232
|
/** Serialize a channel line (newline-terminated). */
|
|
106
233
|
export function serializeActionLine(token, seq, action) {
|
|
107
234
|
const line = { v: 1, token, seq, ts: Date.now(), action };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// @dexterm/data — "paste a CA or a coin link anywhere" (task-1401, building on the task-1388 GO-TO parser). Shared by
|
|
2
|
+
// the CLI (go-to prompt, analyst chat, the screener `/` line, --mint) and the MCP server (a link passed as `mint`).
|
|
3
|
+
//
|
|
4
|
+
// parseMintTarget (task-1388) pulls a base58 mint out of a bare address or a dexscreener / pump.fun / solscan URL.
|
|
5
|
+
// parseCoinTarget adds what the other inputs need: WHICH site the link came from (a dexscreener.com/solana/<addr>
|
|
6
|
+
// link usually names the POOL, so it is resolved to its coin before the token screen opens) and the REST of the
|
|
7
|
+
// text, so "is <CA> a rug?" in the analyst chat opens the coin and asks the question about it.
|
|
8
|
+
// Pure and total: never throws, only ever returns a validated base58 string.
|
|
9
|
+
/** A full base58 Solana mint: 32–44 chars, no 0/O/I/l. Same shape `--mint` validates. */
|
|
10
|
+
export const MINT_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
11
|
+
/**
|
|
12
|
+
* task-1388: turn a pasted/typed go-to target into a mint, or null when there is none. Accepts a bare base58
|
|
13
|
+
* mint, or a dexscreener / pump.fun / solscan URL (or any text) by pulling the first base58 run of mint length.
|
|
14
|
+
* Base58 excludes 0, O, I and l, so splitting on every non-base58 character also splits on them and on the URL
|
|
15
|
+
* punctuation (/, :, ., ?, =, &), leaving each path/query segment as its own token; a real mint is never cut.
|
|
16
|
+
*/
|
|
17
|
+
export function parseMintTarget(raw) {
|
|
18
|
+
const s = raw.trim();
|
|
19
|
+
if (!s)
|
|
20
|
+
return null;
|
|
21
|
+
if (MINT_RE.test(s))
|
|
22
|
+
return s;
|
|
23
|
+
for (const tok of s.split(/[^1-9A-HJ-NP-Za-km-z]+/))
|
|
24
|
+
if (MINT_RE.test(tok))
|
|
25
|
+
return tok;
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const HOSTS = [
|
|
29
|
+
[/(^|\.)dexscreener\.com$/, 'dexscreener'],
|
|
30
|
+
[/(^|\.)pump\.fun$/, 'pumpfun'],
|
|
31
|
+
[/(^|\.)solscan\.io$/, 'solscan'],
|
|
32
|
+
[/(^|\.)birdeye\.so$/, 'birdeye'],
|
|
33
|
+
];
|
|
34
|
+
const URLISH = /^(?:https?:\/\/)?(?:www\.)?([a-z0-9-]+(?:\.[a-z0-9-]+)+)(\/\S*)$/i;
|
|
35
|
+
/**
|
|
36
|
+
* Find the first coin address in free text: a link with an address in its path (its host decides `via`), else a
|
|
37
|
+
* bare base58 address word. `rest` is the remaining text. Null when the text names no address.
|
|
38
|
+
*/
|
|
39
|
+
export function parseCoinTarget(raw) {
|
|
40
|
+
const s = String(raw ?? '').trim().slice(0, 800);
|
|
41
|
+
if (!s)
|
|
42
|
+
return null;
|
|
43
|
+
const words = s.split(/\s+/);
|
|
44
|
+
const restWithout = (i) => words.filter((_, k) => k !== i).join(' ').trim();
|
|
45
|
+
for (let i = 0; i < words.length; i++) {
|
|
46
|
+
const m = URLISH.exec(words[i]);
|
|
47
|
+
if (!m)
|
|
48
|
+
continue;
|
|
49
|
+
const address = parseMintTarget(m[2]);
|
|
50
|
+
if (!address)
|
|
51
|
+
continue;
|
|
52
|
+
const host = m[1].toLowerCase();
|
|
53
|
+
return { address, via: HOSTS.find(([re]) => re.test(host))?.[1] ?? 'link', rest: restWithout(i) };
|
|
54
|
+
}
|
|
55
|
+
for (let i = 0; i < words.length; i++) {
|
|
56
|
+
// a bare address, allowing surrounding punctuation ("<CA>?", "(CA)", "CA,")
|
|
57
|
+
const core = words[i].replace(/^[^1-9A-HJ-NP-Za-km-z]+|[^1-9A-HJ-NP-Za-km-z]+$/g, '');
|
|
58
|
+
if (MINT_RE.test(core))
|
|
59
|
+
return { address: core, via: 'address', rest: restWithout(i) };
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
/** True when the text left after the address asks nothing ("", "open", "check this", "?"): just open the coin. */
|
|
64
|
+
export function isBareOpen(rest) {
|
|
65
|
+
return /^(?:(?:open|go|to|show|me|check|look|at|this|that|one|the|ca|token|coin|pls|please|here|it)\b|[\s?.!:,;\-–—])*$/i.test(rest.trim());
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Resolve a target to the coin's mint. A dexscreener link's address is looked up as a pair first (one keyless
|
|
69
|
+
* request through the limiter); when DexScreener has no such pair, or the lookup fails (offline, a replay without
|
|
70
|
+
* it), the address is used as the mint. Every other target is already a mint.
|
|
71
|
+
*/
|
|
72
|
+
export async function resolveCoinTarget(t, dl) {
|
|
73
|
+
if (t.via !== 'dexscreener' || !dl)
|
|
74
|
+
return { mint: t.address, fromPair: false };
|
|
75
|
+
try {
|
|
76
|
+
const coin = await dl.dexscreener.pairCoin(t.address);
|
|
77
|
+
if (coin && MINT_RE.test(coin))
|
|
78
|
+
return { mint: coin, fromPair: coin !== t.address };
|
|
79
|
+
}
|
|
80
|
+
catch { /* fall through: treat the address as the mint */ }
|
|
81
|
+
return { mint: t.address, fromPair: false };
|
|
82
|
+
}
|
|
@@ -159,7 +159,7 @@ export class FastLane {
|
|
|
159
159
|
return false;
|
|
160
160
|
if (this.tracked.size >= this.cap)
|
|
161
161
|
return false; // over the cap: stay slow, never drop
|
|
162
|
-
const t = { mint, priceAccount, lastTickAt: null, lastPrice: null, migrated: false };
|
|
162
|
+
const t = { mint, priceAccount, lastTickAt: null, lastPrice: null, lastCurve: null, lastCurveAt: null, migrated: false };
|
|
163
163
|
this.tracked.set(mint, t);
|
|
164
164
|
this.byAccount.set(priceAccount.curve, mint);
|
|
165
165
|
this.ws?.subscribe(priceAccount.curve);
|
|
@@ -227,7 +227,13 @@ export class FastLane {
|
|
|
227
227
|
if (!tracked)
|
|
228
228
|
return;
|
|
229
229
|
const curve = decodeBondingCurve(base64);
|
|
230
|
-
if (!curve
|
|
230
|
+
if (!curve)
|
|
231
|
+
return;
|
|
232
|
+
// task-1404: keep the decoded curve (reserves / complete) so the live-signals binding can read bonding
|
|
233
|
+
// progress and migration from the same push, not just the price.
|
|
234
|
+
tracked.lastCurve = curve;
|
|
235
|
+
tracked.lastCurveAt = t;
|
|
236
|
+
if (curve.priceSol == null)
|
|
231
237
|
return;
|
|
232
238
|
if (curve.complete) {
|
|
233
239
|
// Migrated to a DEX pool: the bonding-curve price is stale. Demote to the slow lane honestly;
|
|
@@ -280,6 +286,14 @@ export class FastLane {
|
|
|
280
286
|
price(mint) {
|
|
281
287
|
return this.tracked.get(mint)?.lastPrice ?? null;
|
|
282
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* Last decoded bonding curve for a mint and when it was decoded (task-1404). The live-signals binding reads
|
|
291
|
+
* bonding progress + `complete` from it. null when the mint is not tracked or no push/seed has decoded yet.
|
|
292
|
+
*/
|
|
293
|
+
curve(mint) {
|
|
294
|
+
const t = this.tracked.get(mint);
|
|
295
|
+
return t && t.lastCurve && t.lastCurveAt != null ? { curve: t.lastCurve, at: t.lastCurveAt } : null;
|
|
296
|
+
}
|
|
283
297
|
/**
|
|
284
298
|
* Last fast-lane USD price, recomputed from the stored on-chain SOL price and the *current* SOL/USD —
|
|
285
299
|
* so a tick that arrived before SOL/USD was known still shows a USD value once the rate lands. Null
|
|
@@ -49,11 +49,15 @@ export { parseSignerArgs, buildSignerService } from "./trade/signer/main.js";
|
|
|
49
49
|
export * from "./larp/links.js";
|
|
50
50
|
export * from "./larp/safe-fetch.js";
|
|
51
51
|
export * from "./larp/evidence.js";
|
|
52
|
+
export * from "./larp/link-baseline.js";
|
|
53
|
+
// task-1401: parse a CA / coin link from any input (go-to, analyst chat, filter line, --mint, MCP refs).
|
|
54
|
+
export * from "./coin-target.js";
|
|
52
55
|
export * from "./analyst-actions.js";
|
|
53
56
|
export * from "./intel/rules.js";
|
|
54
57
|
export * from "./intel/holder-class.js";
|
|
55
58
|
export * from "./intel/helius-history.js";
|
|
56
59
|
export * from "./intel/scan.js";
|
|
60
|
+
export * from "./intel/live-signals.js";
|
|
57
61
|
export * from "./images/index.js";
|
|
58
62
|
// task-1330 (I-03): social / news / trending layer. `Provenance` and `Credibility`-style names are
|
|
59
63
|
// namespaced under `Social*` where they would collide with the core model.
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { rpcBodyCheck } from "../sources/solana-rpc.js";
|
|
2
2
|
export const HISTORY_METHODS = new Set(['getTransactionsForAddress']);
|
|
3
|
+
/**
|
|
4
|
+
* Max transaction version we declare support for. Legacy and v0 are all Solana ships today, but Helius's
|
|
5
|
+
* enhanced history has returned a v1 tx in live launch windows (task-1394). We only read static account keys
|
|
6
|
+
* and per-balance owners, which are version-independent, so cap high rather than re-break on the next version.
|
|
7
|
+
*/
|
|
8
|
+
export const MAX_TX_VERSION = 128;
|
|
3
9
|
export class HeliusHistory {
|
|
4
10
|
http;
|
|
5
11
|
/** May carry an api-key: never print it. */
|
|
@@ -25,7 +31,12 @@ export class HeliusHistory {
|
|
|
25
31
|
async transactions(address, opts) {
|
|
26
32
|
const limit = Math.max(1, Math.min(1000, Math.floor(opts.limit)));
|
|
27
33
|
const params = {
|
|
28
|
-
|
|
34
|
+
// A single versioned tx in the page whose version exceeds this cap fails the WHOLE request with
|
|
35
|
+
// rpc -32015 ("Transaction version (N) is not supported … maxSupportedTransactionVersion"), which
|
|
36
|
+
// surfaced as "history failed" on live pump.fun launch windows (task-1394: a v1 tx in the first 100).
|
|
37
|
+
// The rules only read static accountKeys (signers are always static, never in a lookup table) and the
|
|
38
|
+
// per-balance owner/mint, so a high cap is safe and forward-compatible across tx versions.
|
|
39
|
+
transactionDetails: 'full', sortOrder: opts.sortOrder ?? 'asc', limit, encoding: 'jsonParsed', maxSupportedTransactionVersion: MAX_TX_VERSION,
|
|
29
40
|
filters: { status: 'succeeded' },
|
|
30
41
|
};
|
|
31
42
|
if (opts.paginationToken)
|