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.
Files changed (118) hide show
  1. package/dist/packages/agents/src/bus.js +37 -1
  2. package/dist/packages/agents/src/follow.js +105 -0
  3. package/dist/packages/agents/src/index.js +5 -0
  4. package/dist/packages/agents/src/spectator-server.js +153 -0
  5. package/dist/packages/agents/src/spectator.js +212 -0
  6. package/dist/packages/cli/assets/claude/analyst-system.md +6 -1
  7. package/dist/packages/cli/src/agent-cmd.js +71 -7
  8. package/dist/packages/cli/src/agent-spectator.js +37 -0
  9. package/dist/packages/cli/src/alerts/format.js +25 -0
  10. package/dist/packages/cli/src/app-help-gen.js +365 -0
  11. package/dist/packages/cli/src/doctor.js +6 -5
  12. package/dist/packages/cli/src/follow-cmd.js +107 -1
  13. package/dist/packages/cli/src/help-cmd.js +48 -0
  14. package/dist/packages/cli/src/live/index.js +64 -3
  15. package/dist/packages/cli/src/live/screens/token-live.js +24 -0
  16. package/dist/packages/cli/src/live/signals.js +109 -0
  17. package/dist/packages/cli/src/live/trade.js +24 -1
  18. package/dist/packages/cli/src/main.js +355 -10
  19. package/dist/packages/cli/src/mcp-cmd.js +2 -7
  20. package/dist/packages/cli/src/pnl-app.js +221 -0
  21. package/dist/packages/cli/src/pnl-card.js +615 -0
  22. package/dist/packages/cli/src/record-cmd.js +455 -0
  23. package/dist/packages/cli/src/rules.js +50 -2
  24. package/dist/packages/cli/src/screens/follows.js +91 -2
  25. package/dist/packages/cli/src/screens/overlay.js +3 -0
  26. package/dist/packages/cli/src/screens/positions.js +1 -1
  27. package/dist/packages/cli/src/screens/rules.js +1 -1
  28. package/dist/packages/cli/src/spectate-cmd.js +104 -0
  29. package/dist/packages/cli/src/state.js +1 -1
  30. package/dist/packages/cli/src/version.js +1 -1
  31. package/dist/packages/cli/src/voice-cmd.js +86 -11
  32. package/dist/packages/cli/src/voice-hud.js +1 -1
  33. package/dist/packages/custom/src/rules.js +2 -1
  34. package/dist/packages/data/src/analyst-actions.js +130 -3
  35. package/dist/packages/data/src/fast-lane.js +16 -2
  36. package/dist/packages/data/src/index.js +1 -0
  37. package/dist/packages/data/src/intel/live-signals.js +185 -0
  38. package/dist/packages/data/src/trade/copy/index.js +1 -1
  39. package/dist/packages/data/src/trade/copy/stats.js +36 -2
  40. package/dist/packages/data/src/trade/copy/tracker.js +3 -2
  41. package/dist/packages/mcp/assets/kb/app-help/app-help-command.md +35 -0
  42. package/dist/packages/mcp/assets/kb/app-help/ask-the-analyst.md +43 -0
  43. package/dist/packages/mcp/assets/kb/app-help/backtest.md +33 -0
  44. package/dist/packages/mcp/assets/kb/app-help/bubble-map.md +35 -0
  45. package/dist/packages/mcp/assets/kb/app-help/calm-mode.md +31 -0
  46. package/dist/packages/mcp/assets/kb/app-help/change-voice-speed.md +31 -0
  47. package/dist/packages/mcp/assets/kb/app-help/change-voice.md +30 -0
  48. package/dist/packages/mcp/assets/kb/app-help/chart-popup-selftest.md +32 -0
  49. package/dist/packages/mcp/assets/kb/app-help/coin-images.md +29 -0
  50. package/dist/packages/mcp/assets/kb/app-help/configure-rpc-key.md +28 -0
  51. package/dist/packages/mcp/assets/kb/app-help/connect-an-agent-registry.md +35 -0
  52. package/dist/packages/mcp/assets/kb/app-help/connect-an-agent.md +39 -0
  53. package/dist/packages/mcp/assets/kb/app-help/custom-keymap.md +32 -0
  54. package/dist/packages/mcp/assets/kb/app-help/edit-kb-or-skill.md +36 -0
  55. package/dist/packages/mcp/assets/kb/app-help/edit-rules.md +34 -0
  56. package/dist/packages/mcp/assets/kb/app-help/export-a-view.md +36 -0
  57. package/dist/packages/mcp/assets/kb/app-help/follow-a-wallet.md +33 -0
  58. package/dist/packages/mcp/assets/kb/app-help/getting-started.md +37 -0
  59. package/dist/packages/mcp/assets/kb/app-help/kol-lists.md +34 -0
  60. package/dist/packages/mcp/assets/kb/app-help/larp-check.md +36 -0
  61. package/dist/packages/mcp/assets/kb/app-help/link-a-wallet.md +41 -0
  62. package/dist/packages/mcp/assets/kb/app-help/login-with-x.md +33 -0
  63. package/dist/packages/mcp/assets/kb/app-help/narrative-board.md +32 -0
  64. package/dist/packages/mcp/assets/kb/app-help/open-a-coin.md +43 -0
  65. package/dist/packages/mcp/assets/kb/app-help/open-chart-popup.md +33 -0
  66. package/dist/packages/mcp/assets/kb/app-help/order-plans.md +33 -0
  67. package/dist/packages/mcp/assets/kb/app-help/paper-trade.md +36 -0
  68. package/dist/packages/mcp/assets/kb/app-help/price-or-marketcap.md +38 -0
  69. package/dist/packages/mcp/assets/kb/app-help/record-headless.md +34 -0
  70. package/dist/packages/mcp/assets/kb/app-help/recorder-history.md +39 -0
  71. package/dist/packages/mcp/assets/kb/app-help/rug-check.md +39 -0
  72. package/dist/packages/mcp/assets/kb/app-help/run-doctor.md +32 -0
  73. package/dist/packages/mcp/assets/kb/app-help/scan-mode.md +37 -0
  74. package/dist/packages/mcp/assets/kb/app-help/screen-analyst.md +181 -0
  75. package/dist/packages/mcp/assets/kb/app-help/screen-kol.md +190 -0
  76. package/dist/packages/mcp/assets/kb/app-help/screen-newpairs.md +181 -0
  77. package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +180 -0
  78. package/dist/packages/mcp/assets/kb/app-help/screen-screener.md +199 -0
  79. package/dist/packages/mcp/assets/kb/app-help/screen-token.md +240 -0
  80. package/dist/packages/mcp/assets/kb/app-help/screen-trending.md +192 -0
  81. package/dist/packages/mcp/assets/kb/app-help/screen-watchlist.md +181 -0
  82. package/dist/packages/mcp/assets/kb/app-help/set-a-filter.md +36 -0
  83. package/dist/packages/mcp/assets/kb/app-help/setup-neural-voice.md +36 -0
  84. package/dist/packages/mcp/assets/kb/app-help/share-packs.md +34 -0
  85. package/dist/packages/mcp/assets/kb/app-help/sort-the-screener.md +34 -0
  86. package/dist/packages/mcp/assets/kb/app-help/spectate-an-agent.md +36 -0
  87. package/dist/packages/mcp/assets/kb/app-help/standing-mandates.md +33 -0
  88. package/dist/packages/mcp/assets/kb/app-help/switch-theme.md +42 -0
  89. package/dist/packages/mcp/assets/kb/app-help/turn-on-alerts.md +37 -0
  90. package/dist/packages/mcp/assets/kb/app-help/update-dexterm.md +30 -0
  91. package/dist/packages/mcp/assets/kb/app-help/use-presets.md +42 -0
  92. package/dist/packages/mcp/assets/kb/app-help/use-voice-mode.md +36 -0
  93. package/dist/packages/mcp/assets/kb/app-help/watch-a-coin.md +34 -0
  94. package/dist/packages/mcp/assets/kb/app-help/watch-headless.md +35 -0
  95. package/dist/packages/mcp/assets/kb/app-help/what-dexterm-shows-agents.md +31 -0
  96. package/dist/packages/mcp/assets/kb/app-help/workspaces.md +38 -0
  97. package/dist/packages/mcp/src/app-help.js +135 -0
  98. package/dist/packages/mcp/src/index.js +3 -0
  99. package/dist/packages/mcp/src/server.js +90 -3
  100. package/dist/packages/mcp/src/spectator-sink.js +72 -0
  101. package/dist/packages/packs/src/index.js +2 -0
  102. package/dist/packages/packs/src/skill-care.js +206 -0
  103. package/dist/packages/popup/src/pages/spectator.css +45 -0
  104. package/dist/packages/popup/src/pages/spectator.html +25 -0
  105. package/dist/packages/popup/src/pages/spectator.js +74 -0
  106. package/dist/packages/popup/src/selftest.js +19 -0
  107. package/dist/packages/popup/src/server.js +7 -1
  108. package/dist/packages/tui/src/keymap.js +10 -0
  109. package/dist/packages/voice/src/controller.js +42 -1
  110. package/dist/packages/voice/src/doctor.js +25 -8
  111. package/dist/packages/voice/src/engines.js +8 -1
  112. package/dist/packages/voice/src/index.js +3 -1
  113. package/dist/packages/voice/src/speakable.js +111 -0
  114. package/dist/packages/voice/src/tts/piper-setup.js +229 -0
  115. package/dist/packages/voice/src/tts/piper.js +309 -66
  116. package/dist/packages/voice/src/tts/say.js +3 -2
  117. package/dist/packages/voice/src/tts/voices.js +8 -5
  118. package/package.json +1 -1
