dexterm-cli 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/packages/agents/src/bus.js +20 -5
  2. package/dist/packages/agents/src/executor.js +25 -0
  3. package/dist/packages/agents/src/gate.js +3 -0
  4. package/dist/packages/agents/src/identity.js +41 -2
  5. package/dist/packages/agents/src/tiers.js +5 -0
  6. package/dist/packages/agents/src/tools.js +3 -0
  7. package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +11 -3
  8. package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +13 -3
  9. package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +3 -3
  10. package/dist/packages/cli/src/agent-cmd.js +36 -3
  11. package/dist/packages/cli/src/alerts/adapters/desktop.js +40 -0
  12. package/dist/packages/cli/src/alerts/adapters/discord.js +36 -0
  13. package/dist/packages/cli/src/alerts/adapters/telegram.js +43 -0
  14. package/dist/packages/cli/src/alerts/adapters/toast.js +16 -0
  15. package/dist/packages/cli/src/alerts/creds.js +83 -0
  16. package/dist/packages/cli/src/alerts/format.js +41 -0
  17. package/dist/packages/cli/src/alerts/index.js +10 -0
  18. package/dist/packages/cli/src/alerts/notifier.js +53 -0
  19. package/dist/packages/cli/src/alerts/route.js +19 -0
  20. package/dist/packages/cli/src/alerts/types.js +1 -0
  21. package/dist/packages/cli/src/alerts-cmd.js +212 -0
  22. package/dist/packages/cli/src/backtest-cmd.js +62 -3
  23. package/dist/packages/cli/src/coin-target.js +2 -0
  24. package/dist/packages/cli/src/doctor.js +20 -6
  25. package/dist/packages/cli/src/export.js +235 -0
  26. package/dist/packages/cli/src/live/index.js +2 -0
  27. package/dist/packages/cli/src/main.js +243 -41
  28. package/dist/packages/cli/src/mcp-cmd.js +326 -0
  29. package/dist/packages/cli/src/rules.js +21 -2
  30. package/dist/packages/cli/src/skill-eval-build.js +109 -0
  31. package/dist/packages/cli/src/update.js +294 -0
  32. package/dist/packages/cli/src/version.js +3 -0
  33. package/dist/packages/cli/src/voice-cmd.js +119 -0
  34. package/dist/packages/cli/src/watch-cmd.js +190 -0
  35. package/dist/packages/custom/src/rules.js +1 -1
  36. package/dist/packages/custom/src/schema.js +24 -1
  37. package/dist/packages/data/src/coin-target.js +82 -0
  38. package/dist/packages/data/src/index.js +3 -0
  39. package/dist/packages/data/src/intel/helius-history.js +12 -1
  40. package/dist/packages/data/src/larp/link-baseline.js +196 -0
  41. package/dist/packages/data/src/sources/dexscreener.js +14 -0
  42. package/dist/packages/data/src/sources/pumpportal.js +6 -1
  43. package/dist/packages/mcp/src/server.js +89 -2
  44. package/dist/packages/packs/src/index.js +1 -0
  45. package/dist/packages/packs/src/skill-backtest.js +300 -0
  46. package/dist/packages/tui/src/index.js +3 -0
  47. package/dist/packages/tui/src/keymap.js +2 -0
  48. package/dist/packages/tui/src/raster-font.js +195 -0
  49. package/dist/packages/tui/src/raster.js +213 -0
  50. package/dist/packages/voice/src/controller.js +15 -3
  51. package/dist/packages/voice/src/doctor.js +75 -7
  52. package/dist/packages/voice/src/download.js +184 -0
  53. package/dist/packages/voice/src/engines.js +48 -8
  54. package/dist/packages/voice/src/index.js +7 -3
  55. package/dist/packages/voice/src/stt/whisper.js +14 -4
  56. package/dist/packages/voice/src/tts/espeak.js +131 -0
  57. package/dist/packages/voice/src/tts/piper.js +206 -0
  58. package/dist/packages/voice/src/tts/say.js +71 -35
  59. package/dist/packages/voice/src/tts/voices.js +206 -0
  60. package/package.json +5 -9
