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
|
@@ -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.
|
|
3
|
+
export const VERSION = '0.1.3';
|
|
@@ -1,25 +1,47 @@
|
|
|
1
|
-
// `dexterm voice …` (task-1408): inspect the voice engines on this machine
|
|
2
|
-
// optional voice models — whisper.cpp base.en (speech-in) and the Piper en_GB-alan-medium voice
|
|
3
|
-
//
|
|
4
|
-
// it asks before it does, and it verifies the
|
|
5
|
-
// source. Nothing here runs the signer, touches
|
|
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,
|
|
8
|
-
// checksum/stream logic lives in @dexterm/voice
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
22
|
-
|
|
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
|
|
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.
|