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
|
@@ -17,8 +17,13 @@
|
|
|
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 { openPnl, loadPnl, pnlKey } from "./pnl-app.js";
|
|
23
|
+
import { shareCard, runLocal } from "./pnl-card.js";
|
|
24
|
+
import { parseMintTarget, parseCoinTarget, resolveCoinTarget, isBareOpen } from "./coin-target.js";
|
|
20
25
|
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
|
-
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';
|
|
26
|
+
import { resolveRpcUrl, redactSecrets, defaultDbPath, extraRpcUrls, wsUrlFor, SOLANA_PUBLIC_RPC_WS, parseHttpUrl, validateAnalystAction, ACTION_RATE, ACTION_TIER, configChangeForAction, ImageService, encodePng, SummaryLimiter, buildSummaryWindow, KolStore, Cache, PackError, normalizeHandle, terminalSafe } from '../../data/src/index.js';
|
|
22
27
|
import { Recorder, recorderEnabled, MaintenanceSchedule } from '../../recorder/src/index.js';
|
|
23
28
|
import { makeFixtureState, makeFeedState, syncFeed, renderFrame, isScreenKey, activityFor, SCREEN_NAMES, analystKey, syncAnalyst, pinNewpairsSelection, ANALYST_TAB_KEY, computeNarrativeView, newPlansUi, plansService, plansRows, newFollowsUi, copyService, copyAutoService, MIRROR_DEFAULT_CAP_USD } from "./state.js";
|
|
24
29
|
import { mulberry32, tick, spawnPair, shock } from "./fixtures.js";
|
|
@@ -35,6 +40,9 @@ import { TokenPopups, tokenDetailVMFromView } from "./popup.js";
|
|
|
35
40
|
import { widgetPage, widgetEnabled, syncWidgetPopups } from "./widgets.js";
|
|
36
41
|
import { recordLarpVerdict } from "./larp-record.js";
|
|
37
42
|
import { rulesUiOf, rulesTick, rulesMove, beginRuleInput, saveRuleInput, dryRunNow, removeSelectedRule, toggleSelectedRule, dismissSelectedProposal, actOnSelectedProposal, RULE_INPUT_MAX } from "./rules.js";
|
|
43
|
+
import { buildNotifier, hasPushChannel } from "./alerts/notifier.js";
|
|
44
|
+
import { copyAlertToDelivery } from "./alerts/format.js";
|
|
45
|
+
import { readTelegramCreds, readDiscordCreds } from "./alerts/creds.js";
|
|
38
46
|
import { launchIntelCells, launchRetryable } from "./live/intel.js";
|
|
39
47
|
import { AnalystSession } from "./analyst/session.js";
|
|
40
48
|
import { resolveAnalystResults } from "./analyst/results.js";
|
|
@@ -46,13 +54,14 @@ import { buildObservations, observeSig } from "./scan/observe.js";
|
|
|
46
54
|
import { cleanText } from "./screener/rows.js";
|
|
47
55
|
import { resolveImagesConfig } from "./images-config.js";
|
|
48
56
|
import { popupImagePath } from "./screens/coin.js";
|
|
49
|
-
import { VoiceController, selectStt, selectTts } from '../../voice/src/index.js';
|
|
57
|
+
import { VoiceController, selectStt, selectTts, SayTts, voiceStyleDefaults } from '../../voice/src/index.js';
|
|
50
58
|
import { LivePlanExecutor, defaultPlansJournalPath } from '../../data/src/trade/plans/index.js';
|
|
51
59
|
import { isWalletAddress, shortAddr, buildMirror, leaderSoldSince, autoSizeCap } from '../../data/src/trade/copy/index.js';
|
|
52
60
|
import { FOLLOW_INPUT_MAX } from "./screens/follows.js";
|
|
53
61
|
import { isPriceMode, configPriceMode, togglePriceMode, priceModeOf, planLevels } from "./display.js";
|
|
62
|
+
import { VERSION } from "./version.js";
|
|
54
63
|
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';
|
|
64
|
+
import { DEFAULT_WORKSPACE, MORNING_HUD, applyAndJournal, policyCheck } from '../../custom/src/index.js';
|
|
56
65
|
/**
|
|
57
66
|
* Context the `screen_state` MCP tool returns AND the per-turn context block is rendered from
|
|
58
67
|
* (task-1313): what the user is looking at right now — selected/hovered coin, screen, the visible
|
|
@@ -60,6 +69,37 @@ import { DEFAULT_WORKSPACE, MORNING_HUD } from '../../custom/src/index.js';
|
|
|
60
69
|
* that bounds what the analyst's UI actions may reference. Token-derived text stays raw here; the
|
|
61
70
|
* context-block renderer wraps it as untrusted before a model sees it.
|
|
62
71
|
*/
|
|
72
|
+
/** Screens the analyst's `switch_screen` action may target (task-1413); the user-facing set. */
|
|
73
|
+
export const SWITCHABLE_SCREENS = ['screener', 'token', 'newpairs', 'watchlist', 'analyst', 'positions', 'trending', 'kol'];
|
|
74
|
+
/**
|
|
75
|
+
* A read-only snapshot of local UI state for the `app_state` MCP tool (task-1413): what the user sees.
|
|
76
|
+
* NEVER includes a secret — alert channels report a boolean "configured", computed from creds presence,
|
|
77
|
+
* never the token/webhook/key itself. `null` when there is no live config (one-shot fixture frames).
|
|
78
|
+
*/
|
|
79
|
+
export function appStateBlock(state) {
|
|
80
|
+
const ui = state.custom;
|
|
81
|
+
if (!ui)
|
|
82
|
+
return null;
|
|
83
|
+
const cfg = ui.config;
|
|
84
|
+
const home = ui.home;
|
|
85
|
+
const ch = (cfg.alerts?.channels ?? {});
|
|
86
|
+
const alerts = {
|
|
87
|
+
// toast is on unless explicitly disabled; the rest are off unless enabled. telegram/discord also need creds.
|
|
88
|
+
toast: ch.toast?.enabled !== false,
|
|
89
|
+
desktop: ch.desktop?.enabled === true,
|
|
90
|
+
telegram: ch.telegram?.enabled === true && readTelegramCreds(home) !== null,
|
|
91
|
+
discord: ch.discord?.enabled === true && readDiscordCreds(home) !== null,
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
theme: state.theme.name,
|
|
95
|
+
priceMode: priceModeOf(state),
|
|
96
|
+
intensity: state.vfx.intensity,
|
|
97
|
+
calm: isCalm(state.vfx),
|
|
98
|
+
voice: { name: cfg.voice?.name ?? null, rate: cfg.voice?.rate ?? null },
|
|
99
|
+
scan: { on: state.scan?.engine.on ?? false },
|
|
100
|
+
alerts, // booleans only — never a token, webhook or key
|
|
101
|
+
};
|
|
102
|
+
}
|
|
63
103
|
export function buildScreenState(state) {
|
|
64
104
|
const live = state.live;
|
|
65
105
|
const sc = state.screener;
|
|
@@ -122,7 +162,14 @@ export function buildScreenState(state) {
|
|
|
122
162
|
watchlist,
|
|
123
163
|
ticket,
|
|
124
164
|
positions,
|
|
125
|
-
actions: {
|
|
165
|
+
actions: {
|
|
166
|
+
enabled: !!live, mints: [...mints], feeds, sortKeys: SORT_KEYS.map((k) => k.key),
|
|
167
|
+
// task-1413: targets the new tiered actions may reference.
|
|
168
|
+
screens: [...SWITCHABLE_SCREENS],
|
|
169
|
+
workspaces: Object.keys(state.custom?.config.layout?.workspaces ?? {}),
|
|
170
|
+
themes: themeNames(state.custom),
|
|
171
|
+
},
|
|
172
|
+
app: appStateBlock(state),
|
|
126
173
|
now: Date.now(),
|
|
127
174
|
};
|
|
128
175
|
}
|
|
@@ -205,10 +252,13 @@ export function shouldDrawFrame(g) {
|
|
|
205
252
|
return true;
|
|
206
253
|
return g.frame % g.idleEvery === 0;
|
|
207
254
|
}
|
|
208
|
-
export const HELP = `dexterm
|
|
255
|
+
export const HELP = `dexterm ${VERSION} (live: screener, token, new pairs, watchlist · analyst = your local claude CLI · cyberspace look)
|
|
209
256
|
|
|
210
257
|
dexterm interactive, live data (needs a TTY)
|
|
211
258
|
dexterm doctor check this machine is ready (Node, colour, claude, network, Helius key)
|
|
259
|
+
dexterm voice [status|download <model>] voice engines on this machine; download whisper/piper models (asks first, checksum-verified)
|
|
260
|
+
dexterm update [--off|--on] check npm + GitHub for a newer version and print the upgrade command;
|
|
261
|
+
--off/--on toggles the once-a-day startup notice (no telemetry)
|
|
212
262
|
dexterm recorder stats|label|prune|export the history recorder's tables (no network; 'dexterm recorder help')
|
|
213
263
|
dexterm kb list|show|new|edit|check|reset|path your knowledge-base entries (~/.dexterm/kb/entries) over the bundled 17
|
|
214
264
|
dexterm skill list|show|new|edit|check|reset|path your analyst skills (~/.dexterm/skills) over the bundled ones (read tools only)
|
|
@@ -217,7 +267,11 @@ export const HELP = `dexterm 0.1.1 (live: screener, token, new pairs, watchlist
|
|
|
217
267
|
dexterm wallet link|unlink|status|panic|resume link a Phantom wallet via a local bridge (pubkey only); panic disables live trading, resume needs a TTY keypress
|
|
218
268
|
dexterm plan propose|list|arm|cancel|journal conditional order plans (entry/ladder/take-profit/moon bag/stop); arming needs a physical keypress
|
|
219
269
|
dexterm mandate draft|list|arm|revoke standing capped mandates (fast wallet); arming needs a physical keypress — voice/agents can never arm
|
|
220
|
-
dexterm follow add|remove|list|stats|trades|purge copy trading tier 1: follow a wallet (your list, local labels), its decoded swaps + FIFO PnL;
|
|
270
|
+
dexterm follow add|remove|list|stats|trades|mirror|purge copy trading tier 1: follow a wallet (your list, local labels), its decoded swaps + FIFO PnL + win rate/early-entry; 'mirror' previews a one-key MIRROR TICKET from the wallet's latest buy (inert; arm in the app)
|
|
271
|
+
dexterm mcp [install <client>|doctor] read-only MCP server for your own agent; 'install' writes the config for claude-code|claude-desktop|cursor|codex|generic (asks first, idempotent, backs up); 'doctor' verifies the connection
|
|
272
|
+
dexterm alerts status|enable|disable|set-telegram|set-discord|test where rule alerts are delivered (desktop / your Telegram / Discord; creds stay local, never logged)
|
|
273
|
+
dexterm watch [--interval S] [--once] [--replay FILE] run your rules headless and push alerts when the terminal is closed (no signer, proposals only)
|
|
274
|
+
dexterm record run|install|uninstall|status keep the history recorder fed with no TUI open, as a user-level launchd agent (no network beyond the data layer; 'dexterm record help')
|
|
221
275
|
dexterm --fixtures interactive demo on seeded fixtures
|
|
222
276
|
dexterm --screen <name> one-shot frame to stdout (live)
|
|
223
277
|
dexterm --screen <name> --fixtures one-shot frame from fixtures
|
|
@@ -232,7 +286,7 @@ export const HELP = `dexterm 0.1.1 (live: screener, token, new pairs, watchlist
|
|
|
232
286
|
--record FILE save every live response (and the PumpPortal session) to FILE (replay it with --replay)
|
|
233
287
|
--max-requests N hard cap on live requests for this run (default: none)
|
|
234
288
|
--max-credits N hard cap on Helius credits for this run (default: 5000)
|
|
235
|
-
--mint MINT
|
|
289
|
+
--mint MINT|LINK token to open on the token screen (a CA or a dexscreener / pump.fun / solscan / birdeye link)
|
|
236
290
|
--interval IV candle interval for the token screen: ${INTERVALS.join(' | ')} (default 1h)
|
|
237
291
|
--listen SECONDS one-shot new pairs: how long to listen to PumpPortal (default 20)
|
|
238
292
|
--db PATH SQLite path for cache, watchlist, credit meter and history (default: ~/.local/share/dexterm/dexterm.db; ':memory:' for none)
|
|
@@ -296,6 +350,7 @@ ${X_HELP}
|
|
|
296
350
|
--plain monochrome text (snapshot surface)
|
|
297
351
|
--ansi ANSI output even when stdout is not a TTY (24-bit unless --color / NO_COLOR)
|
|
298
352
|
--html FILE write a standalone HTML page (for screenshots); --fit WxH --font PX for video frames
|
|
353
|
+
--export [DIR] write the frame as a PNG + a text report with data ages (default ~/.dexterm/exports; local only)
|
|
299
354
|
--color DEPTH truecolor | 256 | none (default: detected; NO_COLOR honoured)
|
|
300
355
|
--tz ZONE header clock zone (default America/New_York, the prototype's; 'local' = machine zone)
|
|
301
356
|
--title TEXT HTML page title
|
|
@@ -303,9 +358,10 @@ ${X_HELP}
|
|
|
303
358
|
--version print the version
|
|
304
359
|
|
|
305
360
|
Keys: 1-4 screens · tab cycle · j/k move · ⏎ open · esc back · a analyst · v voice · t theme · c calm · p pause · q quit
|
|
306
|
-
voice (JARVIS, macOS): v push-to-talk (press to talk, press to send) · m mute · , change voice · esc stop speaking
|
|
361
|
+
voice (JARVIS, macOS): v push-to-talk (press to talk, press to send) · m mute · , change voice · < / > slower/faster · esc stop speaking
|
|
307
362
|
voice asks the analyst or triggers safe actions (open/filter/watch/open a PAPER ticket);
|
|
308
363
|
a spoken "buy"/"sell" only OPENS a ticket — you still confirm it with a keypress
|
|
364
|
+
spoken replies talk in market cap and round numbers (never a raw per-token price); < / > set the rate (saved to config)
|
|
309
365
|
screener: / filter · s sort · [ ] presets · r refresh · g/G top/bottom
|
|
310
366
|
social (task-1330): T trending in the space · K KOL lists · token screen: s social panel · f filter · x hidden spam
|
|
311
367
|
dexterm kol list | create <name> | add <list> @handle | remove <list> @handle | mute @handle | unmute @handle | enable <list> | disable <list> | import <dir> | import-x
|
|
@@ -321,12 +377,14 @@ ${X_HELP}
|
|
|
321
377
|
priced from live Jupiter quotes. LIVE (L on a ticket, /wallet link first): a separate signer process
|
|
322
378
|
quotes, builds, decodes, simulates and enforces the caps; Phantom signs in your browser; dexterm never
|
|
323
379
|
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
|
|
380
|
+
RPC: DEXTERM_RPC_URL, else ~/.dexterm/helius.key (Helius, metered with a credit meter), else the public RPC.
|
|
381
|
+
Updates: at most once a day a live run asks npm + GitHub for the newest version (two anonymous GETs, no telemetry)
|
|
382
|
+
and shows one toast if there is one; DEXTERM_NO_UPDATE_CHECK=1 or 'dexterm update --off' turns that off.`;
|
|
325
383
|
export function parseArgs(argv) {
|
|
326
384
|
const a = {
|
|
327
385
|
screen: null, fixtures: false, replay: null, record: null, filter: null, sort: null, preset: null, maxRequests: 0, maxCredits: 0, db: null,
|
|
328
386
|
mint: null, interval: '1h', listen: 20, verbose: false,
|
|
329
|
-
width: null, height: null, html: null, theme: DEFAULT_THEME, selected: 0, seed: 7,
|
|
387
|
+
width: null, height: null, html: null, export: null, theme: DEFAULT_THEME, selected: 0, seed: 7,
|
|
330
388
|
noAi: false, title: null, plain: false, ansi: false, color: null, tz: null,
|
|
331
389
|
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
390
|
scan: false, scanConfig: {},
|
|
@@ -375,10 +433,13 @@ export function parseArgs(argv) {
|
|
|
375
433
|
else if (k === '--max-credits')
|
|
376
434
|
a.maxCredits = num();
|
|
377
435
|
else if (k === '--mint') {
|
|
436
|
+
// task-1401: a bare mint, or a dexscreener / pump.fun / solscan / birdeye link (a dexscreener pool is resolved to its coin at open)
|
|
378
437
|
const m = str();
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
438
|
+
const t = parseCoinTarget(m);
|
|
439
|
+
if (!t || (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(m.trim()) && t.via === 'address'))
|
|
440
|
+
throw new Error(`--mint expects a base58 Solana address or a coin link, got ${JSON.stringify(m.slice(0, 20))}`);
|
|
441
|
+
a.mint = t.address;
|
|
442
|
+
a.mintVia = t.via;
|
|
382
443
|
}
|
|
383
444
|
else if (k === '--interval') {
|
|
384
445
|
const iv = str();
|
|
@@ -398,6 +459,8 @@ export function parseArgs(argv) {
|
|
|
398
459
|
a.height = num();
|
|
399
460
|
else if (k === '--html')
|
|
400
461
|
a.html = str();
|
|
462
|
+
else if (k === '--export')
|
|
463
|
+
a.export = argv[i + 1] && !argv[i + 1].startsWith('--') ? str() : '';
|
|
401
464
|
else if (k === '--theme')
|
|
402
465
|
a.theme = str();
|
|
403
466
|
else if (k === '--selected')
|
|
@@ -694,11 +757,11 @@ export function fastLaneEndpoints(rpc) {
|
|
|
694
757
|
return out;
|
|
695
758
|
}
|
|
696
759
|
export async function main(argv, io = process, proc = process) {
|
|
697
|
-
// `dexterm mcp` → the
|
|
760
|
+
// `dexterm mcp` → the read-only MCP server on stdio (SPEC §5.1; `claude mcp add dexterm -- dexterm mcp`),
|
|
761
|
+
// plus `dexterm mcp install <client>` (one-command setup) and `dexterm mcp doctor` (verify the connection).
|
|
698
762
|
if (argv[0] === 'mcp') {
|
|
699
|
-
const {
|
|
700
|
-
|
|
701
|
-
return 0;
|
|
763
|
+
const { runMcp } = await import("./mcp-cmd.js");
|
|
764
|
+
return runMcp(argv.slice(1), io);
|
|
702
765
|
}
|
|
703
766
|
// `dexterm agent <cmd>` → bring-your-own-agent registry + loopback bus (task-1345): add/list/rotate/
|
|
704
767
|
// remove/tier/pause/resume/kick/audit/packs, `serve` (a standalone bus on fixtures) and `connect`.
|
|
@@ -706,6 +769,12 @@ export async function main(argv, io = process, proc = process) {
|
|
|
706
769
|
const { runAgent } = await import("./agent-cmd.js");
|
|
707
770
|
return runAgent(argv.slice(1), io);
|
|
708
771
|
}
|
|
772
|
+
// `dexterm spectate --replay FILE | --info` → watch an agent drive dexterm (task-1406, "watch the agent
|
|
773
|
+
// cook"): replay a saved session, or show the live read-only, token-gated, loopback spectator stream.
|
|
774
|
+
if (argv[0] === 'spectate') {
|
|
775
|
+
const { runSpectate } = await import("./spectate-cmd.js");
|
|
776
|
+
return runSpectate(argv.slice(1), io);
|
|
777
|
+
}
|
|
709
778
|
// `dexterm recorder <cmd>` → the history recorder's tables: stats, label, prune, export (task-1329). No network.
|
|
710
779
|
if (argv[0] === 'recorder') {
|
|
711
780
|
const { recorderCli } = await import('../../recorder/src/index.js');
|
|
@@ -741,6 +810,12 @@ export async function main(argv, io = process, proc = process) {
|
|
|
741
810
|
const { kbSkillCommand } = await import("./kb-skill-cmd.js");
|
|
742
811
|
return kbSkillCommand(argv[0], argv.slice(1), { stdout: io.stdout, stderr: io.stderr, env: io.env, isTTY: io.stdout.isTTY === true });
|
|
743
812
|
}
|
|
813
|
+
// `dexterm help [topic]` → the app-help KB (task-1413): how the app works, with the exact key/command.
|
|
814
|
+
// Backed by the same generated entries the analyst's `app_help` tool uses. No network, no model.
|
|
815
|
+
if (argv[0] === 'help') {
|
|
816
|
+
const { helpCommand } = await import("./help-cmd.js");
|
|
817
|
+
return helpCommand(argv.slice(1), { stdout: io.stdout, stderr: io.stderr });
|
|
818
|
+
}
|
|
744
819
|
// `dexterm theme list|show|init|check` → user themes under ~/.dexterm/themes (task-1358 Tier 2). No network.
|
|
745
820
|
if (argv[0] === 'theme') {
|
|
746
821
|
const { themeCommand } = await import("./theme-cmd.js");
|
|
@@ -761,6 +836,25 @@ export async function main(argv, io = process, proc = process) {
|
|
|
761
836
|
const { rulesCommand } = await import("./rules-cmd.js");
|
|
762
837
|
return rulesCommand(argv.slice(1), { stdout: io.stdout, stderr: io.stderr, env: io.env });
|
|
763
838
|
}
|
|
839
|
+
// `dexterm alerts status|enable|disable|levels|proposals|set-telegram|set-discord|test` → where rule alerts
|
|
840
|
+
// are delivered (task-1398). Credentials live in ~/.dexterm/*.json (0600), never config.json/argv/logs.
|
|
841
|
+
if (argv[0] === 'alerts') {
|
|
842
|
+
const { alertsCommand } = await import("./alerts-cmd.js");
|
|
843
|
+
return alertsCommand(argv.slice(1), { stdout: io.stdout, stderr: io.stderr, env: io.env, stdin: io.stdin });
|
|
844
|
+
}
|
|
845
|
+
// `dexterm watch [--interval S] [--once|--ticks N] [--replay FILE]` → run the rules engine headless and
|
|
846
|
+
// PUSH alerts to the enabled channels so they reach you when the terminal is closed (task-1398). No signer.
|
|
847
|
+
if (argv[0] === 'watch') {
|
|
848
|
+
const { watchCommand } = await import("./watch-cmd.js");
|
|
849
|
+
return watchCommand(argv.slice(1), { stdout: io.stdout, stderr: io.stderr, env: io.env });
|
|
850
|
+
}
|
|
851
|
+
// `dexterm record [run|install|uninstall|status]` → the headless collector that keeps the history recorder
|
|
852
|
+
// fed with no TUI open (task-1414), to be driven by a user-level launchd LaunchAgent. No signer, no trading;
|
|
853
|
+
// requests honour the data layer limits / health gate / sweep budgets, and the Helius key is never printed.
|
|
854
|
+
if (argv[0] === 'record') {
|
|
855
|
+
const { recordCommand } = await import("./record-cmd.js");
|
|
856
|
+
return recordCommand(argv.slice(1), { stdout: io.stdout, stderr: io.stderr, env: io.env });
|
|
857
|
+
}
|
|
764
858
|
// `dexterm pack list|verify|add|remove|export|lock|sign|trust|signers|keymap|widget` → shareable packs (task-1358 Tier 4):
|
|
765
859
|
// plain files + sha256 lock + optional ssh signature. No network, no model; a pack never runs code (widgets land
|
|
766
860
|
// disabled, unsigned rules load disabled, a keymap merges only on the explicit command).
|
|
@@ -789,6 +883,18 @@ export async function main(argv, io = process, proc = process) {
|
|
|
789
883
|
const { runFollow } = await import("./follow-cmd.js");
|
|
790
884
|
return runFollow(argv.slice(1), io);
|
|
791
885
|
}
|
|
886
|
+
// `dexterm update [--off|--on]` → is there a newer version, and the exact upgrade command for this install (task-1397).
|
|
887
|
+
// Two anonymous GETs (npm registry, GitHub releases); never upgrades anything itself.
|
|
888
|
+
if (argv[0] === 'update') {
|
|
889
|
+
const { updateCommand } = await import("./update.js");
|
|
890
|
+
return updateCommand(argv.slice(1), io);
|
|
891
|
+
}
|
|
892
|
+
// `dexterm voice [status|download <model>]` → inspect the voice engines and download the optional
|
|
893
|
+
// voice models (whisper base.en, Piper voice). Downloads ask first and are checksum-verified (task-1408).
|
|
894
|
+
if (argv[0] === 'voice') {
|
|
895
|
+
const { voiceCommand } = await import("./voice-cmd.js");
|
|
896
|
+
return voiceCommand(argv.slice(1), { stdin: io.stdin, stdout: io.stdout, stderr: io.stderr, env: io.env });
|
|
897
|
+
}
|
|
792
898
|
// `dexterm doctor` → a pre-flight that says whether dexterm will run here and how to fix what won't (task-1305).
|
|
793
899
|
if (argv[0] === 'doctor') {
|
|
794
900
|
const { defaultDeps, runDoctor, renderDoctor } = await import("./doctor.js");
|
|
@@ -838,7 +944,7 @@ export async function main(argv, io = process, proc = process) {
|
|
|
838
944
|
return 0;
|
|
839
945
|
}
|
|
840
946
|
if (args.version) {
|
|
841
|
-
io.stdout.write(
|
|
947
|
+
io.stdout.write(`${VERSION}\n`);
|
|
842
948
|
return 0;
|
|
843
949
|
}
|
|
844
950
|
if (args.unknown.length) {
|
|
@@ -932,6 +1038,8 @@ export async function main(argv, io = process, proc = process) {
|
|
|
932
1038
|
feed.extraMints = () => [...liveCtx.newpairs.rows.keys()];
|
|
933
1039
|
applyScreenerArgs(state, args);
|
|
934
1040
|
if (args.mint) {
|
|
1041
|
+
if (args.mintVia === 'dexscreener')
|
|
1042
|
+
args.mint = (await resolveCoinTarget({ address: args.mint, via: 'dexscreener', rest: '' }, ctx.dl)).mint;
|
|
935
1043
|
ctx.openToken({ mint: args.mint, symbol: args.mint.slice(0, 4) + '…', name: '', launchpad: 'unknown' });
|
|
936
1044
|
if (args.screen === null)
|
|
937
1045
|
state.screen = 'token';
|
|
@@ -1063,7 +1171,13 @@ export async function main(argv, io = process, proc = process) {
|
|
|
1063
1171
|
}
|
|
1064
1172
|
const c = renderFrame(state, w, h);
|
|
1065
1173
|
ctx?.newpairs.markDrawn();
|
|
1066
|
-
if (args.
|
|
1174
|
+
if (args.export !== null) {
|
|
1175
|
+
// task-1401: `--export [DIR]` writes the same PNG + text report the `E` key does (default ~/.dexterm/exports).
|
|
1176
|
+
const now = Date.now();
|
|
1177
|
+
const r = writeExport(exportViewFor(state, w, h, now), { now, env: io.env, dir: args.export || undefined });
|
|
1178
|
+
io.stderr.write(`wrote ${r.png} (${r.bytes} bytes) and ${r.txt}\n`);
|
|
1179
|
+
}
|
|
1180
|
+
else if (args.html) {
|
|
1067
1181
|
// task-1326: `--html … --color 256` renders through the xterm palette (a Terminal.app-faithful proof)
|
|
1068
1182
|
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
1183
|
io.stderr.write(`wrote ${args.html} (${w}×${h})\n`);
|
|
@@ -1087,7 +1201,20 @@ export async function main(argv, io = process, proc = process) {
|
|
|
1087
1201
|
feed?.close();
|
|
1088
1202
|
return 0;
|
|
1089
1203
|
}
|
|
1090
|
-
|
|
1204
|
+
// task-1397: live interactive runs only: the cached "new version" notice (no network) plus, at most once a day,
|
|
1205
|
+
// a background refresh that never blocks the start. Off with DEXTERM_NO_UPDATE_CHECK=1 / `dexterm update --off`.
|
|
1206
|
+
const update = live ? (await import("./update.js")).startupCheck(io.env) : null;
|
|
1207
|
+
if (update) {
|
|
1208
|
+
state.updateNotice = update.notice;
|
|
1209
|
+
void update.refresh?.then((n) => { if (n)
|
|
1210
|
+
state.updateNotice = n; });
|
|
1211
|
+
}
|
|
1212
|
+
try {
|
|
1213
|
+
return await interactive(state, io, depth, args, proc);
|
|
1214
|
+
}
|
|
1215
|
+
finally {
|
|
1216
|
+
update?.cancel();
|
|
1217
|
+
}
|
|
1091
1218
|
}
|
|
1092
1219
|
const SCREEN_ORDER = ['screener', 'newpairs', 'watchlist', 'analyst'];
|
|
1093
1220
|
/** Rows PgUp/PgDn jump through the list (task-1303). */
|
|
@@ -1164,6 +1291,10 @@ function followsMove(state, delta) {
|
|
|
1164
1291
|
return false;
|
|
1165
1292
|
if (state.copyUi.input !== null || state.copyUi.mirror || state.copyUi.autoModal)
|
|
1166
1293
|
return true;
|
|
1294
|
+
if (state.copyUi.detail) {
|
|
1295
|
+
state.copyUi.detail.scroll = Math.max(0, state.copyUi.detail.scroll + (delta > 0 ? 1 : -1));
|
|
1296
|
+
return true;
|
|
1297
|
+
} // task-1399: j/k scroll the detail view
|
|
1167
1298
|
const n = state.copy?.follows().length ?? 0;
|
|
1168
1299
|
if (n <= 0) {
|
|
1169
1300
|
state.copyUi.selected = 0;
|
|
@@ -1449,6 +1580,10 @@ export function applyAction(state, action, stack) {
|
|
|
1449
1580
|
state.copyUi.notice = 'mirror preview closed';
|
|
1450
1581
|
break;
|
|
1451
1582
|
} // task-1360: esc closes the mirror preview first
|
|
1583
|
+
if (state.screen === 'follows' && state.copyUi?.detail) {
|
|
1584
|
+
state.copyUi.detail = null;
|
|
1585
|
+
break;
|
|
1586
|
+
} // task-1399: esc closes the wallet detail view before leaving the screen
|
|
1452
1587
|
const prev = stack.pop();
|
|
1453
1588
|
if (prev)
|
|
1454
1589
|
state.screen = prev;
|
|
@@ -2269,6 +2404,14 @@ export function applyAction(state, action, stack) {
|
|
|
2269
2404
|
case 'goto.open':
|
|
2270
2405
|
state.goto = { input: '', notice: null };
|
|
2271
2406
|
break;
|
|
2407
|
+
// task-1401 EXPORT: the loop renders the current view calm and writes the PNG + report (local files only).
|
|
2408
|
+
case 'export.view':
|
|
2409
|
+
state.pendingExport = true;
|
|
2410
|
+
break;
|
|
2411
|
+
// task-1407 PNL CARD: the loop opens the preview (recorded fills only) and runs the share keys.
|
|
2412
|
+
case 'pnl.card':
|
|
2413
|
+
state.pendingPnl = true;
|
|
2414
|
+
break;
|
|
2272
2415
|
case 'display.priceMode':
|
|
2273
2416
|
state.pendingToast = togglePriceMode(state);
|
|
2274
2417
|
break; // task-1366: display only, persisted
|
|
@@ -2518,31 +2661,81 @@ function seedForMint(state, live, mint, symbol) {
|
|
|
2518
2661
|
// task-1388 GO-TO prompt: the longest input we keep (a bare base58 mint is ≤ 44; the longest coin URL we
|
|
2519
2662
|
// accept is well under this), so a huge paste can never balloon the buffer.
|
|
2520
2663
|
const GOTO_INPUT_MAX = 120;
|
|
2521
|
-
|
|
2522
|
-
|
|
2664
|
+
// task-1388's parser now lives in coin-target.ts (shared with the analyst chat, the filter line and --mint).
|
|
2665
|
+
export { parseMintTarget };
|
|
2523
2666
|
/**
|
|
2524
|
-
* task-
|
|
2525
|
-
*
|
|
2526
|
-
*
|
|
2527
|
-
*
|
|
2528
|
-
*
|
|
2667
|
+
* task-1401: open a pasted coin target (a CA, or a dexscreener / pump.fun / solscan / birdeye link) on the LIVE token
|
|
2668
|
+
* screen — the same read-only path `--screen token --mint` and the analyst's open_token use. A mint opens at once; a
|
|
2669
|
+
* dexscreener link is first resolved to its coin (its address is usually the pool) and opens when that answers.
|
|
2670
|
+
* `onOpened` runs after the open either way (the chat uses it to ask its question about the coin). Returns a notice
|
|
2671
|
+
* when it cannot open (fixtures mode), else null. Never trades, never signs.
|
|
2529
2672
|
*/
|
|
2530
|
-
export function
|
|
2531
|
-
const
|
|
2532
|
-
if (!
|
|
2673
|
+
export function openCoinTarget(state, stack, t, onOpened) {
|
|
2674
|
+
const live = state.live && state.screener.source !== 'fixtures' ? state.live : null;
|
|
2675
|
+
if (!live)
|
|
2676
|
+
return 'opening a coin by address needs live mode (not --fixtures)';
|
|
2677
|
+
const open = (mint) => {
|
|
2678
|
+
if (!(state.screen === 'token' && live.token?.view.mint === mint))
|
|
2679
|
+
live.openToken(seedForMint(state, live, mint)); // already open: keep its loaded view
|
|
2680
|
+
if (state.screen !== 'token') {
|
|
2681
|
+
stack.push(state.screen);
|
|
2682
|
+
state.screen = 'token';
|
|
2683
|
+
}
|
|
2684
|
+
onOpened?.(mint);
|
|
2685
|
+
};
|
|
2686
|
+
if (t.via !== 'dexscreener') {
|
|
2687
|
+
open(t.address);
|
|
2533
2688
|
return null;
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
if (
|
|
2538
|
-
return
|
|
2689
|
+
}
|
|
2690
|
+
live.notice = 'resolving the dexscreener link…';
|
|
2691
|
+
void resolveCoinTarget(t, live.dl).then(({ mint, fromPair }) => {
|
|
2692
|
+
if (live.isClosed)
|
|
2693
|
+
return;
|
|
2694
|
+
live.notice = fromPair ? `dexscreener pool → coin ${mint.slice(0, 4)}…${mint.slice(-4)}` : null;
|
|
2695
|
+
open(mint);
|
|
2696
|
+
state.aiFocused = false;
|
|
2697
|
+
syncAnalyst(state);
|
|
2698
|
+
live.onChange('goto');
|
|
2699
|
+
});
|
|
2539
2700
|
return null;
|
|
2540
2701
|
}
|
|
2702
|
+
/**
|
|
2703
|
+
* task-1401: the analyst chat's ⏎. When the typed text names a coin (a CA or a coin link), open that coin's token
|
|
2704
|
+
* view and, if a question came with it ("is <CA> a rug?"), ask it about THAT coin; returns true when it handled
|
|
2705
|
+
* the input. Plain questions return false and go to the analyst as before.
|
|
2706
|
+
*/
|
|
2707
|
+
export function chatOpenCoin(state, stack, q, hooks) {
|
|
2708
|
+
const ct = q ? parseCoinTarget(q) : null;
|
|
2709
|
+
if (!ct)
|
|
2710
|
+
return false;
|
|
2711
|
+
state.aiInput = '';
|
|
2712
|
+
const question = isBareOpen(ct.rest) ? '' : ct.rest;
|
|
2713
|
+
const notice = openCoinTarget(state, stack, ct, (mint) => {
|
|
2714
|
+
syncAnalyst(state);
|
|
2715
|
+
if (question) {
|
|
2716
|
+
state.aiOpen = true;
|
|
2717
|
+
try {
|
|
2718
|
+
hooks.ask(question);
|
|
2719
|
+
}
|
|
2720
|
+
catch (e) {
|
|
2721
|
+
state.screener.notice = `⚠ ask failed: ${describeError(e)}`;
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
else {
|
|
2725
|
+
state.aiFocused = false;
|
|
2726
|
+
hooks.say(`opened ${mint.slice(0, 4)}…${mint.slice(-4)} from the chat`, 'info', 4);
|
|
2727
|
+
}
|
|
2728
|
+
});
|
|
2729
|
+
if (notice)
|
|
2730
|
+
hooks.say(notice, 'warn', 5);
|
|
2731
|
+
return true;
|
|
2732
|
+
}
|
|
2541
2733
|
/**
|
|
2542
2734
|
* task-1388: resolve the GO-TO prompt's input to a coin and open its live token screen (read-only — the same
|
|
2543
2735
|
* path `--screen token --mint` and the analyst's open_token use: live.openToken(seedForMint(...)), which seeds
|
|
2544
2736
|
* an unknown-launchpad stub when the mint is not already on a screen). Bad input keeps the prompt open with a
|
|
2545
2737
|
* one-line notice and changes nothing; a good open closes the prompt and pushes the current screen on the stack.
|
|
2738
|
+
* task-1401: a dexscreener link is resolved pool → coin first (openCoinTarget).
|
|
2546
2739
|
*/
|
|
2547
2740
|
function openGotoTarget(state, stack) {
|
|
2548
2741
|
const g = state.goto;
|
|
@@ -2558,17 +2751,13 @@ function openGotoTarget(state, stack) {
|
|
|
2558
2751
|
g.notice = 'not a contract address or a dexscreener / pump.fun / solscan link';
|
|
2559
2752
|
return;
|
|
2560
2753
|
}
|
|
2561
|
-
const
|
|
2562
|
-
|
|
2563
|
-
|
|
2754
|
+
const t = parseCoinTarget(raw) ?? { address: mint, via: 'address', rest: '' };
|
|
2755
|
+
const notice = openCoinTarget(state, stack, t);
|
|
2756
|
+
if (notice) {
|
|
2757
|
+
g.notice = notice;
|
|
2564
2758
|
return;
|
|
2565
2759
|
}
|
|
2566
|
-
live.openToken(seedForMint(state, live, mint));
|
|
2567
2760
|
state.goto = null;
|
|
2568
|
-
if (state.screen !== 'token') {
|
|
2569
|
-
stack.push(state.screen);
|
|
2570
|
-
state.screen = 'token';
|
|
2571
|
-
}
|
|
2572
2761
|
}
|
|
2573
2762
|
/**
|
|
2574
2763
|
* Apply one analyst-requested UI action to the state (task-1313). This is the trust boundary: every
|
|
@@ -2594,6 +2783,12 @@ export function applyAnalystAction(state, raw, stack) {
|
|
|
2594
2783
|
if (!res.ok)
|
|
2595
2784
|
return { ok: false, error: res.error };
|
|
2596
2785
|
const action = res.action;
|
|
2786
|
+
// task-1413 tier (b): a persisted-config action is never applied by the model — return it as a PROPOSAL
|
|
2787
|
+
// the user accepts with a keypress (applyAnalystConfig then journals it as actor 'analyst').
|
|
2788
|
+
if (ACTION_TIER[action.type] === 'config') {
|
|
2789
|
+
times.push(now);
|
|
2790
|
+
return { ok: true, label: res.label, proposal: { action, summary: res.label } };
|
|
2791
|
+
}
|
|
2597
2792
|
// Resolve a mint to a row object from what is on screen (allow-list guarantees it was visible).
|
|
2598
2793
|
const findSeed = (mint) => seedForMint(state, live, mint);
|
|
2599
2794
|
const toScreen = (next) => { if (state.screen !== next) {
|
|
@@ -2633,6 +2828,10 @@ export function applyAnalystAction(state, raw, stack) {
|
|
|
2633
2828
|
}
|
|
2634
2829
|
case 'show_in_screener': {
|
|
2635
2830
|
toScreen('screener');
|
|
2831
|
+
if (action.preset && !setPreset(sc, action.preset))
|
|
2832
|
+
return { ok: false, error: `preset "${action.preset}" is unavailable` };
|
|
2833
|
+
if (action.preset && state.feed)
|
|
2834
|
+
state.feed.setPreset(sc.preset);
|
|
2636
2835
|
if (action.filter) {
|
|
2637
2836
|
const err = setFilter(sc, action.filter);
|
|
2638
2837
|
if (err)
|
|
@@ -2646,12 +2845,101 @@ export function applyAnalystAction(state, raw, stack) {
|
|
|
2646
2845
|
state.selected = i;
|
|
2647
2846
|
}
|
|
2648
2847
|
state.selected = clampSelection(sc, state.selected);
|
|
2848
|
+
if (action.preset) {
|
|
2849
|
+
times.push(now);
|
|
2850
|
+
return { ok: true, label: res.label, refresh: true };
|
|
2851
|
+
}
|
|
2852
|
+
break;
|
|
2853
|
+
}
|
|
2854
|
+
// --- task-1413 view-only actions: applied immediately, shown as "analyst did X".
|
|
2855
|
+
case 'switch_screen': {
|
|
2856
|
+
toScreen(action.screen);
|
|
2857
|
+
state.selected = 0;
|
|
2858
|
+
break;
|
|
2859
|
+
}
|
|
2860
|
+
case 'switch_workspace': {
|
|
2861
|
+
const r = switchWorkspace(state, action.name, stack);
|
|
2862
|
+
if (!r.ok)
|
|
2863
|
+
return { ok: false, error: r.reason };
|
|
2864
|
+
times.push(now);
|
|
2865
|
+
return { ok: true, label: res.label, refresh: r.refresh };
|
|
2866
|
+
}
|
|
2867
|
+
case 'chart_popup': {
|
|
2868
|
+
// Open the chart popup (the loop performs the open/close); needs a selected coin to chart.
|
|
2869
|
+
state.pendingPopup = action.open ? 'chart' : null;
|
|
2870
|
+
break;
|
|
2871
|
+
}
|
|
2872
|
+
case 'toggle_calm': {
|
|
2873
|
+
if (action.on) {
|
|
2874
|
+
if (!isCalm(state.vfx)) {
|
|
2875
|
+
state.vfx.intensity = 'off';
|
|
2876
|
+
state.vfx.trail.clear();
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
else if (isCalm(state.vfx)) {
|
|
2880
|
+
const back = 'fever';
|
|
2881
|
+
state.vfx.intensity = back;
|
|
2882
|
+
state.vfx.baseline = 0.12;
|
|
2883
|
+
}
|
|
2884
|
+
break;
|
|
2885
|
+
}
|
|
2886
|
+
case 'set_scan': {
|
|
2887
|
+
scanCommand(state, action.on ? 'on' : 'off', stack);
|
|
2888
|
+
break;
|
|
2889
|
+
}
|
|
2890
|
+
case 'export_view': {
|
|
2891
|
+
state.pendingExport = true;
|
|
2649
2892
|
break;
|
|
2650
2893
|
}
|
|
2651
2894
|
}
|
|
2652
2895
|
times.push(now);
|
|
2653
2896
|
return { ok: true, label: res.label };
|
|
2654
2897
|
}
|
|
2898
|
+
/**
|
|
2899
|
+
* task-1413: apply an accepted tier-(b) config proposal through the journaled path as actor `analyst`
|
|
2900
|
+
* (so `ctrl+z` undoes it), creating the parent section when absent (exactly like the `$` / `,` handlers),
|
|
2901
|
+
* then reflect it in the live UI where cheap. Throws on a refusal (schema / policy); the caller toasts it.
|
|
2902
|
+
* Returns the one-line label for the confirmation toast.
|
|
2903
|
+
*/
|
|
2904
|
+
export function applyAnalystConfig(state, proposal) {
|
|
2905
|
+
const ui = state.custom;
|
|
2906
|
+
if (!ui)
|
|
2907
|
+
throw new Error('config changes need a loaded config (not available on this run)');
|
|
2908
|
+
const change = configChangeForAction(proposal.action);
|
|
2909
|
+
if (!change)
|
|
2910
|
+
throw new Error('not a persisted-config action');
|
|
2911
|
+
const parent = ui.config[change.section];
|
|
2912
|
+
const op = parent && typeof parent === 'object'
|
|
2913
|
+
? { op: 'add', path: change.path, value: change.value }
|
|
2914
|
+
: { op: 'add', path: `/${change.section}`, value: { [change.key]: change.value } };
|
|
2915
|
+
// THE GATE (task-1413 r2): re-run the model-actor policy on the exact journal op BEFORE writing. The
|
|
2916
|
+
// analyst is not the user, so `DENIED_FOR_MODELS` applies — a config action can never reach a signer /
|
|
2917
|
+
// trading / keys / caps / mandates / wallet / alerts / VOICE path, even behind the keypress confirm.
|
|
2918
|
+
// CONFIG_ACTION_PATHS is already a closed non-denied allow-list; this is the server-side belt-and-braces.
|
|
2919
|
+
const denied = policyCheck([op], 'claude');
|
|
2920
|
+
if (denied.length)
|
|
2921
|
+
throw new Error(`refused: ${denied.map((e) => e.msg).join('; ')}`);
|
|
2922
|
+
const { config } = applyAndJournal(ui.home, [op], { actor: 'analyst', why: `analyst: ${proposal.summary}`, now: state.now.getTime() });
|
|
2923
|
+
ui.config = config;
|
|
2924
|
+
// Reflect the change in the running UI where it is cheap and local.
|
|
2925
|
+
switch (proposal.action.type) {
|
|
2926
|
+
case 'set_theme': {
|
|
2927
|
+
const t = themeByName(ui, proposal.action.name);
|
|
2928
|
+
if (t)
|
|
2929
|
+
state.theme = t;
|
|
2930
|
+
break;
|
|
2931
|
+
}
|
|
2932
|
+
case 'set_price_mode':
|
|
2933
|
+
state.priceMode = proposal.action.mode;
|
|
2934
|
+
break;
|
|
2935
|
+
case 'set_intensity':
|
|
2936
|
+
state.vfx.intensity = proposal.action.intensity;
|
|
2937
|
+
state.vfx.baseline = proposal.action.intensity === 'fever' ? 0.12 : 0;
|
|
2938
|
+
break;
|
|
2939
|
+
default: break;
|
|
2940
|
+
}
|
|
2941
|
+
return proposal.summary;
|
|
2942
|
+
}
|
|
2655
2943
|
/**
|
|
2656
2944
|
* Route one decoded key: the screener's filter editor and sort menu swallow keys while open,
|
|
2657
2945
|
* everything else goes through the keymap. Exported for tests.
|
|
@@ -2879,6 +3167,22 @@ export function applyKey(state, key, stack) {
|
|
|
2879
3167
|
ui.input = cur + key.ch;
|
|
2880
3168
|
return;
|
|
2881
3169
|
}
|
|
3170
|
+
// task-1399: on the follows screen (not typing, no modal), enter opens/closes the selected wallet's DETAIL view.
|
|
3171
|
+
if (state.screen === 'follows' && state.copyUi && state.copyUi.input === null && !state.copyUi.mirror && !state.copyUi.autoModal && key.name === 'enter') {
|
|
3172
|
+
const ui = state.copyUi;
|
|
3173
|
+
if (ui.detail) {
|
|
3174
|
+
ui.detail = null;
|
|
3175
|
+
return;
|
|
3176
|
+
}
|
|
3177
|
+
const svc = state.copy;
|
|
3178
|
+
const rows = svc?.follows() ?? [];
|
|
3179
|
+
const f = rows[Math.max(0, Math.min(rows.length - 1, ui.selected))];
|
|
3180
|
+
if (f)
|
|
3181
|
+
ui.detail = { wallet: f.addr, scroll: 0 };
|
|
3182
|
+
else
|
|
3183
|
+
ui.notice = 'follow a wallet first (i)';
|
|
3184
|
+
return;
|
|
3185
|
+
}
|
|
2882
3186
|
if (state.screen === 'kol' && state.kolInput !== null && state.kolInput !== undefined) {
|
|
2883
3187
|
const kol = kolOf(state);
|
|
2884
3188
|
if (key.name === 'esc') {
|
|
@@ -2918,6 +3222,18 @@ export function applyKey(state, key, stack) {
|
|
|
2918
3222
|
state.pendingToast = typeof cmd === 'object' ? cmd.error : scanCommand(state, cmd, stack);
|
|
2919
3223
|
return;
|
|
2920
3224
|
}
|
|
3225
|
+
// task-1401: a pasted CA or coin link on the `/` line opens that coin (no filter field is a 32-44 char base58
|
|
3226
|
+
// run); the editor closes and the previous filter stays applied.
|
|
3227
|
+
const ct = parseCoinTarget(sc.edit.text);
|
|
3228
|
+
if (ct && isBareOpen(ct.rest)) {
|
|
3229
|
+
sc.mode = 'table';
|
|
3230
|
+
sc.edit.text = sc.query ?? '';
|
|
3231
|
+
sc.edit.error = null;
|
|
3232
|
+
const notice = openCoinTarget(state, stack, ct);
|
|
3233
|
+
if (notice)
|
|
3234
|
+
state.pendingToast = notice;
|
|
3235
|
+
return;
|
|
3236
|
+
}
|
|
2921
3237
|
// task-1328: `/wallet …` is a command too (the loop performs it: link opens a browser, probe opens a ticket).
|
|
2922
3238
|
const wc = parseWalletCommand(sc.edit.text);
|
|
2923
3239
|
if (wc) {
|
|
@@ -3497,6 +3813,10 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
3497
3813
|
// Live bindings redraw as soon as something lands (one frame per macrotask at most).
|
|
3498
3814
|
let drawPending = false;
|
|
3499
3815
|
let lastFastDraw = 0;
|
|
3816
|
+
// task-1399: a followed wallet's buy/sell also PUSHES through the alerts engine (desktop / Telegram / Discord)
|
|
3817
|
+
// when the user enabled a push channel. Forward-declared; the real impl is set once the Notifier exists below.
|
|
3818
|
+
// Read-only, like a rule alert — nothing here trades, arms or signs.
|
|
3819
|
+
let pushCopyAlert = () => { };
|
|
3500
3820
|
if (live) {
|
|
3501
3821
|
// task-1360: poll the followed wallets for the whole run (live only; replay never spends credits) and toast
|
|
3502
3822
|
// every new buy/sell a followed wallet made. Information only: no path from an alert to a trade exists here.
|
|
@@ -3509,6 +3829,8 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
3509
3829
|
const a = as[as.length - 1];
|
|
3510
3830
|
const who = a.name ?? shortAddr(a.wallet);
|
|
3511
3831
|
say(`${who} ${a.trade.side === 'buy' ? 'BOUGHT' : 'SOLD'} ${shortAddr(a.trade.mint)}${a.trade.usd !== null ? ` ≈ $${a.trade.usd.toFixed(0)}` : ''}${a.lagMs !== null ? ` · lag ${Math.round(a.lagMs / 1000)}s` : ''}${as.length > 1 ? ` (+${as.length - 1} more · U)` : ' (U)'}`, a.trade.side === 'buy' ? 'up' : 'down', 6);
|
|
3832
|
+
for (const ca of as)
|
|
3833
|
+
pushCopyAlert(ca); // task-1399: push each one out through the alerts engine too
|
|
3512
3834
|
}
|
|
3513
3835
|
}
|
|
3514
3836
|
if (done || drawPending)
|
|
@@ -3629,6 +3951,15 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
3629
3951
|
const r = applyAnalystAction(state, raw, stack);
|
|
3630
3952
|
if (!r.ok || !r.label)
|
|
3631
3953
|
return;
|
|
3954
|
+
// task-1413 tier (b): a persisted-config request is a PROPOSAL — show it and wait for the user's key.
|
|
3955
|
+
if (r.proposal) {
|
|
3956
|
+
// The proposal expires with its 8s toast (task-1413 defect c): a 'y' typed after it fades never
|
|
3957
|
+
// persists a stale change. `now` is the frame clock so it stays deterministic under replay.
|
|
3958
|
+
state.analystConfigProposal = { ...r.proposal, expiresAt: state.now.getTime() + 8000 };
|
|
3959
|
+
say(`analyst suggests: ${r.proposal.summary} — press y to accept, any other key to dismiss`, 'ai', 8);
|
|
3960
|
+
scheduleDraw();
|
|
3961
|
+
return;
|
|
3962
|
+
}
|
|
3632
3963
|
const log = (state.analystActions ??= []);
|
|
3633
3964
|
log.push(r.label);
|
|
3634
3965
|
if (log.length > 6)
|
|
@@ -4099,6 +4430,28 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
4099
4430
|
say(`skipped ${plan.skipped.map((s) => `${s.kind} (${s.why})`).join(', ')}`, 'info', 5);
|
|
4100
4431
|
scheduleDraw();
|
|
4101
4432
|
};
|
|
4433
|
+
// task-1398: PUSH delivery while the TUI is open. Built only when the user enabled a push channel
|
|
4434
|
+
// (desktop/telegram/discord); otherwise null and there is no behaviour change. The in-app toast is
|
|
4435
|
+
// still state.pendingToast, so the Notifier's toast sink is a no-op here (routing may still include it,
|
|
4436
|
+
// harmlessly). Delivery is fire-and-forget: a slow channel never blocks the frame.
|
|
4437
|
+
const alertNotifier = state.custom && hasPushChannel(state.custom.config.alerts)
|
|
4438
|
+
? buildNotifier(state.custom.home, state.custom.config.alerts, { toastSink: () => { } })
|
|
4439
|
+
: null;
|
|
4440
|
+
// task-1399: now the Notifier exists, a followed wallet's buy/sell can be pushed. Fire-and-forget; a slow
|
|
4441
|
+
// channel never blocks the loop; routing's `levels` filter lets the user scope (a buy is `up`, a sell `down`).
|
|
4442
|
+
if (alertNotifier)
|
|
4443
|
+
pushCopyAlert = (ca) => { void alertNotifier.deliver(copyAlertToDelivery(ca)).catch(() => { }); };
|
|
4444
|
+
const drainDeliveries = () => {
|
|
4445
|
+
const ui = state.rulesUi;
|
|
4446
|
+
if (!ui || !ui.pendingDeliveries.length)
|
|
4447
|
+
return;
|
|
4448
|
+
const ds = ui.pendingDeliveries;
|
|
4449
|
+
ui.pendingDeliveries = [];
|
|
4450
|
+
if (!alertNotifier)
|
|
4451
|
+
return;
|
|
4452
|
+
for (const d of ds)
|
|
4453
|
+
void alertNotifier.deliver(d).catch(() => { });
|
|
4454
|
+
};
|
|
4102
4455
|
// task-1358 Tier 3: perform what the rules engine / an accepted proposal queued. A ticket proposal opens the
|
|
4103
4456
|
// PAPER ticket in its editing phase (the user sizes + confirms there; signer caps apply) — never a live ticket,
|
|
4104
4457
|
// never a confirm. An ask proposal is a question to the analyst. Popups are the same read-only windows the keys
|
|
@@ -4258,6 +4611,47 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
4258
4611
|
return { price: np.price ?? null, mcap: np.mcap ?? null };
|
|
4259
4612
|
return { price: null, mcap: null };
|
|
4260
4613
|
};
|
|
4614
|
+
// task-1411: a short instruction appended to a VOICE analyst question so the answer is built for the
|
|
4615
|
+
// ear, not the screen — market cap not a per-token price, round numbers, short.
|
|
4616
|
+
const VOICE_ANALYST_SPEECH_HINT = 'Answer out loud for text-to-speech: talk about price as market cap, never a raw per-token price; '
|
|
4617
|
+
+ 'round big numbers like "340 thousand" or "1.2 million"; no long addresses; keep it to two or three short sentences.';
|
|
4618
|
+
// A spoken pronoun that points at the highlighted row ("this one", "this coin", "it").
|
|
4619
|
+
const VOICE_SELECTION_PRONOUN = /\b(?:this (?:one|coin|token)|that (?:one|coin|token)|this|it)\b/i;
|
|
4620
|
+
// A friendly, complete spoken ack for each navigable screen (the router's `say` is the terse key).
|
|
4621
|
+
const NAV_ACK = {
|
|
4622
|
+
screener: 'the screener', newpairs: 'new pairs', watchlist: 'your watchlist', analyst: 'the analyst', positions: 'your positions',
|
|
4623
|
+
};
|
|
4624
|
+
/**
|
|
4625
|
+
* Run a VOICE-originated analyst question (task-1411). Fixes the screener silence: "what do you think
|
|
4626
|
+
* of THIS ONE" aimed at a highlighted screener row used to start a context-free `trench-scan` with no
|
|
4627
|
+
* mint, so the analyst never learned which coin "this one" was and answered with a short, useless
|
|
4628
|
+
* fragment (the tester heard nothing useful). We now attach the SELECTED coin as a token-dossier target —
|
|
4629
|
+
* exactly the context a token screen already gives — so it answers for real. The question also carries
|
|
4630
|
+
* a short "answer for speech" instruction (market cap, round numbers, short).
|
|
4631
|
+
*/
|
|
4632
|
+
const askVoiceAnalyst = (question) => {
|
|
4633
|
+
const q = question.trim().slice(0, 400);
|
|
4634
|
+
const instructed = `${q}\n\n${VOICE_ANALYST_SPEECH_HINT}`;
|
|
4635
|
+
// A warm session: follow up on it so the model keeps context (the screen is already analyst/token).
|
|
4636
|
+
if (state.analyst) {
|
|
4637
|
+
state.analyst.ask(instructed, buildScreenState(state));
|
|
4638
|
+
return;
|
|
4639
|
+
}
|
|
4640
|
+
const tv = live?.token?.view;
|
|
4641
|
+
const sel = voiceSelected();
|
|
4642
|
+
if (state.screen === 'token' && live && tv?.mint) {
|
|
4643
|
+
state.pendingAnalyst = { skill: 'token-dossier', mint: tv.mint, symbol: tv.symbol, target: 'token', question: instructed };
|
|
4644
|
+
}
|
|
4645
|
+
else if (live && sel && VOICE_SELECTION_PRONOUN.test(q)) {
|
|
4646
|
+
// the screener/analyst/watchlist/new-pairs highlighted coin IS "this one" (task-1411)
|
|
4647
|
+
state.pendingAnalyst = { skill: 'token-dossier', mint: sel.mint, symbol: sel.symbol, target: 'analyst', question: instructed };
|
|
4648
|
+
}
|
|
4649
|
+
else {
|
|
4650
|
+
state.pendingAnalyst = { skill: 'trench-scan', target: 'analyst', question: instructed };
|
|
4651
|
+
}
|
|
4652
|
+
if (state.pendingAnalyst)
|
|
4653
|
+
beginAnalyst();
|
|
4654
|
+
};
|
|
4261
4655
|
/**
|
|
4262
4656
|
* Perform a voice decision. Returns `false` when the app spoke its own reply (an ask / clarify), so
|
|
4263
4657
|
* the controller does not read the decision's `say` line over it (task-1339, re-verify N1: the
|
|
@@ -4273,6 +4667,7 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
4273
4667
|
const a = SCREEN_ACTION[d.screen];
|
|
4274
4668
|
if (a)
|
|
4275
4669
|
applyAction(state, a, stack);
|
|
4670
|
+
ask(NAV_ACK[d.screen] ?? d.say);
|
|
4276
4671
|
break;
|
|
4277
4672
|
}
|
|
4278
4673
|
case 'open-token': {
|
|
@@ -4397,14 +4792,14 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
4397
4792
|
case 'analyst': {
|
|
4398
4793
|
// Route the question to the analyst, surface the chat, and let the answer be spoken when it lands.
|
|
4399
4794
|
// Capped like typed chat (400 chars): a runaway transcript never becomes a 200 KB argv (N7).
|
|
4795
|
+
// task-1411: askVoiceAnalyst attaches the highlighted coin as context so a screener "this one"
|
|
4796
|
+
// question is answered for real, and appends the "answer for speech" instruction.
|
|
4400
4797
|
if (state.screen !== 'token' && state.screen !== 'analyst') {
|
|
4401
4798
|
stack.push(state.screen);
|
|
4402
4799
|
state.screen = 'analyst';
|
|
4403
4800
|
}
|
|
4404
4801
|
syncAnalyst(state);
|
|
4405
|
-
|
|
4406
|
-
if (state.pendingAnalyst)
|
|
4407
|
-
beginAnalyst();
|
|
4802
|
+
askVoiceAnalyst(d.question);
|
|
4408
4803
|
break;
|
|
4409
4804
|
}
|
|
4410
4805
|
case 'workspace': {
|
|
@@ -4456,26 +4851,65 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
4456
4851
|
else
|
|
4457
4852
|
voice.abortTurn(session.status === 'error' ? "Sorry — the analyst hit an error. Try again." : "Okay, stopped.");
|
|
4458
4853
|
};
|
|
4854
|
+
// task-1393: persist the `,` voice choice to config.voice.name (actor 'user', journaled → undo works).
|
|
4855
|
+
// Best-effort: a persistence failure keeps the voice for this run and never breaks the UI.
|
|
4856
|
+
const persistVoiceChoice = (voiceName) => {
|
|
4857
|
+
const ui = state.custom;
|
|
4858
|
+
if (!ui)
|
|
4859
|
+
return;
|
|
4860
|
+
try {
|
|
4861
|
+
const op = ui.config.voice
|
|
4862
|
+
? { op: 'add', path: '/voice/name', value: voiceName }
|
|
4863
|
+
: { op: 'add', path: '/voice', value: { name: voiceName } };
|
|
4864
|
+
const { config } = applyAndJournal(ui.home, [op], { actor: 'user', why: `, voice ${voiceName}`, now: state.now.getTime() });
|
|
4865
|
+
ui.config = config;
|
|
4866
|
+
}
|
|
4867
|
+
catch { /* keep the voice for this run; nothing to surface */ }
|
|
4868
|
+
};
|
|
4869
|
+
// task-1411: persist the < / > speaking-rate nudge to config.voice.rate (actor 'user', journaled →
|
|
4870
|
+
// undo works). Best-effort: a failure keeps the rate for this run and never breaks the UI.
|
|
4871
|
+
const persistVoiceRate = (rate) => {
|
|
4872
|
+
const ui = state.custom;
|
|
4873
|
+
if (!ui)
|
|
4874
|
+
return;
|
|
4875
|
+
try {
|
|
4876
|
+
const op = ui.config.voice
|
|
4877
|
+
? { op: 'add', path: '/voice/rate', value: rate }
|
|
4878
|
+
: { op: 'add', path: '/voice', value: { rate } };
|
|
4879
|
+
const { config } = applyAndJournal(ui.home, [op], { actor: 'user', why: `voice rate ${rate}`, now: state.now.getTime() });
|
|
4880
|
+
ui.config = config;
|
|
4881
|
+
}
|
|
4882
|
+
catch { /* keep the rate for this run; nothing to surface */ }
|
|
4883
|
+
};
|
|
4459
4884
|
const initVoice = async () => {
|
|
4460
4885
|
if (voiceInitStarted)
|
|
4461
4886
|
return;
|
|
4462
4887
|
voiceInitStarted = true;
|
|
4463
4888
|
state.voice = { enabled: true, mode: 'idle', muted: false, voice: null, transcript: null, say: null, message: 'preparing voice…' };
|
|
4464
4889
|
scheduleDraw();
|
|
4890
|
+
// task-1393: the user's saved voice/rate (if any). A missing/banned name is resolved to a safe
|
|
4891
|
+
// deep British male by selectTts — it never falls back to the machine's system voice.
|
|
4892
|
+
const cfgVoice = state.custom?.config.voice;
|
|
4465
4893
|
let stt = null, tts = null;
|
|
4466
4894
|
try {
|
|
4467
|
-
[stt, tts] = await Promise.all([selectStt(), selectTts()]);
|
|
4895
|
+
[stt, tts] = await Promise.all([selectStt(), selectTts(undefined, cfgVoice?.name ?? null)]);
|
|
4468
4896
|
}
|
|
4469
4897
|
catch { /* both null → degrade to text */ }
|
|
4470
4898
|
if (done)
|
|
4471
4899
|
return;
|
|
4900
|
+
// Seed the controller with the ENGINE's resolved voice so its cycle/status start on the British male,
|
|
4901
|
+
// not on voices[0] (which could be a novelty voice like "Albert").
|
|
4902
|
+
const resolvedVoice = tts instanceof SayTts ? tts.voiceName : undefined;
|
|
4903
|
+
const style = voiceStyleDefaults(resolvedVoice); // the picked voice's own rate/pitch, unless the user saved theirs
|
|
4472
4904
|
voice = new VoiceController({
|
|
4473
|
-
stt, tts, getScreenState: voiceScreenState,
|
|
4905
|
+
stt, tts, voice: resolvedVoice, rate: cfgVoice?.rate ?? style?.rate, pitch: cfgVoice?.pitch ?? style?.pitch, getScreenState: voiceScreenState,
|
|
4474
4906
|
onState: () => { if (!done && voice) {
|
|
4475
4907
|
state.voice = toHud();
|
|
4476
4908
|
scheduleDraw();
|
|
4477
4909
|
} },
|
|
4478
4910
|
onDecision: handleVoiceDecision,
|
|
4911
|
+
onVoiceChange: persistVoiceChoice,
|
|
4912
|
+
onRateChange: persistVoiceRate,
|
|
4479
4913
|
});
|
|
4480
4914
|
voice.enable();
|
|
4481
4915
|
await voice.toggleTalk(); // the first `v` press both enables and starts listening
|
|
@@ -4539,6 +4973,35 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
4539
4973
|
quit();
|
|
4540
4974
|
return;
|
|
4541
4975
|
}
|
|
4976
|
+
// task-1413: a pending analyst config PROPOSAL owns the next key. `y` accepts and journals it as
|
|
4977
|
+
// actor 'analyst' (ctrl+z undoes); any other key dismisses it. A stray key never persists a change.
|
|
4978
|
+
// Two guards (defect c): the proposal does NOT steal a key while a TEXT INPUT is open — the GO-TO
|
|
4979
|
+
// prompt or a focused analyst chat line — so a 'y' being typed goes into the field, not the accept;
|
|
4980
|
+
// and an EXPIRED proposal (its 8s toast has faded) is silently dropped, never accepted by a late 'y'.
|
|
4981
|
+
if (state.analystConfigProposal) {
|
|
4982
|
+
const inputOpen = !!state.goto || (state.aiFocused === true);
|
|
4983
|
+
const expired = state.now.getTime() >= state.analystConfigProposal.expiresAt;
|
|
4984
|
+
if (expired) {
|
|
4985
|
+
state.analystConfigProposal = null; // let the key fall through to its normal handler below
|
|
4986
|
+
}
|
|
4987
|
+
else if (!inputOpen) {
|
|
4988
|
+
const prop = state.analystConfigProposal;
|
|
4989
|
+
state.analystConfigProposal = null;
|
|
4990
|
+
if (key.ch === 'y' || key.ch === 'Y') {
|
|
4991
|
+
try {
|
|
4992
|
+
say(`analyst ${applyAnalystConfig(state, prop)} · ctrl+z undoes`, 'ai', 5);
|
|
4993
|
+
}
|
|
4994
|
+
catch (e) {
|
|
4995
|
+
say(`change refused: ${describeError(e)}`, 'warn', 4);
|
|
4996
|
+
}
|
|
4997
|
+
}
|
|
4998
|
+
else {
|
|
4999
|
+
say('dismissed the analyst change', 'ai', 3);
|
|
5000
|
+
}
|
|
5001
|
+
draw();
|
|
5002
|
+
continue;
|
|
5003
|
+
}
|
|
5004
|
+
}
|
|
4542
5005
|
// task-1388 GO-TO prompt: while the "open any coin" input is open it owns the keyboard. Route the key
|
|
4543
5006
|
// straight to applyKey (which does the capture / open) and skip the voice, calm, demo and keymap
|
|
4544
5007
|
// handlers below — otherwise a base58 char like v / m / c would toggle voice or calm instead of typing.
|
|
@@ -4573,7 +5036,7 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
4573
5036
|
// `micEngaged` also covers the window while the mic is still OPENING (listen() in flight), so a key
|
|
4574
5037
|
// pressed before the engine came up closes the session the moment it arrives (task-1339, N3/N8).
|
|
4575
5038
|
if (voice?.micEngaged) {
|
|
4576
|
-
const isVoiceCtl = key.name === 'char' && !key.ctrl && !key.alt && (key.ch === 'v' || key.ch === 'm' || key.ch === ',');
|
|
5039
|
+
const isVoiceCtl = key.name === 'char' && !key.ctrl && !key.alt && (key.ch === 'v' || key.ch === 'm' || key.ch === ',' || key.ch === '<' || key.ch === '>');
|
|
4577
5040
|
if (key.name === 'esc') {
|
|
4578
5041
|
voice.cancelListen();
|
|
4579
5042
|
draw();
|
|
@@ -4615,6 +5078,16 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
4615
5078
|
draw();
|
|
4616
5079
|
continue;
|
|
4617
5080
|
}
|
|
5081
|
+
if (voice?.isEnabled && key.ch === '<') {
|
|
5082
|
+
voice.nudgeRate(-1);
|
|
5083
|
+
draw();
|
|
5084
|
+
continue;
|
|
5085
|
+
} // slower (task-1411)
|
|
5086
|
+
if (voice?.isEnabled && key.ch === '>') {
|
|
5087
|
+
voice.nudgeRate(1);
|
|
5088
|
+
draw();
|
|
5089
|
+
continue;
|
|
5090
|
+
} // faster (task-1411)
|
|
4618
5091
|
}
|
|
4619
5092
|
}
|
|
4620
5093
|
// A modal overlay (welcome/help) is open: any key dismisses it; `?` switches welcome→help (task-1305).
|
|
@@ -4633,6 +5106,32 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
4633
5106
|
draw();
|
|
4634
5107
|
continue;
|
|
4635
5108
|
}
|
|
5109
|
+
// task-1407: the PnL card preview owns the keys while open (share / save / toggles / other coin / esc).
|
|
5110
|
+
if (state.overlay === 'pnl' && state.pnl) {
|
|
5111
|
+
const ui = state.pnl;
|
|
5112
|
+
const eff = pnlKey(ui, key);
|
|
5113
|
+
if (eff.kind === 'close') {
|
|
5114
|
+
state.overlay = null;
|
|
5115
|
+
state.pnl = null;
|
|
5116
|
+
}
|
|
5117
|
+
else if (eff.kind === 'reload') {
|
|
5118
|
+
ui.stats = null;
|
|
5119
|
+
ui.error = null;
|
|
5120
|
+
void loadPnl(state).then(() => { if (!done)
|
|
5121
|
+
draw(); });
|
|
5122
|
+
}
|
|
5123
|
+
else if (eff.kind === 'share' && ui.stats) {
|
|
5124
|
+
ui.busy = true;
|
|
5125
|
+
ui.notice = null;
|
|
5126
|
+
void shareCard(ui.stats, { ...ui.opts }, eff.action, { platform: process.platform, env: io.env, run: runLocal, openUrl: (u) => openInBrowser(u), copyText: copyToClipboard })
|
|
5127
|
+
.then((r) => { ui.notice = r.message; say(r.message, 'info', 10); })
|
|
5128
|
+
.catch((e) => { ui.notice = `card failed: ${describeError(e)}`; })
|
|
5129
|
+
.finally(() => { ui.busy = false; if (!done)
|
|
5130
|
+
draw(); });
|
|
5131
|
+
}
|
|
5132
|
+
draw();
|
|
5133
|
+
continue;
|
|
5134
|
+
}
|
|
4636
5135
|
// task-1358: the WORKSPACES card — a digit picks a slot, j/k move, ⏎ activates, esc/q/ctrl+w w close.
|
|
4637
5136
|
if (state.overlay === 'workspaces' && state.custom) {
|
|
4638
5137
|
const ui = state.custom;
|
|
@@ -5016,6 +5515,21 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
5016
5515
|
draw();
|
|
5017
5516
|
continue;
|
|
5018
5517
|
}
|
|
5518
|
+
// task-1401: a CA or a dexscreener / pump.fun / solscan / birdeye link in the chat opens that coin's token
|
|
5519
|
+
// view (read-only, the go-to path); any question typed with it is then asked about THAT coin.
|
|
5520
|
+
const before = state.screen;
|
|
5521
|
+
if (chatOpenCoin(state, stack, q, { ask: sendChat, say })) {
|
|
5522
|
+
if (state.screen !== before) {
|
|
5523
|
+
try {
|
|
5524
|
+
startTransition();
|
|
5525
|
+
}
|
|
5526
|
+
catch {
|
|
5527
|
+
transition = null;
|
|
5528
|
+
}
|
|
5529
|
+
}
|
|
5530
|
+
draw();
|
|
5531
|
+
continue;
|
|
5532
|
+
}
|
|
5019
5533
|
if (q) {
|
|
5020
5534
|
state.aiInput = '';
|
|
5021
5535
|
try {
|
|
@@ -5254,6 +5768,29 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
5254
5768
|
copyToClipboard(m);
|
|
5255
5769
|
say(`copied CA ${m.slice(0, 4)}…${m.slice(-4)}`, 'info');
|
|
5256
5770
|
}
|
|
5771
|
+
if (state.pendingExport) {
|
|
5772
|
+
// task-1401: one key → PNG + text report of the current view in ~/.dexterm/exports. Nothing is uploaded.
|
|
5773
|
+
state.pendingExport = false;
|
|
5774
|
+
try {
|
|
5775
|
+
const [ew, eh] = size();
|
|
5776
|
+
const now = Date.now();
|
|
5777
|
+
const r = writeExport(exportViewFor(state, ew, eh, now), { now, env: io.env });
|
|
5778
|
+
say(`exported PNG + report → ${r.png.replace(homedir(), '~')} · local only, nothing uploaded`, 'info', 8);
|
|
5779
|
+
}
|
|
5780
|
+
catch (e) {
|
|
5781
|
+
say(`export failed: ${describeError(e)}`, 'warn', 6);
|
|
5782
|
+
}
|
|
5783
|
+
}
|
|
5784
|
+
if (state.pendingPnl) {
|
|
5785
|
+
// task-1407: open the PnL card preview for this screen (recorded fills only), then build the card.
|
|
5786
|
+
state.pendingPnl = false;
|
|
5787
|
+
const why = openPnl(state);
|
|
5788
|
+
if (why)
|
|
5789
|
+
say(why, 'warn', 6);
|
|
5790
|
+
else
|
|
5791
|
+
void loadPnl(state).then(() => { if (!done)
|
|
5792
|
+
draw(); });
|
|
5793
|
+
}
|
|
5257
5794
|
if (state.pendingToast) {
|
|
5258
5795
|
// task-1315: surface the link list / "no link" reply as a durable toast. Drained BEFORE
|
|
5259
5796
|
// pendingOpen so that on a successful open the "opened <url>" toast overwrites the "open <url>" one.
|
|
@@ -5317,6 +5854,11 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
5317
5854
|
endBoot();
|
|
5318
5855
|
}
|
|
5319
5856
|
if (v.frame % tickEvery === 0) {
|
|
5857
|
+
// task-1397: the "new version available" toast, once, after the boot and never over the welcome card.
|
|
5858
|
+
if (state.updateNotice && state.screen !== 'boot' && !state.overlay) {
|
|
5859
|
+
say(state.updateNotice, 'info', 8);
|
|
5860
|
+
state.updateNotice = null;
|
|
5861
|
+
}
|
|
5320
5862
|
if (feed) {
|
|
5321
5863
|
if (!state.paused && Date.now() - lastRefresh >= LIVE_REFRESH_MS)
|
|
5322
5864
|
refresh();
|
|
@@ -5328,6 +5870,9 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
5328
5870
|
lastTokenRefresh = Date.now();
|
|
5329
5871
|
void live.token.load();
|
|
5330
5872
|
}
|
|
5873
|
+
// task-1404: keep the open token's credit-metered holder/rug intel LIVE (slow cadence, budget-gated inside).
|
|
5874
|
+
if (state.screen === 'token' && live.token)
|
|
5875
|
+
live.refreshOpenIntel(Date.now());
|
|
5331
5876
|
if (state.screen === 'watchlist' && Date.now() - lastWatchRefresh >= WATCH_REFRESH_MS) {
|
|
5332
5877
|
lastWatchRefresh = Date.now();
|
|
5333
5878
|
void live.watch.refresh();
|
|
@@ -5361,6 +5906,7 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
5361
5906
|
if (rulesTick(state, Date.now()))
|
|
5362
5907
|
flusher.invalidate();
|
|
5363
5908
|
drainRulesPending();
|
|
5909
|
+
drainDeliveries();
|
|
5364
5910
|
}
|
|
5365
5911
|
catch (e) {
|
|
5366
5912
|
state.screener.notice = `⚠ rules tick failed: ${describeError(e)}`;
|
|
@@ -5391,6 +5937,7 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
5391
5937
|
if (rulesTick(state, Date.now()))
|
|
5392
5938
|
flusher.invalidate();
|
|
5393
5939
|
drainRulesPending();
|
|
5940
|
+
drainDeliveries();
|
|
5394
5941
|
}
|
|
5395
5942
|
catch (e) {
|
|
5396
5943
|
state.screener.notice = `⚠ rules tick failed: ${describeError(e)}`;
|