@@ -0,0 +1,235 @@
1
+ // dexterm — EXPORT / SHARE (task-1401). One key (`E`) renders the current view to a PNG plus a plain-text report in
2
+ // ~/.dexterm/exports (DEXTERM_HOME overrides). Nothing is uploaded, posted or copied anywhere else.
3
+ //
4
+ // The renderer is reusable on purpose (task-1407 PNL cards build on it): give `renderExportPng` / `renderExportText`
5
+ // / `writeExport` an ExportView — any rendered Canvas plus its title, the data label (live / sample / replay) and the
6
+ // data ages — and it frames the view with a header band (title, the label, the export time) and a footer band (every
7
+ // data age, "nothing was uploaded"). The label is never optional: a sample-data frame always says SAMPLE DATA.
8
+ //
9
+ // The frame is rendered CALM (no rain, glitch or scanlines) with no toast, help card or prompt on top, so the PNG
10
+ // is the data, not an effect. PNGs are pure TypeScript (@dexterm/tui rasterize + @dexterm/data encodePng).
11
+ import { mkdirSync, writeFileSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+ import { Canvas, rasterizeCanvas, fmtUsd, fmtPrice, fmtPct } from '../../tui/src/index.js';
14
+ import { encodePng } from '../../data/src/index.js';
15
+ import { dextermHome } from "./boot-state.js";
16
+ import { cleanText } from "./screener/rows.js";
17
+ import { rugP, fmtRugP, RUG_MODEL_LABEL } from "./screener/rug.js";
18
+ import { renderFrame } from "./state.js";
19
+ export const LABEL_TEXT = {
20
+ live: '● LIVE DATA',
21
+ sample: '◌ SAMPLE DATA · not real prices',
22
+ replay: '◌ REPLAY · recorded data',
23
+ };
24
+ /** `4s` / `3m` / `2h` / `5d`; `—` when unknown. */
25
+ export function fmtAge(sec) {
26
+ if (sec === null || !Number.isFinite(sec))
27
+ return '—';
28
+ const s = Math.max(0, Math.round(sec));
29
+ return s < 60 ? `${s}s` : s < 3600 ? `${Math.floor(s / 60)}m` : s < 86_400 ? `${Math.floor(s / 3600)}h` : `${Math.floor(s / 86_400)}d`;
30
+ }
31
+ const ageText = (a) => (a.ageSec !== null ? `${a.name} ${fmtAge(a.ageSec)}` : `${a.name} ${a.state || 'n/a'}`);
32
+ /** Local timestamp `2026-10-16 01:23:45` (the machine's own clock; the report also carries the ISO time). */
33
+ function localStamp(now) {
34
+ const d = new Date(now);
35
+ const p = (n) => String(n).padStart(2, '0');
36
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
37
+ }
38
+ /** Wrap `parts` joined by ` · ` into lines of at most `w` columns. */
39
+ function wrapParts(prefix, parts, w) {
40
+ const out = [];
41
+ let line = prefix;
42
+ for (const p of parts) {
43
+ const add = line === prefix ? p : ` · ${p}`;
44
+ if (line.length + add.length > w && line !== prefix) {
45
+ out.push(line);
46
+ line = ' '.repeat(prefix.length) + p;
47
+ }
48
+ else
49
+ line += add;
50
+ }
51
+ out.push(line);
52
+ return out;
53
+ }
54
+ /**
55
+ * Frame a rendered view: a header band (title, label, export time), the view itself, and a footer band (every data
56
+ * age, local-only note). Returns a new canvas; the view canvas is not changed.
57
+ */
58
+ export function composeExportCanvas(v, now) {
59
+ const src = v.canvas;
60
+ const w = Math.max(src.w, 60);
61
+ const t = src.theme;
62
+ const footer = [
63
+ ...(v.mint ? [` contract ${v.mint}`] : []),
64
+ ...wrapParts(v.label === 'replay' ? ' data ages (replay clock): ' : ' data ages: ', v.ages.length ? v.ages.map(ageText) : ['none reported for this view'], w - 1),
65
+ ` exported locally by dexterm ${localStamp(now)} · nothing was uploaded · not financial advice`,
66
+ ];
67
+ const c = new Canvas(w, src.h + 2 + footer.length, t);
68
+ c.fill(0, 0, w, c.h, ' ', { bg: t.bg });
69
+ // header band
70
+ c.fill(0, 0, w, 1, ' ', { bg: t.headBg });
71
+ const label = ` ${LABEL_TEXT[v.label]} `;
72
+ const labelFg = v.label === 'live' ? t.up : t.warn;
73
+ c.text(1, 0, `dexterm export · ${v.title}`, { fg: t.text, bg: t.headBg, b: true }, Math.max(0, w - label.length - 3));
74
+ c.textR(0, 0, w - 1, label, { fg: labelFg, bg: t.headBg, b: true });
75
+ // the view
76
+ for (let y = 0; y < src.h; y++)
77
+ for (let x = 0; x < src.w; x++) {
78
+ const i = src.idx(x, y), j = c.idx(x, y + 1);
79
+ c.ch[j] = src.ch[i];
80
+ c.fg[j] = src.fg[i];
81
+ c.bg[j] = src.bg[i];
82
+ c.at[j] = src.at[i];
83
+ c.hard[j] = src.hard[i];
84
+ }
85
+ for (const s of src.images)
86
+ c.images.push({ ...s, y: s.y + 1 });
87
+ c.imageMode = src.imageMode;
88
+ // footer band
89
+ const fy = src.h + 1;
90
+ c.fill(0, fy, w, c.h - fy, ' ', { bg: t.headBg });
91
+ footer.forEach((line, k) => c.text(0, fy + 1 + k, line, { fg: k === footer.length - 1 ? t.muted : t.text, bg: t.headBg }, w));
92
+ c.text(0, fy, ` ${LABEL_TEXT[v.label]}`, { fg: labelFg, bg: t.headBg, b: true }, w);
93
+ return c;
94
+ }
95
+ /** The PNG bytes of a framed view. */
96
+ export function renderExportPng(v, now, opts = {}) {
97
+ const px = rasterizeCanvas(composeExportCanvas(v, now), opts);
98
+ return encodePng({ w: px.w, h: px.h, rgba: px.rgba });
99
+ }
100
+ /** The plain-text report: header, label, ages, the view's own lines, the local-only note. */
101
+ export function renderExportText(v, now) {
102
+ const L = [];
103
+ L.push(`dexterm export · ${v.title}`);
104
+ if (v.mint)
105
+ L.push(`contract ${v.mint}`);
106
+ L.push(`data ${LABEL_TEXT[v.label]}`);
107
+ L.push(`exported ${localStamp(now)} (local time) · ${new Date(now).toISOString()}`);
108
+ L.push(`data ages${v.label === 'replay' ? ' (on the replay clock)' : ''}`);
109
+ if (!v.ages.length)
110
+ L.push(' none reported for this view');
111
+ for (const a of v.ages)
112
+ L.push(` ${a.name.padEnd(12)} ${(a.ageSec !== null ? fmtAge(a.ageSec) : a.state || 'n/a').padEnd(14)} ${a.source ?? ''}`.trimEnd());
113
+ L.push('');
114
+ L.push(...v.report);
115
+ L.push('');
116
+ L.push('Exported locally by dexterm. Nothing was uploaded. Model outputs are labelled as such. Not financial advice.');
117
+ return L.join('\n') + '\n';
118
+ }
119
+ /** ~/.dexterm/exports (DEXTERM_HOME overrides). */
120
+ export function exportsDir(env = process.env) {
121
+ return join(dextermHome(env), 'exports');
122
+ }
123
+ const slug = (s) => s.replace(/[^A-Za-z0-9_-]+/g, '').slice(0, 16) || 'view';
124
+ /** Write `<dir>/dexterm-<kind>-<subject>-<stamp>.png` and `.txt` (dir 0700, files 0600). */
125
+ export function writeExport(v, opts = {}) {
126
+ const now = opts.now ?? Date.now();
127
+ const dir = opts.dir ?? exportsDir(opts.env);
128
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
129
+ const stamp = localStamp(now).replace(/[-: ]/g, '').replace(/^(\d{8})(\d{6})$/, '$1-$2');
130
+ const base = join(dir, `dexterm-${slug(v.kind)}-${slug(opts.subject ?? v.mint?.slice(0, 6) ?? '')}-${stamp}`);
131
+ const png = renderExportPng(v, now, opts.raster);
132
+ writeFileSync(`${base}.png`, png, { mode: 0o600 });
133
+ writeFileSync(`${base}.txt`, renderExportText(v, now), { mode: 0o600 });
134
+ return { png: `${base}.png`, txt: `${base}.txt`, bytes: png.length };
135
+ }
136
+ // ------------------------------------------------------------------------------------------ view builders
137
+ const PANEL_NAMES = { market: 'market', candles: 'candles', authorities: 'authorities', holders: 'holders', rugcheck: 'rugcheck', goplus: 'goplus', trades: 'trades' };
138
+ /** Data ages of a live token view, one per panel, from each panel's own fetch time. */
139
+ export function tokenAges(v, now) {
140
+ return Object.keys(PANEL_NAMES).map((p) => {
141
+ const s = v.panels[p];
142
+ const ok = s.fetchedAt !== null && (s.state === 'ok' || s.stale);
143
+ return {
144
+ name: PANEL_NAMES[p], source: s.via,
145
+ ageSec: ok ? (now - s.fetchedAt) / 1000 : null,
146
+ state: ok ? (s.stale ? 'stale' : null) : s.state === 'needs-key' ? 'needs key' : s.state === 'error' ? `unavailable${s.reason ? ` (${s.reason})` : ''}` : s.state,
147
+ };
148
+ });
149
+ }
150
+ const yesNo = (b) => (b === null ? 'unknown' : b ? 'ENABLED' : 'revoked');
151
+ /** The text report body for a live token view: the numbers on screen, each from its panel. */
152
+ export function tokenReport(state, v, now) {
153
+ const L = [];
154
+ L.push(`coin $${cleanText(v.symbol, 16)}${v.name ? ` · ${cleanText(v.name, 40)}` : ''} · ${v.launchpad}`);
155
+ L.push(`price ${fmtPrice(v.price)} 5m ${fmtPct(v.chg.m5)} 1h ${fmtPct(v.chg.h1)} 6h ${fmtPct(v.chg.h6)} 24h ${fmtPct(v.chg.h24)}`);
156
+ L.push(`market cap ${fmtUsd(v.mcap)}${v.mcapFlagged ? ' (flagged)' : ''} liquidity ${fmtUsd(v.liq)} vol 24h ${fmtUsd(v.vol24)}`);
157
+ L.push(`holders ${v.holders ?? '—'} top-10 ${v.top10 === null ? '—' : `${v.top10.toFixed(1)}%`} dev ${v.dev === null ? '—' : `${v.dev.toFixed(1)}%`}${v.holdersSample ? ` (${v.holdersSample})` : ''}`);
158
+ L.push(`age ${v.ageMin === null ? '—' : fmtAge(v.ageMin * 60)}`);
159
+ L.push(`authorities mint ${yesNo(v.mintAuth)} · freeze ${yesNo(v.freezeAuth)}${v.riskyExtensions.length ? ` · risky extensions: ${v.riskyExtensions.join(', ')}` : ''}`);
160
+ if (v.rug)
161
+ L.push(`rugcheck score ${v.rug.score ?? '—'} · ${v.rug.risks.length} risk${v.rug.risks.length === 1 ? '' : 's'}${v.rug.risks.length ? `: ${v.rug.risks.slice(0, 4).map((r) => cleanText(r.name, 40)).join('; ')}` : ''}${v.rug.lpLockedPct !== null ? ` · LP locked ${v.rug.lpLockedPct.toFixed(0)}%` : ''}`);
162
+ if (v.flags.length)
163
+ L.push(`flags ${v.flags.join(', ')}`);
164
+ const row = state.screener.rows.find((r) => r.mint === v.mint);
165
+ const p = row ? rugP(row) : null;
166
+ if (p !== null)
167
+ L.push(`p(rug) ${fmtRugP(p)} · model output: ${RUG_MODEL_LABEL} (a ranking, not a probability; see dexterm backtest)`);
168
+ const larp = state.larpByMint?.get(v.mint);
169
+ if (larp)
170
+ L.push(`LARP check ${larp.verdict}${larp.confidence !== null ? ` · confidence ${larp.confidence.toFixed(2)}` : ''} · model output, ${fmtAge((now - larp.at) / 1000)} old · a risk flag, not a certificate`);
171
+ const links = [v.socialLinks.web, v.socialLinks.x, v.socialLinks.tg].filter((x) => !!x).map((u) => cleanText(u, 120));
172
+ if (links.length)
173
+ L.push(`links ${links.join(' ')} (advertised by the coin; unverified)`);
174
+ return L;
175
+ }
176
+ /** Plain text of every row of a canvas (the generic report body for screens without a dedicated builder). */
177
+ export function canvasText(c) {
178
+ const out = [];
179
+ for (let y = 0; y < c.h; y++)
180
+ out.push(c.ch.slice(y * c.w, (y + 1) * c.w).join('').trimEnd());
181
+ while (out.length && out[out.length - 1] === '')
182
+ out.pop();
183
+ return out;
184
+ }
185
+ /**
186
+ * Render the current screen for export: CALM (no rain / glitch / scanlines), and without the toast, help card or
187
+ * GO-TO prompt on top. The state is restored afterwards, so the live loop is unaffected.
188
+ */
189
+ export function renderCalmFrame(state, w, h) {
190
+ const v = state.vfx;
191
+ const saved = { intensity: v.intensity, toast: v.toast, overlay: state.overlay, goto: state.goto };
192
+ try {
193
+ v.intensity = 'off';
194
+ v.toast = null;
195
+ state.overlay = null;
196
+ state.goto = null;
197
+ return renderFrame(state, w, h);
198
+ }
199
+ finally {
200
+ v.intensity = saved.intensity;
201
+ v.toast = saved.toast;
202
+ state.overlay = saved.overlay;
203
+ state.goto = saved.goto;
204
+ }
205
+ }
206
+ /** The data label of what is on screen: seeded fixtures are SAMPLE, a `--replay` file is REPLAY, else LIVE. */
207
+ export function dataLabelOf(state) {
208
+ return state.screener.source === 'fixtures' ? 'sample' : state.screener.source === 'replay' ? 'replay' : 'live';
209
+ }
210
+ /** Build the ExportView for whatever screen is showing (the token screen gets the full per-panel report). */
211
+ export function exportViewFor(state, w, h, now) {
212
+ const canvas = renderCalmFrame(state, w, h);
213
+ const label = dataLabelOf(state);
214
+ const tv = state.screen === 'token' && label !== 'sample' ? state.live?.token?.view ?? null : null;
215
+ if (tv) {
216
+ return {
217
+ kind: 'token', title: `TOKEN · $${cleanText(tv.symbol, 16)}${tv.name ? ` · ${cleanText(tv.name, 32)}` : ''}`, mint: tv.mint,
218
+ label, canvas, ages: tokenAges(tv, now), report: tokenReport(state, tv, now),
219
+ };
220
+ }
221
+ const sample = label === 'sample' ? [{ name: 'all panels', source: 'seeded fixtures', ageSec: null, state: 'sample data' }] : [];
222
+ if (state.screen === 'token') {
223
+ const tk = state.token || state.fx.tokens[state.selected || 0];
224
+ return { kind: 'token', title: `TOKEN · $${cleanText(tk?.symbol ?? '?', 16)}`, mint: tk?.mint ?? null, label, canvas, ages: sample, report: canvasText(canvas) };
225
+ }
226
+ const ages = [...sample];
227
+ if (label !== 'sample' && state.screener.rows.length) {
228
+ const ts = state.screener.rows.map((r) => r.fetchedAt).filter((t) => typeof t === 'number' && Number.isFinite(t));
229
+ if (ts.length) {
230
+ ages.push({ name: 'rows newest', source: 'screener feed', ageSec: (now - Math.max(...ts)) / 1000 });
231
+ ages.push({ name: 'rows oldest', source: 'screener feed', ageSec: (now - Math.min(...ts)) / 1000 });
232
+ }
233
+ }
234
+ return { kind: state.screen, title: state.screen.toUpperCase(), mint: null, label, canvas, ages, report: canvasText(canvas) };
235
+ }
@@ -78,6 +78,8 @@ export class LiveContext {
78
78
  started = false;
79
79
  fastLaneStarted = false;
80
80
  closed = false;
81
+ /** task-1401: true once close() ran (an async coin-link resolution must not open a token after quit). */
82
+ get isClosed() { return this.closed; }
81
83
  /** Latest SOL/USD, fed from the screener feed each refresh; the fast lane turns on-chain SOL into USD. */
82
84
  solUsd = null;
83
85
  lastSolFetch = 0;