@@ -0,0 +1,104 @@
1
+ // `dexterm spectate …` — watch an agent drive dexterm (task-1406, "watch the agent cook").
2
+ //
3
+ // dexterm spectate --replay FILE [--json] replay a saved session file (demos + bug reports): fold the
4
+ // recorded agent actions into the screen model + activity panel
5
+ // dexterm spectate --info [--json] show the live spectator stream endpoint (loopback + token)
6
+ // from ~/.dexterm/spectator.json, if a session is streaming
7
+ //
8
+ // Replay is deterministic and offline. The live stream itself is started by `dexterm agent serve --spectate`;
9
+ // it is read-only, loopback-only and token-gated, and redacts wallet addresses/keys (see SPECTATOR.md).
10
+ import { readFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { FollowController, loadSpectatorRecording } from '../../agents/src/index.js';
13
+ function flag(rest, name) { const i = rest.indexOf(name); return i >= 0 ? rest[i + 1] : undefined; }
14
+ function has(rest, name) { return rest.includes(name); }
15
+ const USAGE = `dexterm spectate <command>
16
+ --replay FILE [--json] replay a saved session (fold agent actions into the screen + activity panel)
17
+ --info [--json] show the live stream endpoint from ~/.dexterm/spectator.json (if streaming)
18
+ read-only · redacts wallet addresses/keys · the live stream is started by \`dexterm agent serve --spectate\``;
19
+ function ago(t) { const s = Math.max(0, Math.round((Date.now() - t) / 1000)); return s < 60 ? `${s}s` : s < 3600 ? `${Math.floor(s / 60)}m` : `${Math.floor(s / 3600)}h`; }
20
+ function age(s) { if (s == null)
21
+ return ''; return s < 60 ? ` · ${Math.round(s)}s old` : s < 3600 ? ` · ${Math.floor(s / 60)}m old` : ` · ${Math.floor(s / 3600)}h old`; }
22
+ function renderView(view, meta, out) {
23
+ const s = view.screen;
24
+ out('SCREEN');
25
+ const bits = [`screen ${s.screen}`];
26
+ if (s.selectedMint)
27
+ bits.push(`token ${s.selectedMint}`);
28
+ if (s.feed)
29
+ bits.push(`feed ${s.feed}`);
30
+ if (s.filter)
31
+ bits.push(`filter ${s.filter}`);
32
+ if (s.highlight)
33
+ bits.push(`★ ${s.highlight}`);
34
+ if (s.watchlist.length)
35
+ bits.push(`watchlist ${s.watchlist.length}`);
36
+ out(` ${bits.join(' · ')}`);
37
+ if (view.control === 'user')
38
+ out(' YOU HAVE TAKEN OVER (agent frozen)');
39
+ out('');
40
+ out('AGENTS');
41
+ if (!view.agents.length)
42
+ out(' (none)');
43
+ for (const a of view.agents)
44
+ out(` [agent] ${a.name} ${a.tier ?? '—'} ${a.status} · ${ago(a.lastAt)}`);
45
+ out('');
46
+ out(`ACTIVITY (${view.activity.length}${meta.redacted ? ' · redacted' : ' · full'})`);
47
+ for (const ev of view.activity) {
48
+ const ms = ev.ms != null ? ` ${Math.round(ev.ms)}ms` : '';
49
+ out(` ${ago(ev.t).padStart(4)} [agent] ${ev.agent} ${ev.phase.padEnd(8)} ${(ev.tool ?? '').padEnd(16)} ${ev.summary ?? ''}${ms}${age(ev.dataAgeSec)}`);
50
+ }
51
+ }
52
+ export async function runSpectate(rest, io) {
53
+ const out = (s) => io.stdout.write(s + '\n');
54
+ const errOut = (s) => io.stderr.write(s + '\n');
55
+ const replayFile = flag(rest, '--replay');
56
+ if (replayFile) {
57
+ let recording;
58
+ try {
59
+ recording = loadSpectatorRecording(replayFile);
60
+ }
61
+ catch (e) {
62
+ errOut(`dexterm spectate: cannot read ${replayFile}: ${e.message}`);
63
+ return 1;
64
+ }
65
+ const follow = new FollowController({ activityCap: 500 });
66
+ for (const ev of recording.events)
67
+ follow.apply(ev);
68
+ if (has(rest, '--json')) {
69
+ out(JSON.stringify({ meta: recording.meta, view: follow.view() }));
70
+ return 0;
71
+ }
72
+ out(`replaying ${recording.events.length} event(s) from ${replayFile}${recording.meta.redacted ? ' (redacted)' : ''}`);
73
+ out('');
74
+ renderView(follow.view(), recording.meta, out);
75
+ return 0;
76
+ }
77
+ if (has(rest, '--info')) {
78
+ const home = io.env.DEXTERM_HOME || join(io.env.HOME || '', '.dexterm');
79
+ const path = join(home, 'spectator.json');
80
+ let info;
81
+ try {
82
+ info = JSON.parse(readFileSync(path, 'utf8'));
83
+ }
84
+ catch {
85
+ errOut('no live spectator stream (start one with `dexterm agent serve --spectate`).');
86
+ return 1;
87
+ }
88
+ if (has(rest, '--json')) {
89
+ out(JSON.stringify(info));
90
+ return 0;
91
+ }
92
+ out(`live spectator stream on ws://127.0.0.1:${info.port} (loopback only).`);
93
+ out(`token is in ${path} (mode 0600) — pass it in the viewer's hello; never on a command line.`);
94
+ out('the stream is read-only and redacts wallet addresses/keys.');
95
+ return 0;
96
+ }
97
+ if (rest.length && !rest[0].startsWith('--')) {
98
+ errOut(`dexterm spectate: unknown subcommand ${JSON.stringify(rest[0])}`);
99
+ errOut(USAGE);
100
+ return 2;
101
+ }
102
+ out(USAGE);
103
+ return rest.length ? 2 : 0;
104
+ }
@@ -120,7 +120,7 @@ export function plansRows(state) {
120
120
  return [];
121
121
  return state.plansUi?.tab === 'mandates' ? svc.listMandates() : svc.listPlans();
122
122
  }
123
- export function newFollowsUi() { return { selected: 0, input: null, notice: null, mirror: null, autoModal: null }; }
123
+ export function newFollowsUi() { return { selected: 0, input: null, notice: null, mirror: null, autoModal: null, detail: null }; }
124
124
  /**
125
125
  * Tier 3: the auto-mirror service for this run, attached to the copy binding on first use. It sits on the plan
126
126
  * service (the signer trust domain) and the copy binding's market resolver; its journal is ~/.dexterm/copy.journal.
@@ -1,3 +1,3 @@
1
1
  // The one place the app's version lives (task-1397). --version, the help banner and `dexterm update` read it;
2
2
  // cli.test.ts keeps it equal to packages/cli/package.json (release.yml refuses a tag that differs).
3
- export const VERSION = '0.1.2';
3
+ export const VERSION = '0.1.3';
@@ -1,25 +1,47 @@
1
- // `dexterm voice …` (task-1408): inspect the voice engines on this machine and download the two
2
- // optional voice models — whisper.cpp base.en (speech-in) and the Piper en_GB-alan-medium voice
3
- // (neural speech-out). Downloads are OFF by default: this is the ONLY place dexterm fetches a model,
4
- // it asks before it does, and it verifies the download against a pinned checksum from the official
5
- // source. Nothing here runs the signer, touches keys, or changes trading behaviour.
1
+ // `dexterm voice …` (task-1408, task-1416): inspect the voice engines on this machine, download the two
2
+ // optional voice models — whisper.cpp base.en (speech-in) and the Piper en_GB-alan-medium voice (neural
3
+ // speech-out) — and SET UP the managed neural voice on macOS. Network use is OFF by default: this is the
4
+ // only place dexterm fetches a model or installs a runtime, it asks before it does, and it verifies the
5
+ // voice model against a pinned checksum from the official source. Nothing here runs the signer, touches
6
+ // keys, or changes trading behaviour.
6
7
  //
7
- // No new dependencies: the confirm prompt is node:readline, the download uses global fetch, and the
8
- // checksum/stream logic lives in @dexterm/voice (unit-tested with an injected fetch).
8
+ // No new dependencies: the confirm prompt is node:readline, downloads use global fetch, `voice setup`
9
+ // shells out to the system python/pip, and the checksum/stream/venv logic lives in @dexterm/voice.
9
10
  import { createInterface } from 'node:readline';
11
+ import { spawn } from 'node:child_process';
10
12
  import { existsSync } from 'node:fs';
11
13
  import { join } from 'node:path';
12
- import { modelRegistry, downloadModel, modelStatus, resolvePiperBin, pickPlayer, PIPER_MODEL, DEFAULT_PIPER_VOICE } from '../../voice/src/index.js';
14
+ import { modelRegistry, downloadModel, modelStatus, resolvePiperBin, pickPlayer, PIPER_MODEL, DEFAULT_PIPER_VOICE, setupPiperRuntime, uninstallPiperRuntime, piperRuntimeStatus, } from '../../voice/src/index.js';
13
15
  import { dextermHome } from '../../custom/src/index.js';
14
16
  export const VOICE_USAGE = [
15
17
  'usage:',
16
18
  ' dexterm voice [status] show the voice engines + which models are installed',
19
+ ' dexterm voice setup [--yes] set up the neural voice (Piper) on this machine (asks first)',
20
+ ' dexterm voice uninstall [--purge-model] remove the managed neural-voice runtime',
17
21
  ' dexterm voice download <model> [--yes] download a voice model (asks first; checksum-verified)',
18
22
  '',
19
23
  ' models: whisper whisper.cpp base.en (speech-to-text, off macOS this is the default)',
20
- ' piper-voice Piper en_GB-alan-medium (neural speech-out, preferred over espeak-ng)',
24
+ ' piper-voice Piper en_GB-alan-medium (neural speech-out, the default speaking voice)',
21
25
  ' all both of the above',
22
26
  ].join('\n');
27
+ /** Run a command, capture its output, and resolve a RunResult (never rejects). Used by `voice setup`. */
28
+ function runCmd(cmd, args) {
29
+ return new Promise((resolve) => {
30
+ let child;
31
+ try {
32
+ child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] });
33
+ }
34
+ catch (e) {
35
+ resolve({ code: 127, stdout: '', stderr: e.message });
36
+ return;
37
+ }
38
+ let stdout = '', stderr = '';
39
+ child.stdout?.on('data', (d) => { stdout += d; });
40
+ child.stderr?.on('data', (d) => { stderr += d; });
41
+ child.on('error', (e) => resolve({ code: 127, stdout, stderr: stderr || e.message }));
42
+ child.on('close', (code) => resolve({ code, stdout, stderr }));
43
+ });
44
+ }
23
45
  function onPath(bin, env) {
24
46
  const sep = process.platform === 'win32' ? ';' : ':';
25
47
  for (const p of (env.PATH ?? '').split(sep)) {
@@ -42,7 +64,18 @@ async function printStatus(io) {
42
64
  const reg = modelRegistry(home);
43
65
  const lines = ['dexterm voice', ''];
44
66
  if (process.platform === 'darwin') {
45
- lines.push(' speech-out macOS `say` (built-in) · see `dexterm doctor` for the resolved voice');
67
+ const rt = await piperRuntimeStatus(home);
68
+ const player = pickPlayer();
69
+ const piperReady = rt.installed && player !== null;
70
+ if (piperReady) {
71
+ lines.push(` speech-out Piper ready (${DEFAULT_PIPER_VOICE}, neural, plays via ${player}) · fallback: macOS \`say\``);
72
+ }
73
+ else if (rt.pythonPresent || rt.modelInstalled) {
74
+ lines.push(` speech-out Piper partly set up (${rt.pythonPresent ? 'runtime ok' : 'no runtime'}, ${rt.modelInstalled ? 'model ok' : 'no model'}) — finish with \`dexterm voice setup\` · now using macOS \`say\``);
75
+ }
76
+ else {
77
+ lines.push(' speech-out macOS `say` (built-in) · neural voice: run `dexterm voice setup` · see `dexterm doctor` for the resolved voice');
78
+ }
46
79
  lines.push(' speech-in Apple Speech (swiftc) preferred, else whisper.cpp');
47
80
  }
48
81
  else {
@@ -95,13 +128,55 @@ async function runDownload(io, which, yes) {
95
128
  }
96
129
  return anyFailed ? 1 : 0;
97
130
  }
131
+ /** `dexterm voice setup`: build the managed neural-voice runtime (venv + model) on this machine. */
132
+ async function runSetup(io, yes) {
133
+ const home = dextermHome(io.env);
134
+ if (!yes && !io.stdin.isTTY) {
135
+ io.stderr.write('dexterm voice setup: no interactive terminal to confirm; re-run with --yes to approve the setup.\n');
136
+ return 2;
137
+ }
138
+ // Let the user point setup at a specific interpreter (e.g. a pyenv/homebrew python) via DEXTERM_PYTHON.
139
+ const candidates = io.env.DEXTERM_PYTHON ? [io.env.DEXTERM_PYTHON] : undefined;
140
+ const res = await setupPiperRuntime({
141
+ home,
142
+ run: runCmd,
143
+ confirm: (prompt) => (yes ? Promise.resolve(true) : askYesNo(io, `\n${prompt}`)),
144
+ log: (s) => io.stdout.write(s + '\n'),
145
+ pythonCandidates: candidates,
146
+ });
147
+ if (res.status === 'ok' || res.status === 'already')
148
+ return 0;
149
+ if (res.status === 'cancelled')
150
+ return 1;
151
+ // no-python / failed: print the exact fix line.
152
+ io.stderr.write(`dexterm voice setup: ${res.status === 'no-python' ? 'no suitable Python 3 found' : 'setup failed'}.\n`);
153
+ if (res.fix)
154
+ io.stderr.write(` fix: ${res.fix}\n`);
155
+ return 1;
156
+ }
157
+ /** `dexterm voice uninstall`: remove the managed neural-voice runtime. */
158
+ async function runUninstall(io, purgeModel) {
159
+ const home = dextermHome(io.env);
160
+ const res = await uninstallPiperRuntime(home, { purgeModel });
161
+ if (res.status === 'absent') {
162
+ io.stdout.write('No managed neural-voice runtime to remove.\n');
163
+ return 0;
164
+ }
165
+ io.stdout.write(`Removed the managed neural-voice runtime${purgeModel ? ' and voice model' : ' (voice model kept — remove with --purge-model)'}.\n`);
166
+ return 0;
167
+ }
98
168
  /** `dexterm voice …`. Returns the process exit code. */
99
169
  export async function voiceCommand(argv, io) {
100
170
  const yes = argv.includes('--yes') || argv.includes('-y');
101
- const rest = argv.filter((a) => a !== '--yes' && a !== '-y');
171
+ const purgeModel = argv.includes('--purge-model');
172
+ const rest = argv.filter((a) => a !== '--yes' && a !== '-y' && a !== '--purge-model');
102
173
  const sub = rest[0];
103
174
  if (sub === undefined || sub === 'status')
104
175
  return printStatus(io);
176
+ if (sub === 'setup')
177
+ return runSetup(io, yes);
178
+ if (sub === 'uninstall' || sub === 'remove')
179
+ return runUninstall(io, purgeModel);
105
180
  if (sub === 'download') {
106
181
  const which = rest[1];
107
182
  if (!which) {
@@ -28,7 +28,7 @@ export function renderVoiceHud(c, v, t) {
28
28
  else {
29
29
  // idle but enabled: a quiet "ready" badge with the key hints.
30
30
  x = c.text(x, y, ' ◎ voice ', { fg: t.borderFocus, bg, b: true });
31
- x = c.text(x, y, `v talk · m ${v.muted ? 'unmute' : 'mute'} · , voice${voiceName}`, { fg: t.muted, bg });
31
+ x = c.text(x, y, `v talk · m ${v.muted ? 'unmute' : 'mute'} · , voice · < > rate${voiceName}`, { fg: t.muted, bg });
32
32
  }
33
33
  if (mute && v.mode !== 'off')
34
34
  x = c.text(x, y, mute, { fg: t.warn, bg, b: true });
@@ -4,11 +4,12 @@ export const FIELDS = {
4
4
  holders: (r) => r.holders, org: (r) => r.org, top10: (r) => r.top10, dev: (r) => r.dev,
5
5
  lplocked: (r) => r.lpLocked, bundled: (r) => r.bundled, snipers: (r) => r.snipers,
6
6
  cluster: (r) => r.cluster, larp: (r) => r.larp,
7
+ fresh: (r) => r.fresh, mintable: (r) => r.mintable, freezable: (r) => r.freezable,
7
8
  age: (r) => r.ageMin, m5: (r) => r.chg?.m5, h1: (r) => r.chg?.h1, h6: (r) => r.chg?.h6, h24: (r) => r.chg?.h24,
8
9
  buys: (r) => r.buys, sells: (r) => r.sells, bonding: (r) => r.bonding,
9
10
  };
10
11
  export const FIELD_NAMES = Object.keys(FIELDS);
11
- const ALIASES = { vol: 'vol24', volume: 'vol24', liquidity: 'liq', cap: 'mcap', mc: 'mcap', holder: 'holders', organic: 'org', top: 'top10', locked: 'lplocked', 'cluster%': 'cluster', clusterpct: 'cluster' };
12
+ const ALIASES = { vol: 'vol24', volume: 'vol24', liquidity: 'liq', cap: 'mcap', mc: 'mcap', holder: 'holders', organic: 'org', top: 'top10', locked: 'lplocked', 'cluster%': 'cluster', clusterpct: 'cluster', freshwallets: 'fresh', mintauth: 'mintable', freezeauth: 'freezable' };
12
13
  const OPS = { '>': (a, b) => a > b, '>=': (a, b) => a >= b, '<': (a, b) => a < b, '<=': (a, b) => a <= b, '=': (a, b) => a === b, '==': (a, b) => a === b, '!=': (a, b) => a !== b };
13
14
  const TAGS = {
14
15
  launchpad: (r, v) => (r.launchpad ?? '').toLowerCase().replace('.fun', '') === v.toLowerCase().replace('.fun', ''),
@@ -18,8 +18,53 @@ export const BASE58_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
18
18
  export const ACTION_FILE_ENV = 'DEXTERM_ACTION_FILE';
19
19
  /** Env var carrying the per-run random token that authenticates channel lines. */
20
20
  export const ACTION_TOKEN_ENV = 'DEXTERM_ACTION_TOKEN';
21
- /** The five non-financial UI actions the analyst may request. */
22
- export const ANALYST_ACTION_TYPES = ['open_token', 'show_in_screener', 'add_to_watchlist', 'switch_feed', 'open_ticket'];
21
+ /**
22
+ * The non-financial UI actions the analyst may request (task-1313, extended in task-1413).
23
+ *
24
+ * Two tiers (see `ACTION_TIER`):
25
+ * - `view` — local, reversible runtime changes applied immediately with an "analyst did X" note.
26
+ * - `config` — a persisted change to `config.json`. It is NOT applied by the model; the consumer turns
27
+ * it into a one-line PROPOSAL the user accepts with a keypress, then writes it through the
28
+ * journaled config path (`applyAndJournal`, actor `analyst`, so `ctrl+z` undoes it).
29
+ *
30
+ * FORBIDDEN forever (task-1413 tier c): there is deliberately NO action kind that touches wallets, keys,
31
+ * credentials, alert-channel secrets, signing/arming/caps/mandates, trade tickets beyond the existing
32
+ * PAPER `open_ticket`, network/RPC settings, or software install/update. Such a request fails here as an
33
+ * "unknown action type". The journaled config paths a `config` action may write are a closed list
34
+ * (`CONFIG_ACTION_PATHS`), AND the consumer re-runs `policyCheck(..., non-user actor)` on the journal op, so
35
+ * no `config` action may ever write a `DENIED_FOR_MODELS` path — none of the paths below is denied or a
36
+ * money/secret path.
37
+ *
38
+ * NOTE (task-1413 r2): voice rate / voice name are NOT analyst actions. `/voice` is `DENIED_FOR_MODELS`
39
+ * ("only the keyboard sets it", schema.ts), and the gate stays the gate: a model may never write it, even
40
+ * behind a keypress proposal. The analyst still EXPLAINS how to change the voice (app-help: edit config.json
41
+ * or the `,` key); it just cannot write it. Re-enabling it would need a maintainer-approved DENIED_FOR_MODELS carve-out.
42
+ */
43
+ export const ANALYST_ACTION_TYPES = [
44
+ // task-1313 (unchanged):
45
+ 'open_token', 'show_in_screener', 'add_to_watchlist', 'switch_feed', 'open_ticket',
46
+ // task-1413 view-only:
47
+ 'switch_screen', 'switch_workspace', 'chart_popup', 'toggle_calm', 'set_scan', 'export_view',
48
+ // task-1413 persisted config (user-confirmed, journaled; non-denied display paths only):
49
+ 'set_theme', 'set_price_mode', 'set_intensity',
50
+ ];
51
+ /** Tier of each action: `view` = immediate runtime, `config` = persisted via a user-confirmed journal write. */
52
+ export const ACTION_TIER = {
53
+ open_token: 'view', show_in_screener: 'view', add_to_watchlist: 'view', switch_feed: 'view', open_ticket: 'view',
54
+ switch_screen: 'view', switch_workspace: 'view', chart_popup: 'view', toggle_calm: 'view', set_scan: 'view', export_view: 'view',
55
+ set_theme: 'config', set_price_mode: 'config', set_intensity: 'config',
56
+ };
57
+ /**
58
+ * The exact `config.json` JSON-Pointer each `config` action writes — a closed allow-list. Every path here
59
+ * is a local, reversible, non-financial DISPLAY setting and NONE is a `DENIED_FOR_MODELS` path (the journal
60
+ * write re-checks that with `policyCheck`). NOTHING here is matched by `DENIED_VALUE_KEYS` / the money-safety
61
+ * sections; the consumer additionally confirms each with a keypress.
62
+ */
63
+ export const CONFIG_ACTION_PATHS = {
64
+ set_theme: '/theme/name',
65
+ set_price_mode: '/display/priceMode',
66
+ set_intensity: '/fx/intensity',
67
+ };
23
68
  /** Max actions the consumer should apply per rolling window, and the window length. Advisory shared constants. */
24
69
  export const ACTION_RATE = { max: 6, windowMs: 10_000 };
25
70
  const FILTER_MAX = 120;
@@ -27,6 +72,10 @@ const FILTER_MAX = 120;
27
72
  // `!` is included so tag negation (`!flag:mint`, `!launchpad:pump`) — a documented part of the grammar —
28
73
  // survives the boundary instead of being rejected as malformed (task-1313 part E).
29
74
  const FILTER_SAFE_RE = /^[\w\s<>=:.%$,+!\-|()]*$/;
75
+ // task-1413 value shapes. The consumer re-validates against config schema (`validateConfig`) too.
76
+ const THEME_NAME_RE = /^[a-z][a-z0-9-]{0,39}$/; // mirrors schema.ts NAME
77
+ const PRICE_MODES = ['price', 'mcap'];
78
+ const INTENSITIES = ['off', 'normal', 'fever'];
30
79
  function isObj(x) {
31
80
  return typeof x === 'object' && x !== null && !Array.isArray(x);
32
81
  }
@@ -94,14 +143,92 @@ export function validateAnalystAction(raw, allow) {
94
143
  out.highlight = raw.highlight;
95
144
  bits.push(`highlight ${short(raw.highlight)}`);
96
145
  }
146
+ if (raw.preset !== undefined && raw.preset !== null && raw.preset !== '') {
147
+ if (typeof raw.preset !== 'string' || !allow.feeds.includes(raw.preset)) {
148
+ return { ok: false, error: `show_in_screener: preset must be one of ${allow.feeds.join(', ') || '(none)'}` };
149
+ }
150
+ out.preset = raw.preset;
151
+ bits.push(`preset ${out.preset}`);
152
+ }
97
153
  if (!bits.length)
98
- return { ok: false, error: 'show_in_screener: nothing to do (give filter, sort, or highlight)' };
154
+ return { ok: false, error: 'show_in_screener: nothing to do (give filter, sort, preset, or highlight)' };
99
155
  return { ok: true, action: out, label: `showed the screener (${bits.join(', ')})` };
100
156
  }
157
+ case 'switch_screen': {
158
+ const screens = allow.screens ?? [];
159
+ if (typeof raw.screen !== 'string' || !screens.includes(raw.screen)) {
160
+ return { ok: false, error: `switch_screen: screen must be one of ${screens.join(', ') || '(none)'}` };
161
+ }
162
+ return { ok: true, action: { type, screen: raw.screen }, label: `switched to the ${raw.screen} screen` };
163
+ }
164
+ case 'switch_workspace': {
165
+ const workspaces = allow.workspaces ?? [];
166
+ if (typeof raw.name !== 'string' || !workspaces.includes(raw.name)) {
167
+ return { ok: false, error: `switch_workspace: name must be one of ${workspaces.join(', ') || '(none)'}` };
168
+ }
169
+ return { ok: true, action: { type, name: raw.name }, label: `opened the "${raw.name}" workspace` };
170
+ }
171
+ case 'chart_popup': {
172
+ if (typeof raw.open !== 'boolean')
173
+ return { ok: false, error: 'chart_popup: open must be true or false' };
174
+ return { ok: true, action: { type, open: raw.open }, label: raw.open ? 'opened the chart popup' : 'closed the chart popup' };
175
+ }
176
+ case 'toggle_calm': {
177
+ if (typeof raw.on !== 'boolean')
178
+ return { ok: false, error: 'toggle_calm: on must be true or false' };
179
+ return { ok: true, action: { type, on: raw.on }, label: raw.on ? 'turned on calm mode' : 'turned off calm mode' };
180
+ }
181
+ case 'set_scan': {
182
+ if (typeof raw.on !== 'boolean')
183
+ return { ok: false, error: 'set_scan: on must be true or false' };
184
+ return { ok: true, action: { type, on: raw.on }, label: raw.on ? 'turned scan mode on' : 'turned scan mode off' };
185
+ }
186
+ case 'export_view': {
187
+ return { ok: true, action: { type }, label: 'exported the current view' };
188
+ }
189
+ case 'set_theme': {
190
+ if (typeof raw.name !== 'string' || !THEME_NAME_RE.test(raw.name))
191
+ return { ok: false, error: 'set_theme: invalid theme name' };
192
+ const themes = allow.themes ?? [];
193
+ if (!themes.includes(raw.name))
194
+ return { ok: false, error: `set_theme: theme must be one of ${themes.join(', ') || '(none)'}` };
195
+ return { ok: true, action: { type, name: raw.name }, label: `set the theme to ${raw.name}` };
196
+ }
197
+ case 'set_price_mode': {
198
+ if (raw.mode !== 'price' && raw.mode !== 'mcap')
199
+ return { ok: false, error: `set_price_mode: mode must be one of ${PRICE_MODES.join(', ')}` };
200
+ return { ok: true, action: { type, mode: raw.mode }, label: `set the price display to ${raw.mode === 'mcap' ? 'market cap' : 'price'}` };
201
+ }
202
+ case 'set_intensity': {
203
+ if (typeof raw.intensity !== 'string' || !INTENSITIES.includes(raw.intensity)) {
204
+ return { ok: false, error: `set_intensity: intensity must be one of ${INTENSITIES.join(', ')}` };
205
+ }
206
+ return { ok: true, action: { type, intensity: raw.intensity }, label: `set the visual intensity to ${raw.intensity}` };
207
+ }
101
208
  default:
102
209
  return { ok: false, error: `unknown action type ${type}` };
103
210
  }
104
211
  }
212
+ /**
213
+ * Map a validated `config`-tier action to a `{section, key, value}` descriptor of the `config.json`
214
+ * change it requests. Returns `null` for a `view`-tier action (nothing to persist). Pure and
215
+ * dependency-free: the consumer builds the actual `PatchOp` (creating the parent section when it is
216
+ * absent, exactly like the `dexterm display` / `,` voice handlers), runs the config schema validator,
217
+ * and writes through `applyAndJournal` (actor `analyst`). `section`/`key` are fixed, never taken from
218
+ * model input — only `value` comes from the (already-validated) action.
219
+ */
220
+ export function configChangeForAction(action) {
221
+ const at = (path, value) => {
222
+ const [, section, key] = path.split('/');
223
+ return { section, key, value, path };
224
+ };
225
+ switch (action.type) {
226
+ case 'set_theme': return at(CONFIG_ACTION_PATHS.set_theme, action.name);
227
+ case 'set_price_mode': return at(CONFIG_ACTION_PATHS.set_price_mode, action.mode);
228
+ case 'set_intensity': return at(CONFIG_ACTION_PATHS.set_intensity, action.intensity);
229
+ default: return null;
230
+ }
231
+ }
105
232
  /** Serialize a channel line (newline-terminated). */
106
233
  export function serializeActionLine(token, seq, action) {
107
234
  const line = { v: 1, token, seq, ts: Date.now(), action };
@@ -159,7 +159,7 @@ export class FastLane {
159
159
  return false;
160
160
  if (this.tracked.size >= this.cap)
161
161
  return false; // over the cap: stay slow, never drop
162
- const t = { mint, priceAccount, lastTickAt: null, lastPrice: null, migrated: false };
162
+ const t = { mint, priceAccount, lastTickAt: null, lastPrice: null, lastCurve: null, lastCurveAt: null, migrated: false };
163
163
  this.tracked.set(mint, t);
164
164
  this.byAccount.set(priceAccount.curve, mint);
165
165
  this.ws?.subscribe(priceAccount.curve);
@@ -227,7 +227,13 @@ export class FastLane {
227
227
  if (!tracked)
228
228
  return;
229
229
  const curve = decodeBondingCurve(base64);
230
- if (!curve || curve.priceSol == null)
230
+ if (!curve)
231
+ return;
232
+ // task-1404: keep the decoded curve (reserves / complete) so the live-signals binding can read bonding
233
+ // progress and migration from the same push, not just the price.
234
+ tracked.lastCurve = curve;
235
+ tracked.lastCurveAt = t;
236
+ if (curve.priceSol == null)
231
237
  return;
232
238
  if (curve.complete) {
233
239
  // Migrated to a DEX pool: the bonding-curve price is stale. Demote to the slow lane honestly;
@@ -280,6 +286,14 @@ export class FastLane {
280
286
  price(mint) {
281
287
  return this.tracked.get(mint)?.lastPrice ?? null;
282
288
  }
289
+ /**
290
+ * Last decoded bonding curve for a mint and when it was decoded (task-1404). The live-signals binding reads
291
+ * bonding progress + `complete` from it. null when the mint is not tracked or no push/seed has decoded yet.
292
+ */
293
+ curve(mint) {
294
+ const t = this.tracked.get(mint);
295
+ return t && t.lastCurve && t.lastCurveAt != null ? { curve: t.lastCurve, at: t.lastCurveAt } : null;
296
+ }
283
297
  /**
284
298
  * Last fast-lane USD price, recomputed from the stored on-chain SOL price and the *current* SOL/USD —
285
299
  * so a tick that arrived before SOL/USD was known still shows a USD value once the rate lands. Null
@@ -57,6 +57,7 @@ export * from "./intel/rules.js";
57
57
  export * from "./intel/holder-class.js";
58
58
  export * from "./intel/helius-history.js";
59
59
  export * from "./intel/scan.js";
60
+ export * from "./intel/live-signals.js";
60
61
  export * from "./images/index.js";
61
62
  // task-1330 (I-03): social / news / trending layer. `Provenance` and `Credibility`-style names are
62
63
  // namespaced under `Social*` where they would collide with the core model.