dexterm-cli 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/packages/agents/src/bus.js +37 -1
- package/dist/packages/agents/src/follow.js +105 -0
- package/dist/packages/agents/src/index.js +5 -0
- package/dist/packages/agents/src/spectator-server.js +153 -0
- package/dist/packages/agents/src/spectator.js +212 -0
- package/dist/packages/cli/assets/claude/analyst-system.md +6 -1
- package/dist/packages/cli/src/agent-cmd.js +71 -7
- package/dist/packages/cli/src/agent-spectator.js +37 -0
- package/dist/packages/cli/src/alerts/format.js +25 -0
- package/dist/packages/cli/src/app-help-gen.js +365 -0
- package/dist/packages/cli/src/doctor.js +6 -5
- package/dist/packages/cli/src/follow-cmd.js +107 -1
- package/dist/packages/cli/src/help-cmd.js +48 -0
- package/dist/packages/cli/src/live/index.js +64 -3
- package/dist/packages/cli/src/live/screens/token-live.js +24 -0
- package/dist/packages/cli/src/live/signals.js +109 -0
- package/dist/packages/cli/src/live/trade.js +24 -1
- package/dist/packages/cli/src/main.js +355 -10
- package/dist/packages/cli/src/mcp-cmd.js +2 -7
- package/dist/packages/cli/src/pnl-app.js +221 -0
- package/dist/packages/cli/src/pnl-card.js +615 -0
- package/dist/packages/cli/src/record-cmd.js +455 -0
- package/dist/packages/cli/src/rules.js +50 -2
- package/dist/packages/cli/src/screens/follows.js +91 -2
- package/dist/packages/cli/src/screens/overlay.js +3 -0
- package/dist/packages/cli/src/screens/positions.js +1 -1
- package/dist/packages/cli/src/screens/rules.js +1 -1
- package/dist/packages/cli/src/spectate-cmd.js +104 -0
- package/dist/packages/cli/src/state.js +1 -1
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/cli/src/voice-cmd.js +86 -11
- package/dist/packages/cli/src/voice-hud.js +1 -1
- package/dist/packages/custom/src/rules.js +2 -1
- package/dist/packages/data/src/analyst-actions.js +130 -3
- package/dist/packages/data/src/fast-lane.js +16 -2
- package/dist/packages/data/src/index.js +1 -0
- package/dist/packages/data/src/intel/live-signals.js +185 -0
- package/dist/packages/data/src/trade/copy/index.js +1 -1
- package/dist/packages/data/src/trade/copy/stats.js +36 -2
- package/dist/packages/data/src/trade/copy/tracker.js +3 -2
- package/dist/packages/mcp/assets/kb/app-help/app-help-command.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/ask-the-analyst.md +43 -0
- package/dist/packages/mcp/assets/kb/app-help/backtest.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/bubble-map.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/calm-mode.md +31 -0
- package/dist/packages/mcp/assets/kb/app-help/change-voice-speed.md +31 -0
- package/dist/packages/mcp/assets/kb/app-help/change-voice.md +30 -0
- package/dist/packages/mcp/assets/kb/app-help/chart-popup-selftest.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/coin-images.md +29 -0
- package/dist/packages/mcp/assets/kb/app-help/configure-rpc-key.md +28 -0
- package/dist/packages/mcp/assets/kb/app-help/connect-an-agent-registry.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/connect-an-agent.md +39 -0
- package/dist/packages/mcp/assets/kb/app-help/custom-keymap.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/edit-kb-or-skill.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/edit-rules.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/export-a-view.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/follow-a-wallet.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/getting-started.md +37 -0
- package/dist/packages/mcp/assets/kb/app-help/kol-lists.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/larp-check.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/link-a-wallet.md +41 -0
- package/dist/packages/mcp/assets/kb/app-help/login-with-x.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/narrative-board.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/open-a-coin.md +43 -0
- package/dist/packages/mcp/assets/kb/app-help/open-chart-popup.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/order-plans.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/paper-trade.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/price-or-marketcap.md +38 -0
- package/dist/packages/mcp/assets/kb/app-help/record-headless.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/recorder-history.md +39 -0
- package/dist/packages/mcp/assets/kb/app-help/rug-check.md +39 -0
- package/dist/packages/mcp/assets/kb/app-help/run-doctor.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/scan-mode.md +37 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-analyst.md +181 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-kol.md +190 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-newpairs.md +181 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +180 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-screener.md +199 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-token.md +240 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-trending.md +192 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-watchlist.md +181 -0
- package/dist/packages/mcp/assets/kb/app-help/set-a-filter.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/setup-neural-voice.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/share-packs.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/sort-the-screener.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/spectate-an-agent.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/standing-mandates.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/switch-theme.md +42 -0
- package/dist/packages/mcp/assets/kb/app-help/turn-on-alerts.md +37 -0
- package/dist/packages/mcp/assets/kb/app-help/update-dexterm.md +30 -0
- package/dist/packages/mcp/assets/kb/app-help/use-presets.md +42 -0
- package/dist/packages/mcp/assets/kb/app-help/use-voice-mode.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/watch-a-coin.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/watch-headless.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/what-dexterm-shows-agents.md +31 -0
- package/dist/packages/mcp/assets/kb/app-help/workspaces.md +38 -0
- package/dist/packages/mcp/src/app-help.js +135 -0
- package/dist/packages/mcp/src/index.js +3 -0
- package/dist/packages/mcp/src/server.js +90 -3
- package/dist/packages/mcp/src/spectator-sink.js +72 -0
- package/dist/packages/packs/src/index.js +2 -0
- package/dist/packages/packs/src/skill-care.js +206 -0
- package/dist/packages/popup/src/pages/spectator.css +45 -0
- package/dist/packages/popup/src/pages/spectator.html +25 -0
- package/dist/packages/popup/src/pages/spectator.js +74 -0
- package/dist/packages/popup/src/selftest.js +19 -0
- package/dist/packages/popup/src/server.js +7 -1
- package/dist/packages/tui/src/keymap.js +10 -0
- package/dist/packages/voice/src/controller.js +42 -1
- package/dist/packages/voice/src/doctor.js +25 -8
- package/dist/packages/voice/src/engines.js +8 -1
- package/dist/packages/voice/src/index.js +3 -1
- package/dist/packages/voice/src/speakable.js +111 -0
- package/dist/packages/voice/src/tts/piper-setup.js +229 -0
- package/dist/packages/voice/src/tts/piper.js +309 -66
- package/dist/packages/voice/src/tts/say.js +3 -2
- package/dist/packages/voice/src/tts/voices.js +8 -5
- package/package.json +1 -1
|
@@ -19,9 +19,11 @@ import { spawn } from 'node:child_process';
|
|
|
19
19
|
import { dirname, join } from 'node:path';
|
|
20
20
|
import { homedir } from 'node:os';
|
|
21
21
|
import { writeExport, exportViewFor } from "./export.js";
|
|
22
|
+
import { openPnl, loadPnl, pnlKey } from "./pnl-app.js";
|
|
23
|
+
import { shareCard, runLocal } from "./pnl-card.js";
|
|
22
24
|
import { parseMintTarget, parseCoinTarget, resolveCoinTarget, isBareOpen } from "./coin-target.js";
|
|
23
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';
|
|
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';
|
|
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';
|
|
25
27
|
import { Recorder, recorderEnabled, MaintenanceSchedule } from '../../recorder/src/index.js';
|
|
26
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";
|
|
27
29
|
import { mulberry32, tick, spawnPair, shock } from "./fixtures.js";
|
|
@@ -39,6 +41,8 @@ import { widgetPage, widgetEnabled, syncWidgetPopups } from "./widgets.js";
|
|
|
39
41
|
import { recordLarpVerdict } from "./larp-record.js";
|
|
40
42
|
import { rulesUiOf, rulesTick, rulesMove, beginRuleInput, saveRuleInput, dryRunNow, removeSelectedRule, toggleSelectedRule, dismissSelectedProposal, actOnSelectedProposal, RULE_INPUT_MAX } from "./rules.js";
|
|
41
43
|
import { buildNotifier, hasPushChannel } from "./alerts/notifier.js";
|
|
44
|
+
import { copyAlertToDelivery } from "./alerts/format.js";
|
|
45
|
+
import { readTelegramCreds, readDiscordCreds } from "./alerts/creds.js";
|
|
42
46
|
import { launchIntelCells, launchRetryable } from "./live/intel.js";
|
|
43
47
|
import { AnalystSession } from "./analyst/session.js";
|
|
44
48
|
import { resolveAnalystResults } from "./analyst/results.js";
|
|
@@ -57,7 +61,7 @@ import { FOLLOW_INPUT_MAX } from "./screens/follows.js";
|
|
|
57
61
|
import { isPriceMode, configPriceMode, togglePriceMode, priceModeOf, planLevels } from "./display.js";
|
|
58
62
|
import { VERSION } from "./version.js";
|
|
59
63
|
import { initCustom, planWorkspace, planPopupOpens, findWorkspace, nextWorkspaceName, workspaceSlots, saveWorkspace, themeFor, themeByName, cycleTheme, themeNames, CHORD_TTL_MS, MAX_WORKSPACE_NAME } from "./custom.js";
|
|
60
|
-
import { DEFAULT_WORKSPACE, MORNING_HUD, applyAndJournal } from '../../custom/src/index.js';
|
|
64
|
+
import { DEFAULT_WORKSPACE, MORNING_HUD, applyAndJournal, policyCheck } from '../../custom/src/index.js';
|
|
61
65
|
/**
|
|
62
66
|
* Context the `screen_state` MCP tool returns AND the per-turn context block is rendered from
|
|
63
67
|
* (task-1313): what the user is looking at right now — selected/hovered coin, screen, the visible
|
|
@@ -65,6 +69,37 @@ import { DEFAULT_WORKSPACE, MORNING_HUD, applyAndJournal } from '../../custom/sr
|
|
|
65
69
|
* that bounds what the analyst's UI actions may reference. Token-derived text stays raw here; the
|
|
66
70
|
* context-block renderer wraps it as untrusted before a model sees it.
|
|
67
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
|
+
}
|
|
68
103
|
export function buildScreenState(state) {
|
|
69
104
|
const live = state.live;
|
|
70
105
|
const sc = state.screener;
|
|
@@ -127,7 +162,14 @@ export function buildScreenState(state) {
|
|
|
127
162
|
watchlist,
|
|
128
163
|
ticket,
|
|
129
164
|
positions,
|
|
130
|
-
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),
|
|
131
173
|
now: Date.now(),
|
|
132
174
|
};
|
|
133
175
|
}
|
|
@@ -225,10 +267,11 @@ export const HELP = `dexterm ${VERSION} (live: screener, token, new pairs, watch
|
|
|
225
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
|
|
226
268
|
dexterm plan propose|list|arm|cancel|journal conditional order plans (entry/ladder/take-profit/moon bag/stop); arming needs a physical keypress
|
|
227
269
|
dexterm mandate draft|list|arm|revoke standing capped mandates (fast wallet); arming needs a physical keypress — voice/agents can never arm
|
|
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;
|
|
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|
|
|
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
|
|
230
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)
|
|
231
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')
|
|
232
275
|
dexterm --fixtures interactive demo on seeded fixtures
|
|
233
276
|
dexterm --screen <name> one-shot frame to stdout (live)
|
|
234
277
|
dexterm --screen <name> --fixtures one-shot frame from fixtures
|
|
@@ -315,9 +358,10 @@ ${X_HELP}
|
|
|
315
358
|
--version print the version
|
|
316
359
|
|
|
317
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
|
|
318
|
-
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
|
|
319
362
|
voice asks the analyst or triggers safe actions (open/filter/watch/open a PAPER ticket);
|
|
320
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)
|
|
321
365
|
screener: / filter · s sort · [ ] presets · r refresh · g/G top/bottom
|
|
322
366
|
social (task-1330): T trending in the space · K KOL lists · token screen: s social panel · f filter · x hidden spam
|
|
323
367
|
dexterm kol list | create <name> | add <list> @handle | remove <list> @handle | mute @handle | unmute @handle | enable <list> | disable <list> | import <dir> | import-x
|
|
@@ -725,6 +769,12 @@ export async function main(argv, io = process, proc = process) {
|
|
|
725
769
|
const { runAgent } = await import("./agent-cmd.js");
|
|
726
770
|
return runAgent(argv.slice(1), io);
|
|
727
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
|
+
}
|
|
728
778
|
// `dexterm recorder <cmd>` → the history recorder's tables: stats, label, prune, export (task-1329). No network.
|
|
729
779
|
if (argv[0] === 'recorder') {
|
|
730
780
|
const { recorderCli } = await import('../../recorder/src/index.js');
|
|
@@ -760,6 +810,12 @@ export async function main(argv, io = process, proc = process) {
|
|
|
760
810
|
const { kbSkillCommand } = await import("./kb-skill-cmd.js");
|
|
761
811
|
return kbSkillCommand(argv[0], argv.slice(1), { stdout: io.stdout, stderr: io.stderr, env: io.env, isTTY: io.stdout.isTTY === true });
|
|
762
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
|
+
}
|
|
763
819
|
// `dexterm theme list|show|init|check` → user themes under ~/.dexterm/themes (task-1358 Tier 2). No network.
|
|
764
820
|
if (argv[0] === 'theme') {
|
|
765
821
|
const { themeCommand } = await import("./theme-cmd.js");
|
|
@@ -792,6 +848,13 @@ export async function main(argv, io = process, proc = process) {
|
|
|
792
848
|
const { watchCommand } = await import("./watch-cmd.js");
|
|
793
849
|
return watchCommand(argv.slice(1), { stdout: io.stdout, stderr: io.stderr, env: io.env });
|
|
794
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
|
+
}
|
|
795
858
|
// `dexterm pack list|verify|add|remove|export|lock|sign|trust|signers|keymap|widget` → shareable packs (task-1358 Tier 4):
|
|
796
859
|
// plain files + sha256 lock + optional ssh signature. No network, no model; a pack never runs code (widgets land
|
|
797
860
|
// disabled, unsigned rules load disabled, a keymap merges only on the explicit command).
|
|
@@ -1228,6 +1291,10 @@ function followsMove(state, delta) {
|
|
|
1228
1291
|
return false;
|
|
1229
1292
|
if (state.copyUi.input !== null || state.copyUi.mirror || state.copyUi.autoModal)
|
|
1230
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
|
|
1231
1298
|
const n = state.copy?.follows().length ?? 0;
|
|
1232
1299
|
if (n <= 0) {
|
|
1233
1300
|
state.copyUi.selected = 0;
|
|
@@ -1513,6 +1580,10 @@ export function applyAction(state, action, stack) {
|
|
|
1513
1580
|
state.copyUi.notice = 'mirror preview closed';
|
|
1514
1581
|
break;
|
|
1515
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
|
|
1516
1587
|
const prev = stack.pop();
|
|
1517
1588
|
if (prev)
|
|
1518
1589
|
state.screen = prev;
|
|
@@ -2337,6 +2408,10 @@ export function applyAction(state, action, stack) {
|
|
|
2337
2408
|
case 'export.view':
|
|
2338
2409
|
state.pendingExport = true;
|
|
2339
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;
|
|
2340
2415
|
case 'display.priceMode':
|
|
2341
2416
|
state.pendingToast = togglePriceMode(state);
|
|
2342
2417
|
break; // task-1366: display only, persisted
|
|
@@ -2708,6 +2783,12 @@ export function applyAnalystAction(state, raw, stack) {
|
|
|
2708
2783
|
if (!res.ok)
|
|
2709
2784
|
return { ok: false, error: res.error };
|
|
2710
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
|
+
}
|
|
2711
2792
|
// Resolve a mint to a row object from what is on screen (allow-list guarantees it was visible).
|
|
2712
2793
|
const findSeed = (mint) => seedForMint(state, live, mint);
|
|
2713
2794
|
const toScreen = (next) => { if (state.screen !== next) {
|
|
@@ -2747,6 +2828,10 @@ export function applyAnalystAction(state, raw, stack) {
|
|
|
2747
2828
|
}
|
|
2748
2829
|
case 'show_in_screener': {
|
|
2749
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);
|
|
2750
2835
|
if (action.filter) {
|
|
2751
2836
|
const err = setFilter(sc, action.filter);
|
|
2752
2837
|
if (err)
|
|
@@ -2760,12 +2845,101 @@ export function applyAnalystAction(state, raw, stack) {
|
|
|
2760
2845
|
state.selected = i;
|
|
2761
2846
|
}
|
|
2762
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;
|
|
2763
2892
|
break;
|
|
2764
2893
|
}
|
|
2765
2894
|
}
|
|
2766
2895
|
times.push(now);
|
|
2767
2896
|
return { ok: true, label: res.label };
|
|
2768
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
|
+
}
|
|
2769
2943
|
/**
|
|
2770
2944
|
* Route one decoded key: the screener's filter editor and sort menu swallow keys while open,
|
|
2771
2945
|
* everything else goes through the keymap. Exported for tests.
|
|
@@ -2993,6 +3167,22 @@ export function applyKey(state, key, stack) {
|
|
|
2993
3167
|
ui.input = cur + key.ch;
|
|
2994
3168
|
return;
|
|
2995
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
|
+
}
|
|
2996
3186
|
if (state.screen === 'kol' && state.kolInput !== null && state.kolInput !== undefined) {
|
|
2997
3187
|
const kol = kolOf(state);
|
|
2998
3188
|
if (key.name === 'esc') {
|
|
@@ -3623,6 +3813,10 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
3623
3813
|
// Live bindings redraw as soon as something lands (one frame per macrotask at most).
|
|
3624
3814
|
let drawPending = false;
|
|
3625
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 = () => { };
|
|
3626
3820
|
if (live) {
|
|
3627
3821
|
// task-1360: poll the followed wallets for the whole run (live only; replay never spends credits) and toast
|
|
3628
3822
|
// every new buy/sell a followed wallet made. Information only: no path from an alert to a trade exists here.
|
|
@@ -3635,6 +3829,8 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
3635
3829
|
const a = as[as.length - 1];
|
|
3636
3830
|
const who = a.name ?? shortAddr(a.wallet);
|
|
3637
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
|
|
3638
3834
|
}
|
|
3639
3835
|
}
|
|
3640
3836
|
if (done || drawPending)
|
|
@@ -3755,6 +3951,15 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
3755
3951
|
const r = applyAnalystAction(state, raw, stack);
|
|
3756
3952
|
if (!r.ok || !r.label)
|
|
3757
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
|
+
}
|
|
3758
3963
|
const log = (state.analystActions ??= []);
|
|
3759
3964
|
log.push(r.label);
|
|
3760
3965
|
if (log.length > 6)
|
|
@@ -4232,6 +4437,10 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
4232
4437
|
const alertNotifier = state.custom && hasPushChannel(state.custom.config.alerts)
|
|
4233
4438
|
? buildNotifier(state.custom.home, state.custom.config.alerts, { toastSink: () => { } })
|
|
4234
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(() => { }); };
|
|
4235
4444
|
const drainDeliveries = () => {
|
|
4236
4445
|
const ui = state.rulesUi;
|
|
4237
4446
|
if (!ui || !ui.pendingDeliveries.length)
|
|
@@ -4402,6 +4611,47 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
4402
4611
|
return { price: np.price ?? null, mcap: np.mcap ?? null };
|
|
4403
4612
|
return { price: null, mcap: null };
|
|
4404
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
|
+
};
|
|
4405
4655
|
/**
|
|
4406
4656
|
* Perform a voice decision. Returns `false` when the app spoke its own reply (an ask / clarify), so
|
|
4407
4657
|
* the controller does not read the decision's `say` line over it (task-1339, re-verify N1: the
|
|
@@ -4417,6 +4667,7 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
4417
4667
|
const a = SCREEN_ACTION[d.screen];
|
|
4418
4668
|
if (a)
|
|
4419
4669
|
applyAction(state, a, stack);
|
|
4670
|
+
ask(NAV_ACK[d.screen] ?? d.say);
|
|
4420
4671
|
break;
|
|
4421
4672
|
}
|
|
4422
4673
|
case 'open-token': {
|
|
@@ -4541,14 +4792,14 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
4541
4792
|
case 'analyst': {
|
|
4542
4793
|
// Route the question to the analyst, surface the chat, and let the answer be spoken when it lands.
|
|
4543
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.
|
|
4544
4797
|
if (state.screen !== 'token' && state.screen !== 'analyst') {
|
|
4545
4798
|
stack.push(state.screen);
|
|
4546
4799
|
state.screen = 'analyst';
|
|
4547
4800
|
}
|
|
4548
4801
|
syncAnalyst(state);
|
|
4549
|
-
|
|
4550
|
-
if (state.pendingAnalyst)
|
|
4551
|
-
beginAnalyst();
|
|
4802
|
+
askVoiceAnalyst(d.question);
|
|
4552
4803
|
break;
|
|
4553
4804
|
}
|
|
4554
4805
|
case 'workspace': {
|
|
@@ -4615,6 +4866,21 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
4615
4866
|
}
|
|
4616
4867
|
catch { /* keep the voice for this run; nothing to surface */ }
|
|
4617
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
|
+
};
|
|
4618
4884
|
const initVoice = async () => {
|
|
4619
4885
|
if (voiceInitStarted)
|
|
4620
4886
|
return;
|
|
@@ -4643,6 +4909,7 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
4643
4909
|
} },
|
|
4644
4910
|
onDecision: handleVoiceDecision,
|
|
4645
4911
|
onVoiceChange: persistVoiceChoice,
|
|
4912
|
+
onRateChange: persistVoiceRate,
|
|
4646
4913
|
});
|
|
4647
4914
|
voice.enable();
|
|
4648
4915
|
await voice.toggleTalk(); // the first `v` press both enables and starts listening
|
|
@@ -4706,6 +4973,35 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
4706
4973
|
quit();
|
|
4707
4974
|
return;
|
|
4708
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
|
+
}
|
|
4709
5005
|
// task-1388 GO-TO prompt: while the "open any coin" input is open it owns the keyboard. Route the key
|
|
4710
5006
|
// straight to applyKey (which does the capture / open) and skip the voice, calm, demo and keymap
|
|
4711
5007
|
// handlers below — otherwise a base58 char like v / m / c would toggle voice or calm instead of typing.
|
|
@@ -4740,7 +5036,7 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
4740
5036
|
// `micEngaged` also covers the window while the mic is still OPENING (listen() in flight), so a key
|
|
4741
5037
|
// pressed before the engine came up closes the session the moment it arrives (task-1339, N3/N8).
|
|
4742
5038
|
if (voice?.micEngaged) {
|
|
4743
|
-
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 === '>');
|
|
4744
5040
|
if (key.name === 'esc') {
|
|
4745
5041
|
voice.cancelListen();
|
|
4746
5042
|
draw();
|
|
@@ -4782,6 +5078,16 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
4782
5078
|
draw();
|
|
4783
5079
|
continue;
|
|
4784
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)
|
|
4785
5091
|
}
|
|
4786
5092
|
}
|
|
4787
5093
|
// A modal overlay (welcome/help) is open: any key dismisses it; `?` switches welcome→help (task-1305).
|
|
@@ -4800,6 +5106,32 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
4800
5106
|
draw();
|
|
4801
5107
|
continue;
|
|
4802
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
|
+
}
|
|
4803
5135
|
// task-1358: the WORKSPACES card — a digit picks a slot, j/k move, ⏎ activates, esc/q/ctrl+w w close.
|
|
4804
5136
|
if (state.overlay === 'workspaces' && state.custom) {
|
|
4805
5137
|
const ui = state.custom;
|
|
@@ -5449,6 +5781,16 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
5449
5781
|
say(`export failed: ${describeError(e)}`, 'warn', 6);
|
|
5450
5782
|
}
|
|
5451
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
|
+
}
|
|
5452
5794
|
if (state.pendingToast) {
|
|
5453
5795
|
// task-1315: surface the link list / "no link" reply as a durable toast. Drained BEFORE
|
|
5454
5796
|
// pendingOpen so that on a successful open the "opened <url>" toast overwrites the "open <url>" one.
|
|
@@ -5528,6 +5870,9 @@ export function interactive(state, io, depth, args, proc = process) {
|
|
|
5528
5870
|
lastTokenRefresh = Date.now();
|
|
5529
5871
|
void live.token.load();
|
|
5530
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());
|
|
5531
5876
|
if (state.screen === 'watchlist' && Date.now() - lastWatchRefresh >= WATCH_REFRESH_MS) {
|
|
5532
5877
|
lastWatchRefresh = Date.now();
|
|
5533
5878
|
void live.watch.refresh();
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// dexterm mcp run the read-only MCP server on stdio (what the analyst uses and
|
|
4
4
|
// what `claude mcp add dexterm -- dexterm mcp` wires up)
|
|
5
5
|
// dexterm mcp install <client> write/print the MCP config for a client (claude-code, claude-desktop,
|
|
6
|
-
// cursor, codex,
|
|
6
|
+
// cursor, codex, generic). Shows the exact change, asks first,
|
|
7
7
|
// is idempotent, and backs the file up before editing.
|
|
8
8
|
// dexterm mcp doctor spawn the server over stdio, do the MCP handshake, and list the tools,
|
|
9
9
|
// prompts and resources the agent sees — proof the connection works.
|
|
@@ -60,15 +60,10 @@ export const CLIENTS = [
|
|
|
60
60
|
path: (env) => join(env.CODEX_HOME || join(home(env), '.codex'), 'config.toml'),
|
|
61
61
|
note: 'Codex reads MCP servers from the [mcp_servers.*] tables in ~/.codex/config.toml.',
|
|
62
62
|
},
|
|
63
|
-
{
|
|
64
|
-
id: 'openclaw', label: 'OpenClaw / generic agent', format: 'print', keyPath: 'mcpServers',
|
|
65
|
-
path: () => null,
|
|
66
|
-
note: 'OpenClaw and most agents take a standard mcpServers entry; paste this where your agent reads MCP servers, or pass --config FILE to write a JSON file.',
|
|
67
|
-
},
|
|
68
63
|
{
|
|
69
64
|
id: 'generic', label: 'Generic (print the snippet)', format: 'print', keyPath: 'mcpServers',
|
|
70
65
|
path: () => null,
|
|
71
|
-
note: 'the standard stdio
|
|
66
|
+
note: 'the standard stdio mcpServers entry most agents take; paste it where your agent reads MCP servers, or pass --config FILE to write a JSON file.',
|
|
72
67
|
},
|
|
73
68
|
];
|
|
74
69
|
export function clientById(id) { return CLIENTS.find((c) => c.id === id); }
|