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.
- package/dist/packages/agents/src/bus.js +20 -5
- package/dist/packages/agents/src/executor.js +25 -0
- package/dist/packages/agents/src/gate.js +3 -0
- package/dist/packages/agents/src/identity.js +41 -2
- package/dist/packages/agents/src/tiers.js +5 -0
- package/dist/packages/agents/src/tools.js +3 -0
- 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 +36 -3
- 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 +41 -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/backtest-cmd.js +62 -3
- package/dist/packages/cli/src/coin-target.js +2 -0
- package/dist/packages/cli/src/doctor.js +20 -6
- package/dist/packages/cli/src/export.js +235 -0
- package/dist/packages/cli/src/live/index.js +2 -0
- package/dist/packages/cli/src/main.js +243 -41
- package/dist/packages/cli/src/mcp-cmd.js +326 -0
- package/dist/packages/cli/src/rules.js +21 -2
- package/dist/packages/cli/src/skill-eval-build.js +109 -0
- 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 +119 -0
- package/dist/packages/cli/src/watch-cmd.js +190 -0
- package/dist/packages/custom/src/rules.js +1 -1
- package/dist/packages/custom/src/schema.js +24 -1
- package/dist/packages/data/src/coin-target.js +82 -0
- package/dist/packages/data/src/index.js +3 -0
- package/dist/packages/data/src/intel/helius-history.js +12 -1
- 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/mcp/src/server.js +89 -2
- package/dist/packages/packs/src/index.js +1 -0
- package/dist/packages/packs/src/skill-backtest.js +300 -0
- package/dist/packages/tui/src/index.js +3 -0
- package/dist/packages/tui/src/keymap.js +2 -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 +15 -3
- package/dist/packages/voice/src/doctor.js +75 -7
- package/dist/packages/voice/src/download.js +184 -0
- package/dist/packages/voice/src/engines.js +48 -8
- package/dist/packages/voice/src/index.js +7 -3
- 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.js +206 -0
- package/dist/packages/voice/src/tts/say.js +71 -35
- package/dist/packages/voice/src/tts/voices.js +206 -0
- package/package.json +5 -9
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
import { writeFileSync, existsSync } from 'node:fs';
|
|
18
18
|
import { spawn } from 'node:child_process';
|
|
19
19
|
import { dirname, join } from 'node:path';
|
|
20
|
+
import { homedir } from 'node:os';
|
|
21
|
+
import { writeExport, exportViewFor } from "./export.js";
|
|
22
|
+
import { parseMintTarget, parseCoinTarget, resolveCoinTarget, isBareOpen } from "./coin-target.js";
|
|
20
23
|
import { THEMES, isThemeName, DEFAULT_THEME, THEME_NAMES, Flusher, ENTER_SCREEN, LEAVE_SCREEN, DEFAULT_KEYMAP, Keymap, nextTheme, parseKeys, keyId, detectColorDepth, parseColorDepth, blinkFrame, blinkFrames, stepVfx, pulse, isCalm, fxBusy, blendActivity, ImageLayer, queryTerminal, resolveProtocol, fireMoment, fireInflight, clearInflight, skipMoment, momentKindFor, playMoment, soundOn, breakpoint, fmtUsd as fmtUsdTui, } from '../../tui/src/index.js';
|
|
21
24
|
import { resolveRpcUrl, redactSecrets, defaultDbPath, extraRpcUrls, wsUrlFor, SOLANA_PUBLIC_RPC_WS, parseHttpUrl, validateAnalystAction, ACTION_RATE, ImageService, encodePng, SummaryLimiter, buildSummaryWindow, KolStore, Cache, PackError, normalizeHandle, terminalSafe } from '../../data/src/index.js';
|
|
22
25
|
import { Recorder, recorderEnabled, MaintenanceSchedule } from '../../recorder/src/index.js';
|
|
@@ -35,6 +38,7 @@ import { TokenPopups, tokenDetailVMFromView } from "./popup.js";
|
|
|
35
38
|
import { widgetPage, widgetEnabled, syncWidgetPopups } from "./widgets.js";
|
|
36
39
|
import { recordLarpVerdict } from "./larp-record.js";
|
|
37
40
|
import { rulesUiOf, rulesTick, rulesMove, beginRuleInput, saveRuleInput, dryRunNow, removeSelectedRule, toggleSelectedRule, dismissSelectedProposal, actOnSelectedProposal, RULE_INPUT_MAX } from "./rules.js";
|
|
41
|
+
import { buildNotifier, hasPushChannel } from "./alerts/notifier.js";
|
|
38
42
|
import { launchIntelCells, launchRetryable } from "./live/intel.js";
|
|
39
43
|
import { AnalystSession } from "./analyst/session.js";
|
|
40
44
|
import { resolveAnalystResults } from "./analyst/results.js";
|
|
@@ -46,13 +50,14 @@ import { buildObservations, observeSig } from "./scan/observe.js";
|
|
|
46
50
|
import { cleanText } from "./screener/rows.js";
|
|
47
51
|
import { resolveImagesConfig } from "./images-config.js";
|
|
48
52
|
import { popupImagePath } from "./screens/coin.js";
|
|
49
|
-
import { VoiceController, selectStt, selectTts } from '../../voice/src/index.js';
|
|
53
|
+
import { VoiceController, selectStt, selectTts, SayTts, voiceStyleDefaults } from '../../voice/src/index.js';
|
|
50
54
|
import { LivePlanExecutor, defaultPlansJournalPath } from '../../data/src/trade/plans/index.js';
|
|
51
55
|
import { isWalletAddress, shortAddr, buildMirror, leaderSoldSince, autoSizeCap } from '../../data/src/trade/copy/index.js';
|
|
52
56
|
import { FOLLOW_INPUT_MAX } from "./screens/follows.js";
|
|
53
57
|
import { isPriceMode, configPriceMode, togglePriceMode, priceModeOf, planLevels } from "./display.js";
|
|
58
|
+
import { VERSION } from "./version.js";
|
|
54
59
|
import { initCustom, planWorkspace, planPopupOpens, findWorkspace, nextWorkspaceName, workspaceSlots, saveWorkspace, themeFor, themeByName, cycleTheme, themeNames, CHORD_TTL_MS, MAX_WORKSPACE_NAME } from "./custom.js";
|
|
55
|
-
import { DEFAULT_WORKSPACE, MORNING_HUD } from '../../custom/src/index.js';
|
|
60
|
+
import { DEFAULT_WORKSPACE, MORNING_HUD, applyAndJournal } from '../../custom/src/index.js';
|
|
56
61
|
/**
|
|
57
62
|
* Context the `screen_state` MCP tool returns AND the per-turn context block is rendered from
|
|
58
63
|
* (task-1313): what the user is looking at right now — selected/hovered coin, screen, the visible
|
|
@@ -205,10 +210,13 @@ export function shouldDrawFrame(g) {
|
|
|
205
210
|
return true;
|
|
206
211
|
return g.frame % g.idleEvery === 0;
|
|
207
212
|
}
|
|
208
|
-
export const HELP = `dexterm
|
|
213
|
+
export const HELP = `dexterm ${VERSION} (live: screener, token, new pairs, watchlist · analyst = your local claude CLI · cyberspace look)
|
|
209
214
|
|
|
210
215
|
dexterm interactive, live data (needs a TTY)
|
|
211
216
|
dexterm doctor check this machine is ready (Node, colour, claude, network, Helius key)
|
|
217
|
+
dexterm voice [status|download <model>] voice engines on this machine; download whisper/piper models (asks first, checksum-verified)
|
|
218
|
+
dexterm update [--off|--on] check npm + GitHub for a newer version and print the upgrade command;
|
|
219
|
+
--off/--on toggles the once-a-day startup notice (no telemetry)
|
|
212
220
|
dexterm recorder stats|label|prune|export the history recorder's tables (no network; 'dexterm recorder help')
|
|
213
221
|
dexterm kb list|show|new|edit|check|reset|path your knowledge-base entries (~/.dexterm/kb/entries) over the bundled 17
|
|
214
222
|
dexterm skill list|show|new|edit|check|reset|path your analyst skills (~/.dexterm/skills) over the bundled ones (read tools only)
|
|
@@ -218,6 +226,9 @@ export const HELP = `dexterm 0.1.1 (live: screener, token, new pairs, watchlist
|
|
|
218
226
|
dexterm plan propose|list|arm|cancel|journal conditional order plans (entry/ladder/take-profit/moon bag/stop); arming needs a physical keypress
|
|
219
227
|
dexterm mandate draft|list|arm|revoke standing capped mandates (fast wallet); arming needs a physical keypress — voice/agents can never arm
|
|
220
228
|
dexterm follow add|remove|list|stats|trades|purge copy trading tier 1: follow a wallet (your list, local labels), its decoded swaps + FIFO PnL; alerts in the app
|
|
229
|
+
dexterm mcp [install <client>|doctor] read-only MCP server for your own agent; 'install' writes the config for claude-code|claude-desktop|cursor|codex|openclaw|generic (asks first, idempotent, backs up); 'doctor' verifies the connection
|
|
230
|
+
dexterm alerts status|enable|disable|set-telegram|set-discord|test where rule alerts are delivered (desktop / your Telegram / Discord; creds stay local, never logged)
|
|
231
|
+
dexterm watch [--interval S] [--once] [--replay FILE] run your rules headless and push alerts when the terminal is closed (no signer, proposals only)
|
|
221
232
|
dexterm --fixtures interactive demo on seeded fixtures
|
|
222
233
|
dexterm --screen <name> one-shot frame to stdout (live)
|
|
223
234
|
dexterm --screen <name> --fixtures one-shot frame from fixtures
|
|
@@ -232,7 +243,7 @@ export const HELP = `dexterm 0.1.1 (live: screener, token, new pairs, watchlist
|
|
|
232
243
|
--record FILE save every live response (and the PumpPortal session) to FILE (replay it with --replay)
|
|
233
244
|
--max-requests N hard cap on live requests for this run (default: none)
|
|
234
245
|
--max-credits N hard cap on Helius credits for this run (default: 5000)
|
|
235
|
-
--mint MINT
|
|
246
|
+
--mint MINT|LINK token to open on the token screen (a CA or a dexscreener / pump.fun / solscan / birdeye link)
|
|
236
247
|
--interval IV candle interval for the token screen: ${INTERVALS.join(' | ')} (default 1h)
|
|
237
248
|
--listen SECONDS one-shot new pairs: how long to listen to PumpPortal (default 20)
|
|
238
249
|
--db PATH SQLite path for cache, watchlist, credit meter and history (default: ~/.local/share/dexterm/dexterm.db; ':memory:' for none)
|
|
@@ -296,6 +307,7 @@ ${X_HELP}
|
|
|
296
307
|
--plain monochrome text (snapshot surface)
|
|
297
308
|
--ansi ANSI output even when stdout is not a TTY (24-bit unless --color / NO_COLOR)
|
|
298
309
|
--html FILE write a standalone HTML page (for screenshots); --fit WxH --font PX for video frames
|
|
310
|
+
--export [DIR] write the frame as a PNG + a text report with data ages (default ~/.dexterm/exports; local only)
|
|
299
311
|
--color DEPTH truecolor | 256 | none (default: detected; NO_COLOR honoured)
|
|
300
312
|
--tz ZONE header clock zone (default America/New_York, the prototype's; 'local' = machine zone)
|
|
301
313
|
--title TEXT HTML page title
|
|
@@ -321,12 +333,14 @@ ${X_HELP}
|
|
|
321
333
|
priced from live Jupiter quotes. LIVE (L on a ticket, /wallet link first): a separate signer process
|
|
322
334
|
quotes, builds, decodes, simulates and enforces the caps; Phantom signs in your browser; dexterm never
|
|
323
335
|
holds a key. The analyst, voice and scan mode can never open, confirm or sign a live trade.
|
|
324
|
-
RPC: DEXTERM_RPC_URL, else ~/.dexterm/helius.key (Helius, metered with a credit meter), else the public RPC
|
|
336
|
+
RPC: DEXTERM_RPC_URL, else ~/.dexterm/helius.key (Helius, metered with a credit meter), else the public RPC.
|
|
337
|
+
Updates: at most once a day a live run asks npm + GitHub for the newest version (two anonymous GETs, no telemetry)
|
|
338
|
+
and shows one toast if there is one; DEXTERM_NO_UPDATE_CHECK=1 or 'dexterm update --off' turns that off.`;
|
|
325
339
|
export function parseArgs(argv) {
|
|
326
340
|
const a = {
|
|
327
341
|
screen: null, fixtures: false, replay: null, record: null, filter: null, sort: null, preset: null, maxRequests: 0, maxCredits: 0, db: null,
|
|
328
342
|
mint: null, interval: '1h', listen: 20, verbose: false,
|
|
329
|
-
width: null, height: null, html: null, theme: DEFAULT_THEME, selected: 0, seed: 7,
|
|
343
|
+
width: null, height: null, html: null, export: null, theme: DEFAULT_THEME, selected: 0, seed: 7,
|
|
330
344
|
noAi: false, title: null, plain: false, ansi: false, color: null, tz: null,
|
|
331
345
|
calm: false, intensity: null, fps: null, frame: 0, t: 1, event: null, ai: null, mood: null, activity: null, noBoot: false, fit: null, fontPx: null, asciiRain: false, noImages: false,
|
|
332
346
|
scan: false, scanConfig: {},
|
|
@@ -375,10 +389,13 @@ export function parseArgs(argv) {
|
|
|
375
389
|
else if (k === '--max-credits')
|
|
376
390
|
a.maxCredits = num();
|
|
377
391
|
else if (k === '--mint') {
|
|
392
|
+
// task-1401: a bare mint, or a dexscreener / pump.fun / solscan / birdeye link (a dexscreener pool is resolved to its coin at open)
|
|
378
393
|
const m = str();
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
394
|
+
const t = parseCoinTarget(m);
|
|
395
|
+
if (!t || (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(m.trim()) && t.via === 'address'))
|
|
396
|
+
throw new Error(`--mint expects a base58 Solana address or a coin link, got ${JSON.stringify(m.slice(0, 20))}`);
|
|
397
|
+
a.mint = t.address;
|
|
398
|
+
a.mintVia = t.via;
|
|
382
399
|
}
|
|
383
400
|
else if (k === '--interval') {
|
|
384
401
|
const iv = str();
|
|
@@ -398,6 +415,8 @@ export function parseArgs(argv) {
|
|
|
398
415
|
a.height = num();
|
|
399
416
|
else if (k === '--html')
|
|
400
417
|
a.html = str();
|
|
418
|
+
else if (k === '--export')
|
|
419
|
+
a.export = argv[i + 1] && !argv[i + 1].startsWith('--') ? str() : '';
|
|
401
420
|
else if (k === '--theme')
|
|
402
421
|
a.theme = str();
|
|
403
422
|
else if (k === '--selected')
|
|
@@ -694,11 +713,11 @@ export function fastLaneEndpoints(rpc) {
|
|
|
694
713
|
return out;
|
|
695
714
|
}
|
|
696
715
|
export async function main(argv, io = process, proc = process) {
|
|
697
|
-
// `dexterm mcp` → the
|
|
716
|
+
// `dexterm mcp` → the read-only MCP server on stdio (SPEC §5.1; `claude mcp add dexterm -- dexterm mcp`),
|
|
717
|
+
// plus `dexterm mcp install <client>` (one-command setup) and `dexterm mcp doctor` (verify the connection).
|
|
698
718
|
if (argv[0] === 'mcp') {
|
|
699
|
-
const {
|
|
700
|
-
|
|
701
|
-
return 0;
|
|
719
|
+
const { runMcp } = await import("./mcp-cmd.js");
|
|
720
|
+
return runMcp(argv.slice(1), io);
|
|
702
721
|
}
|
|
703
722
|
// `dexterm agent <cmd>` → bring-your-own-agent registry + loopback bus (task-1345): add/list/rotate/
|
|
704
723
|
// remove/tier/pause/resume/kick/audit/packs, `serve` (a standalone bus on fixtures) and `connect`.
|
|
@@ -761,6 +780,18 @@ export async function main(argv, io = process, proc = process) {
|
|
|
761
780
|
const { rulesCommand } = await import("./rules-cmd.js");
|
|
762
781
|
return rulesCommand(argv.slice(1), { stdout: io.stdout, stderr: io.stderr, env: io.env });
|
|
763
782
|
}
|
|
783
|
+
// `dexterm alerts status|enable|disable|levels|proposals|set-telegram|set-discord|test` → where rule alerts
|
|
784
|
+
// are delivered (task-1398). Credentials live in ~/.dexterm/*.json (0600), never config.json/argv/logs.
|
|
785
|
+
if (argv[0] === 'alerts') {
|
|
786
|
+
const { alertsCommand } = await import("./alerts-cmd.js");
|
|
787
|
+
return alertsCommand(argv.slice(1), { stdout: io.stdout, stderr: io.stderr, env: io.env, stdin: io.stdin });
|
|
788
|
+
}
|
|
789
|
+
// `dexterm watch [--interval S] [--once|--ticks N] [--replay FILE]` → run the rules engine headless and
|
|
790
|
+
// PUSH alerts to the enabled channels so they reach you when the terminal is closed (task-1398). No signer.
|
|
791
|
+
if (argv[0] === 'watch') {
|
|
792
|
+
const { watchCommand } = await import("./watch-cmd.js");
|
|
793
|
+
return watchCommand(argv.slice(1), { stdout: io.stdout, stderr: io.stderr, env: io.env });
|
|
794
|
+
}
|
|
764
795
|
// `dexterm pack list|verify|add|remove|export|lock|sign|trust|signers|keymap|widget` → shareable packs (task-1358 Tier 4):
|
|
765
796
|
// plain files + sha256 lock + optional ssh signature. No network, no model; a pack never runs code (widgets land
|
|
766
797
|
// disabled, unsigned rules load disabled, a keymap merges only on the explicit command).
|
|
@@ -789,6 +820,18 @@ export async function main(argv, io = process, proc = process) {
|
|
|
789
820
|
const { runFollow } = await import("./follow-cmd.js");
|
|
790
821
|
return runFollow(argv.slice(1), io);
|
|
791
822
|
}
|
|
823
|
+
// `dexterm update [--off|--on]` → is there a newer version, and the exact upgrade command for this install (task-1397).
|
|
824
|
+
// Two anonymous GETs (npm registry, GitHub releases); never upgrades anything itself.
|
|
825
|
+
if (argv[0] === 'update') {
|
|
826
|
+
const { updateCommand } = await import("./update.js");
|
|
827
|
+
return updateCommand(argv.slice(1), io);
|
|
828
|
+
}
|
|
829
|
+
// `dexterm voice [status|download <model>]` → inspect the voice engines and download the optional
|
|
830
|
+
// voice models (whisper base.en, Piper voice). Downloads ask first and are checksum-verified (task-1408).
|
|
831
|
+
if (argv[0] === 'voice') {
|
|
832
|
+
const { voiceCommand } = await import("./voice-cmd.js");
|
|
833
|
+
return voiceCommand(argv.slice(1), { stdin: io.stdin, stdout: io.stdout, stderr: io.stderr, env: io.env });
|
|
834
|
+
}
|
|
792
835
|
// `dexterm doctor` → a pre-flight that says whether dexterm will run here and how to fix what won't (task-1305).
|
|
793
836
|
if (argv[0] === 'doctor') {
|
|
794
837
|
const { defaultDeps, runDoctor, renderDoctor } = await import("./doctor.js");
|
|
@@ -838,7 +881,7 @@ export async function main(argv, io = process, proc = process) {
|
|
|
838
881
|
return 0;
|
|
839
882
|
}
|
|
840
883
|
if (args.version) {
|
|
841
|
-
io.stdout.write(
|
|
884
|
+
io.stdout.write(`${VERSION}\n`);
|
|
842
885
|
return 0;
|
|
843
886
|
}
|
|
844
887
|
if (args.unknown.length) {
|
|
@@ -932,6 +975,8 @@ export async function main(argv, io = process, proc = process) {
|
|
|
932
975
|
feed.extraMints = () => [...liveCtx.newpairs.rows.keys()];
|
|
933
976
|
applyScreenerArgs(state, args);
|
|
934
977
|
if (args.mint) {
|
|
978
|
+
if (args.mintVia === 'dexscreener')
|
|
979
|
+
args.mint = (await resolveCoinTarget({ address: args.mint, via: 'dexscreener', rest: '' }, ctx.dl)).mint;
|
|
935
980
|
ctx.openToken({ mint: args.mint, symbol: args.mint.slice(0, 4) + '…', name: '', launchpad: 'unknown' });
|
|
936
981
|
if (args.screen === null)
|
|
937
982
|
state.screen = 'token';
|
|
@@ -1063,7 +1108,13 @@ export async function main(argv, io = process, proc = process) {
|
|
|
1063
1108
|
}
|
|
1064
1109
|
const c = renderFrame(state, w, h);
|
|
1065
1110
|
ctx?.newpairs.markDrawn();
|
|
1066
|
-
if (args.
|
|
1111
|
+
if (args.export !== null) {
|
|
1112
|
+
// task-1401: `--export [DIR]` writes the same PNG + text report the `E` key does (default ~/.dexterm/exports).
|
|
1113
|
+
const now = Date.now();
|
|
1114
|
+
const r = writeExport(exportViewFor(state, w, h, now), { now, env: io.env, dir: args.export || undefined });
|
|
1115
|
+
io.stderr.write(`wrote ${r.png} (${r.bytes} bytes) and ${r.txt}\n`);
|
|
1116
|
+
}
|
|
1117
|
+
else if (args.html) {
|
|
1067
1118
|
// task-1326: `--html … --color 256` renders through the xterm palette (a Terminal.app-faithful proof)
|
|
1068
1119
|
writeFileSync(args.html, c.toHtml({ title: args.title || `dexterm — ${args.screen} — ${state.theme.name}`, fit: args.fit, fontPx: args.fontPx ?? 15, depth: args.color ?? 'truecolor' }));
|
|
1069
1120
|
io.stderr.write(`wrote ${args.html} (${w}×${h})\n`);
|
|
@@ -1087,7 +1138,20 @@ export async function main(argv, io = process, proc = process) {
|
|
|
1087
1138
|
feed?.close();
|
|
1088
1139
|
return 0;
|
|
1089
1140
|
}
|
|
1090
|
-
|
|
1141
|
+
// task-1397: live interactive runs only: the cached "new version" notice (no network) plus, at most once a day,
|
|
1142
|
+
// a background refresh that never blocks the start. Off with DEXTERM_NO_UPDATE_CHECK=1 / `dexterm update --off`.
|
|
1143
|
+
const update = live ? (await import("./update.js")).startupCheck(io.env) : null;
|
|
1144
|
+
if (update) {
|
|
1145
|
+
state.updateNotice = update.notice;
|
|
1146
|
+
void update.refresh?.then((n) => { if (n)
|
|
1147
|
+
state.updateNotice = n; });
|
|
1148
|
+
}
|
|
1149
|
+
try {
|
|
1150
|
+
return await interactive(state, io, depth, args, proc);
|
|
1151
|
+
}
|
|
1152
|
+
finally {
|
|
1153
|
+
update?.cancel();
|
|
1154
|
+
}
|
|
1091
1155
|
}
|
|
1092
1156
|
const SCREEN_ORDER = ['screener', 'newpairs', 'watchlist', 'analyst'];
|
|
1093
1157
|
/** Rows PgUp/PgDn jump through the list (task-1303). */
|
|
@@ -2269,6 +2333,10 @@ export function applyAction(state, action, stack) {
|
|
|
2269
2333
|
case 'goto.open':
|
|
2270
2334
|
state.goto = { input: '', notice: null };
|
|
2271
2335
|
break;
|
|
2336
|
+
// task-1401 EXPORT: the loop renders the current view calm and writes the PNG + report (local files only).
|
|
2337
|
+
case 'export.view':
|
|
2338
|
+
state.pendingExport = true;
|
|
2339
|
+
break;
|
|
2272
2340
|
case 'display.priceMode':
|
|
2273
2341
|
state.pendingToast = togglePriceMode(state);
|
|
2274
2342
|
break; // task-1366: display only, persisted
|
|
@@ -2518,31 +2586,81 @@ function seedForMint(state, live, mint, symbol) {
|
|
|
2518
2586
|
// task-1388 GO-TO prompt: the longest input we keep (a bare base58 mint is ≤ 44; the longest coin URL we
|
|
2519
2587
|
// accept is well under this), so a huge paste can never balloon the buffer.
|
|
2520
2588
|
const GOTO_INPUT_MAX = 120;
|
|
2521
|
-
|
|
2522
|
-
|
|
2589
|
+
// task-1388's parser now lives in coin-target.ts (shared with the analyst chat, the filter line and --mint).
|
|
2590
|
+
export { parseMintTarget };
|
|
2523
2591
|
/**
|
|
2524
|
-
* task-
|
|
2525
|
-
*
|
|
2526
|
-
*
|
|
2527
|
-
*
|
|
2528
|
-
*
|
|
2592
|
+
* task-1401: open a pasted coin target (a CA, or a dexscreener / pump.fun / solscan / birdeye link) on the LIVE token
|
|
2593
|
+
* screen — the same read-only path `--screen token --mint` and the analyst's open_token use. A mint opens at once; a
|
|
2594
|
+
* dexscreener link is first resolved to its coin (its address is usually the pool) and opens when that answers.
|
|
2595
|
+
* `onOpened` runs after the open either way (the chat uses it to ask its question about the coin). Returns a notice
|
|
2596
|
+
* when it cannot open (fixtures mode), else null. Never trades, never signs.
|
|
2529
2597
|
*/
|
|
2530
|
-
export function
|
|
2531
|
-
const
|
|
2532
|
-
if (!
|
|
2598
|
+
export function openCoinTarget(state, stack, t, onOpened) {
|
|
2599
|
+
const live = state.live && state.screener.source !== 'fixtures' ? state.live : null;
|
|
2600
|
+
if (!live)
|
|
2601
|
+
return 'opening a coin by address needs live mode (not --fixtures)';
|
|
2602
|
+
const open = (mint) => {
|
|
2603
|
+
if (!(state.screen === 'token' && live.token?.view.mint === mint))
|
|
2604
|
+
live.openToken(seedForMint(state, live, mint)); // already open: keep its loaded view
|
|
2605
|
+
if (state.screen !== 'token') {
|
|
2606
|
+
stack.push(state.screen);
|
|
2607
|
+
state.screen = 'token';
|
|
2608
|
+
}
|
|
2609
|
+
onOpened?.(mint);
|
|
2610
|
+
};
|
|
2611
|
+
if (t.via !== 'dexscreener') {
|
|
2612
|
+
open(t.address);
|
|
2533
2613
|
return null;
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
if (
|
|
2538
|
-
return
|
|
2614
|
+
}
|
|
2615
|
+
live.notice = 'resolving the dexscreener link…';
|
|
2616
|
+
void resolveCoinTarget(t, live.dl).then(({ mint, fromPair }) => {
|
|
2617
|
+
if (live.isClosed)
|
|
2618
|
+
return;
|
|
2619
|
+
live.notice = fromPair ? `dexscreener pool → coin ${mint.slice(0, 4)}…${mint.slice(-4)}` : null;
|
|
2620
|
+
open(mint);
|
|
2621
|
+
state.aiFocused = false;
|
|
2622
|
+
syncAnalyst(state);
|
|
2623
|
+
live.onChange('goto');
|
|
2624
|
+
});
|
|
2539
2625
|
return null;
|
|
2540
2626
|
}
|
|
2627
|
+
/**
|
|
2628
|
+
* task-1401: the analyst chat's ⏎. When the typed text names a coin (a CA or a coin link), open that coin's token
|
|
2629
|
+
* view and, if a question came with it ("is <CA> a rug?"), ask it about THAT coin; returns true when it handled
|
|
2630
|
+
* the input. Plain questions return false and go to the analyst as before.
|
|
2631
|
+
*/
|
|
2632
|
+
export function chatOpenCoin(state, stack, q, hooks) {
|
|
2633
|
+
const ct = q ? parseCoinTarget(q) : null;
|
|
2634
|
+
if (!ct)
|
|
2635
|
+
return false;
|
|
2636
|
+
state.aiInput = '';
|
|
2637
|
+
const question = isBareOpen(ct.rest) ? '' : ct.rest;
|
|
2638
|
+
const notice = openCoinTarget(state, stack, ct, (mint) => {
|
|
2639
|
+
syncAnalyst(state);
|
|
2640
|
+
if (question) {
|
|
2641
|
+
state.aiOpen = true;
|
|
2642
|
+
try {
|
|
2643
|
+
hooks.ask(question);
|
|
2644
|
+
}
|
|
2645
|
+
catch (e) {
|
|
2646
|
+
state.screener.notice = `⚠ ask failed: ${describeError(e)}`;
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
else {
|
|
2650
|
+
state.aiFocused = false;
|
|
2651
|
+
hooks.say(`opened ${mint.slice(0, 4)}…${mint.slice(-4)} from the chat`, 'info', 4);
|
|
2652
|
+
}
|
|
2653
|
+
});
|
|
2654
|
+
if (notice)
|
|
2655
|
+
hooks.say(notice, 'warn', 5);
|
|
2656
|
+
return true;
|
|
2657
|
+
}
|
|
2541
2658
|
/**
|
|
2542
2659
|
* task-1388: resolve the GO-TO prompt's input to a coin and open its live token screen (read-only — the same
|
|
2543
2660
|
* path `--screen token --mint` and the analyst's open_token use: live.openToken(seedForMint(...)), which seeds
|
|
2544
2661
|
* an unknown-launchpad stub when the mint is not already on a screen). Bad input keeps the prompt open with a
|
|
2545
2662
|
* one-line notice and changes nothing; a good open closes the prompt and pushes the current screen on the stack.
|
|
2663
|
+
* task-1401: a dexscreener link is resolved pool → coin first (openCoinTarget).
|
|
2546
2664
|
*/
|
|
2547
2665
|
function openGotoTarget(state, stack) {
|
|
2548
2666
|
const g = state.goto;
|
|
@@ -2558,17 +2676,13 @@ function openGotoTarget(state, stack) {
|
|
|
2558
2676
|
g.notice = 'not a contract address or a dexscreener / pump.fun / solscan link';
|
|
2559
2677
|
return;
|
|
2560
2678
|
}
|
|
2561
|
-
const
|
|
2562
|
-
|
|
2563
|
-
|
|
2679
|
+
const t = parseCoinTarget(raw) ?? { address: mint, via: 'address', rest: '' };
|
|
2680
|
+
const notice = openCoinTarget(state, stack, t);
|
|
2681
|
+
if (notice) {
|
|
2682
|
+
g.notice = notice;
|
|
2564
2683
|
return;
|
|
2565
2684
|
}
|
|
2566
|
-
live.openToken(seedForMint(state, live, mint));
|
|
2567
2685
|
state.goto = null;
|
|
2568
|
-
if (state.screen !== 'token') {
|
|
2569
|
-
stack.push(state.screen);
|
|
2570
|
-
state.screen = 'token';
|
|
2571
|
-
}
|
|
2572
2686
|
}
|
|
2573
2687
|
/**
|
|
2574
2688
|
* Apply one analyst-requested UI action to the state (task-1313). This is the trust boundary: every
|
|
@@ -2918,6 +3032,18 @@ export function applyKey(state, key, stack) {
|
|
|
2918
3032
|
state.pendingToast = typeof cmd === 'object' ? cmd.error : scanCommand(state, cmd, stack);
|
|
2919
3033
|
return;
|
|
2920
3034
|
}
|
|
3035
|
+
// task-1401: a pasted CA or coin link on the `/` line opens that coin (no filter field is a 32-44 char base58
|
|
3036
|
+
// run); the editor closes and the previous filter stays applied.
|
|
3037
|
+
const ct = parseCoinTarget(sc.edit.text);
|
|
3038
|
+
if (ct && isBareOpen(ct.rest)) {
|
|
3039
|
+
sc.mode = 'table';
|
|
3040
|
+
sc.edit.text = sc.query ?? '';
|
|
3041
|
+
sc.edit.error = null;
|
|
3042
|
+
const notice = openCoinTarget(state, stack, ct);
|
|
3043
|
+
if (notice)
|
|
3044
|
+
state.pendingToast = notice;
|
|
3045
|
+
return;
|
|
3046
|
+
}
|
|
2921
3047
|
// task-1328: `/wallet …` is a command too (the loop performs it: link opens a browser, probe opens a ticket).
|
|
2922
3048
|
const wc = parseWalletCommand(sc.edit.text);
|
|
2923
3049
|
if (wc) {
|
|
@@ -4099,6 +4225,24 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
4099
4225
|
say(`skipped ${plan.skipped.map((s) => `${s.kind} (${s.why})`).join(', ')}`, 'info', 5);
|
|
4100
4226
|
scheduleDraw();
|
|
4101
4227
|
};
|
|
4228
|
+
// task-1398: PUSH delivery while the TUI is open. Built only when the user enabled a push channel
|
|
4229
|
+
// (desktop/telegram/discord); otherwise null and there is no behaviour change. The in-app toast is
|
|
4230
|
+
// still state.pendingToast, so the Notifier's toast sink is a no-op here (routing may still include it,
|
|
4231
|
+
// harmlessly). Delivery is fire-and-forget: a slow channel never blocks the frame.
|
|
4232
|
+
const alertNotifier = state.custom && hasPushChannel(state.custom.config.alerts)
|
|
4233
|
+
? buildNotifier(state.custom.home, state.custom.config.alerts, { toastSink: () => { } })
|
|
4234
|
+
: null;
|
|
4235
|
+
const drainDeliveries = () => {
|
|
4236
|
+
const ui = state.rulesUi;
|
|
4237
|
+
if (!ui || !ui.pendingDeliveries.length)
|
|
4238
|
+
return;
|
|
4239
|
+
const ds = ui.pendingDeliveries;
|
|
4240
|
+
ui.pendingDeliveries = [];
|
|
4241
|
+
if (!alertNotifier)
|
|
4242
|
+
return;
|
|
4243
|
+
for (const d of ds)
|
|
4244
|
+
void alertNotifier.deliver(d).catch(() => { });
|
|
4245
|
+
};
|
|
4102
4246
|
// task-1358 Tier 3: perform what the rules engine / an accepted proposal queued. A ticket proposal opens the
|
|
4103
4247
|
// PAPER ticket in its editing phase (the user sizes + confirms there; signer caps apply) — never a live ticket,
|
|
4104
4248
|
// never a confirm. An ask proposal is a question to the analyst. Popups are the same read-only windows the keys
|
|
@@ -4456,26 +4600,49 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
4456
4600
|
else
|
|
4457
4601
|
voice.abortTurn(session.status === 'error' ? "Sorry — the analyst hit an error. Try again." : "Okay, stopped.");
|
|
4458
4602
|
};
|
|
4603
|
+
// task-1393: persist the `,` voice choice to config.voice.name (actor 'user', journaled → undo works).
|
|
4604
|
+
// Best-effort: a persistence failure keeps the voice for this run and never breaks the UI.
|
|
4605
|
+
const persistVoiceChoice = (voiceName) => {
|
|
4606
|
+
const ui = state.custom;
|
|
4607
|
+
if (!ui)
|
|
4608
|
+
return;
|
|
4609
|
+
try {
|
|
4610
|
+
const op = ui.config.voice
|
|
4611
|
+
? { op: 'add', path: '/voice/name', value: voiceName }
|
|
4612
|
+
: { op: 'add', path: '/voice', value: { name: voiceName } };
|
|
4613
|
+
const { config } = applyAndJournal(ui.home, [op], { actor: 'user', why: `, voice ${voiceName}`, now: state.now.getTime() });
|
|
4614
|
+
ui.config = config;
|
|
4615
|
+
}
|
|
4616
|
+
catch { /* keep the voice for this run; nothing to surface */ }
|
|
4617
|
+
};
|
|
4459
4618
|
const initVoice = async () => {
|
|
4460
4619
|
if (voiceInitStarted)
|
|
4461
4620
|
return;
|
|
4462
4621
|
voiceInitStarted = true;
|
|
4463
4622
|
state.voice = { enabled: true, mode: 'idle', muted: false, voice: null, transcript: null, say: null, message: 'preparing voice…' };
|
|
4464
4623
|
scheduleDraw();
|
|
4624
|
+
// task-1393: the user's saved voice/rate (if any). A missing/banned name is resolved to a safe
|
|
4625
|
+
// deep British male by selectTts — it never falls back to the machine's system voice.
|
|
4626
|
+
const cfgVoice = state.custom?.config.voice;
|
|
4465
4627
|
let stt = null, tts = null;
|
|
4466
4628
|
try {
|
|
4467
|
-
[stt, tts] = await Promise.all([selectStt(), selectTts()]);
|
|
4629
|
+
[stt, tts] = await Promise.all([selectStt(), selectTts(undefined, cfgVoice?.name ?? null)]);
|
|
4468
4630
|
}
|
|
4469
4631
|
catch { /* both null → degrade to text */ }
|
|
4470
4632
|
if (done)
|
|
4471
4633
|
return;
|
|
4634
|
+
// Seed the controller with the ENGINE's resolved voice so its cycle/status start on the British male,
|
|
4635
|
+
// not on voices[0] (which could be a novelty voice like "Albert").
|
|
4636
|
+
const resolvedVoice = tts instanceof SayTts ? tts.voiceName : undefined;
|
|
4637
|
+
const style = voiceStyleDefaults(resolvedVoice); // the picked voice's own rate/pitch, unless the user saved theirs
|
|
4472
4638
|
voice = new VoiceController({
|
|
4473
|
-
stt, tts, getScreenState: voiceScreenState,
|
|
4639
|
+
stt, tts, voice: resolvedVoice, rate: cfgVoice?.rate ?? style?.rate, pitch: cfgVoice?.pitch ?? style?.pitch, getScreenState: voiceScreenState,
|
|
4474
4640
|
onState: () => { if (!done && voice) {
|
|
4475
4641
|
state.voice = toHud();
|
|
4476
4642
|
scheduleDraw();
|
|
4477
4643
|
} },
|
|
4478
4644
|
onDecision: handleVoiceDecision,
|
|
4645
|
+
onVoiceChange: persistVoiceChoice,
|
|
4479
4646
|
});
|
|
4480
4647
|
voice.enable();
|
|
4481
4648
|
await voice.toggleTalk(); // the first `v` press both enables and starts listening
|
|
@@ -5016,6 +5183,21 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
5016
5183
|
draw();
|
|
5017
5184
|
continue;
|
|
5018
5185
|
}
|
|
5186
|
+
// task-1401: a CA or a dexscreener / pump.fun / solscan / birdeye link in the chat opens that coin's token
|
|
5187
|
+
// view (read-only, the go-to path); any question typed with it is then asked about THAT coin.
|
|
5188
|
+
const before = state.screen;
|
|
5189
|
+
if (chatOpenCoin(state, stack, q, { ask: sendChat, say })) {
|
|
5190
|
+
if (state.screen !== before) {
|
|
5191
|
+
try {
|
|
5192
|
+
startTransition();
|
|
5193
|
+
}
|
|
5194
|
+
catch {
|
|
5195
|
+
transition = null;
|
|
5196
|
+
}
|
|
5197
|
+
}
|
|
5198
|
+
draw();
|
|
5199
|
+
continue;
|
|
5200
|
+
}
|
|
5019
5201
|
if (q) {
|
|
5020
5202
|
state.aiInput = '';
|
|
5021
5203
|
try {
|
|
@@ -5254,6 +5436,19 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
5254
5436
|
copyToClipboard(m);
|
|
5255
5437
|
say(`copied CA ${m.slice(0, 4)}…${m.slice(-4)}`, 'info');
|
|
5256
5438
|
}
|
|
5439
|
+
if (state.pendingExport) {
|
|
5440
|
+
// task-1401: one key → PNG + text report of the current view in ~/.dexterm/exports. Nothing is uploaded.
|
|
5441
|
+
state.pendingExport = false;
|
|
5442
|
+
try {
|
|
5443
|
+
const [ew, eh] = size();
|
|
5444
|
+
const now = Date.now();
|
|
5445
|
+
const r = writeExport(exportViewFor(state, ew, eh, now), { now, env: io.env });
|
|
5446
|
+
say(`exported PNG + report → ${r.png.replace(homedir(), '~')} · local only, nothing uploaded`, 'info', 8);
|
|
5447
|
+
}
|
|
5448
|
+
catch (e) {
|
|
5449
|
+
say(`export failed: ${describeError(e)}`, 'warn', 6);
|
|
5450
|
+
}
|
|
5451
|
+
}
|
|
5257
5452
|
if (state.pendingToast) {
|
|
5258
5453
|
// task-1315: surface the link list / "no link" reply as a durable toast. Drained BEFORE
|
|
5259
5454
|
// pendingOpen so that on a successful open the "opened <url>" toast overwrites the "open <url>" one.
|
|
@@ -5317,6 +5512,11 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
5317
5512
|
endBoot();
|
|
5318
5513
|
}
|
|
5319
5514
|
if (v.frame % tickEvery === 0) {
|
|
5515
|
+
// task-1397: the "new version available" toast, once, after the boot and never over the welcome card.
|
|
5516
|
+
if (state.updateNotice && state.screen !== 'boot' && !state.overlay) {
|
|
5517
|
+
say(state.updateNotice, 'info', 8);
|
|
5518
|
+
state.updateNotice = null;
|
|
5519
|
+
}
|
|
5320
5520
|
if (feed) {
|
|
5321
5521
|
if (!state.paused && Date.now() - lastRefresh >= LIVE_REFRESH_MS)
|
|
5322
5522
|
refresh();
|
|
@@ -5361,6 +5561,7 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
5361
5561
|
if (rulesTick(state, Date.now()))
|
|
5362
5562
|
flusher.invalidate();
|
|
5363
5563
|
drainRulesPending();
|
|
5564
|
+
drainDeliveries();
|
|
5364
5565
|
}
|
|
5365
5566
|
catch (e) {
|
|
5366
5567
|
state.screener.notice = `⚠ rules tick failed: ${describeError(e)}`;
|
|
@@ -5391,6 +5592,7 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
5391
5592
|
if (rulesTick(state, Date.now()))
|
|
5392
5593
|
flusher.invalidate();
|
|
5393
5594
|
drainRulesPending();
|
|
5595
|
+
drainDeliveries();
|
|
5394
5596
|
}
|
|
5395
5597
|
catch (e) {
|
|
5396
5598
|
state.screener.notice = `⚠ rules tick failed: ${describeError(e)}`;
|