dexterm-cli 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/packages/agents/src/bus.js +57 -6
- package/dist/packages/agents/src/executor.js +25 -0
- package/dist/packages/agents/src/follow.js +105 -0
- package/dist/packages/agents/src/gate.js +3 -0
- package/dist/packages/agents/src/identity.js +41 -2
- package/dist/packages/agents/src/index.js +5 -0
- package/dist/packages/agents/src/spectator-server.js +153 -0
- package/dist/packages/agents/src/spectator.js +212 -0
- package/dist/packages/agents/src/tiers.js +5 -0
- package/dist/packages/agents/src/tools.js +3 -0
- package/dist/packages/cli/assets/claude/analyst-system.md +6 -1
- package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +11 -3
- package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +13 -3
- package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +3 -3
- package/dist/packages/cli/src/agent-cmd.js +107 -10
- package/dist/packages/cli/src/agent-spectator.js +37 -0
- package/dist/packages/cli/src/alerts/adapters/desktop.js +40 -0
- package/dist/packages/cli/src/alerts/adapters/discord.js +36 -0
- package/dist/packages/cli/src/alerts/adapters/telegram.js +43 -0
- package/dist/packages/cli/src/alerts/adapters/toast.js +16 -0
- package/dist/packages/cli/src/alerts/creds.js +83 -0
- package/dist/packages/cli/src/alerts/format.js +66 -0
- package/dist/packages/cli/src/alerts/index.js +10 -0
- package/dist/packages/cli/src/alerts/notifier.js +53 -0
- package/dist/packages/cli/src/alerts/route.js +19 -0
- package/dist/packages/cli/src/alerts/types.js +1 -0
- package/dist/packages/cli/src/alerts-cmd.js +212 -0
- package/dist/packages/cli/src/app-help-gen.js +365 -0
- package/dist/packages/cli/src/backtest-cmd.js +62 -3
- package/dist/packages/cli/src/coin-target.js +2 -0
- package/dist/packages/cli/src/doctor.js +21 -6
- package/dist/packages/cli/src/export.js +235 -0
- package/dist/packages/cli/src/follow-cmd.js +107 -1
- package/dist/packages/cli/src/help-cmd.js +48 -0
- package/dist/packages/cli/src/live/index.js +66 -3
- package/dist/packages/cli/src/live/screens/token-live.js +24 -0
- package/dist/packages/cli/src/live/signals.js +109 -0
- package/dist/packages/cli/src/live/trade.js +24 -1
- package/dist/packages/cli/src/main.js +596 -49
- package/dist/packages/cli/src/mcp-cmd.js +321 -0
- package/dist/packages/cli/src/pnl-app.js +221 -0
- package/dist/packages/cli/src/pnl-card.js +615 -0
- package/dist/packages/cli/src/record-cmd.js +455 -0
- package/dist/packages/cli/src/rules.js +69 -2
- package/dist/packages/cli/src/screens/follows.js +91 -2
- package/dist/packages/cli/src/screens/overlay.js +3 -0
- package/dist/packages/cli/src/screens/positions.js +1 -1
- package/dist/packages/cli/src/screens/rules.js +1 -1
- package/dist/packages/cli/src/skill-eval-build.js +109 -0
- package/dist/packages/cli/src/spectate-cmd.js +104 -0
- package/dist/packages/cli/src/state.js +1 -1
- package/dist/packages/cli/src/update.js +294 -0
- package/dist/packages/cli/src/version.js +3 -0
- package/dist/packages/cli/src/voice-cmd.js +194 -0
- package/dist/packages/cli/src/voice-hud.js +1 -1
- package/dist/packages/cli/src/watch-cmd.js +190 -0
- package/dist/packages/custom/src/rules.js +3 -2
- package/dist/packages/custom/src/schema.js +24 -1
- package/dist/packages/data/src/analyst-actions.js +130 -3
- package/dist/packages/data/src/coin-target.js +82 -0
- package/dist/packages/data/src/fast-lane.js +16 -2
- package/dist/packages/data/src/index.js +4 -0
- package/dist/packages/data/src/intel/helius-history.js +12 -1
- package/dist/packages/data/src/intel/live-signals.js +185 -0
- package/dist/packages/data/src/larp/link-baseline.js +196 -0
- package/dist/packages/data/src/sources/dexscreener.js +14 -0
- package/dist/packages/data/src/sources/pumpportal.js +6 -1
- package/dist/packages/data/src/trade/copy/index.js +1 -1
- package/dist/packages/data/src/trade/copy/stats.js +36 -2
- package/dist/packages/data/src/trade/copy/tracker.js +3 -2
- package/dist/packages/mcp/assets/kb/app-help/app-help-command.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/ask-the-analyst.md +43 -0
- package/dist/packages/mcp/assets/kb/app-help/backtest.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/bubble-map.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/calm-mode.md +31 -0
- package/dist/packages/mcp/assets/kb/app-help/change-voice-speed.md +31 -0
- package/dist/packages/mcp/assets/kb/app-help/change-voice.md +30 -0
- package/dist/packages/mcp/assets/kb/app-help/chart-popup-selftest.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/coin-images.md +29 -0
- package/dist/packages/mcp/assets/kb/app-help/configure-rpc-key.md +28 -0
- package/dist/packages/mcp/assets/kb/app-help/connect-an-agent-registry.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/connect-an-agent.md +39 -0
- package/dist/packages/mcp/assets/kb/app-help/custom-keymap.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/edit-kb-or-skill.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/edit-rules.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/export-a-view.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/follow-a-wallet.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/getting-started.md +37 -0
- package/dist/packages/mcp/assets/kb/app-help/kol-lists.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/larp-check.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/link-a-wallet.md +41 -0
- package/dist/packages/mcp/assets/kb/app-help/login-with-x.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/narrative-board.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/open-a-coin.md +43 -0
- package/dist/packages/mcp/assets/kb/app-help/open-chart-popup.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/order-plans.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/paper-trade.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/price-or-marketcap.md +38 -0
- package/dist/packages/mcp/assets/kb/app-help/record-headless.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/recorder-history.md +39 -0
- package/dist/packages/mcp/assets/kb/app-help/rug-check.md +39 -0
- package/dist/packages/mcp/assets/kb/app-help/run-doctor.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/scan-mode.md +37 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-analyst.md +181 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-kol.md +190 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-newpairs.md +181 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +180 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-screener.md +199 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-token.md +240 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-trending.md +192 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-watchlist.md +181 -0
- package/dist/packages/mcp/assets/kb/app-help/set-a-filter.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/setup-neural-voice.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/share-packs.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/sort-the-screener.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/spectate-an-agent.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/standing-mandates.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/switch-theme.md +42 -0
- package/dist/packages/mcp/assets/kb/app-help/turn-on-alerts.md +37 -0
- package/dist/packages/mcp/assets/kb/app-help/update-dexterm.md +30 -0
- package/dist/packages/mcp/assets/kb/app-help/use-presets.md +42 -0
- package/dist/packages/mcp/assets/kb/app-help/use-voice-mode.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/watch-a-coin.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/watch-headless.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/what-dexterm-shows-agents.md +31 -0
- package/dist/packages/mcp/assets/kb/app-help/workspaces.md +38 -0
- package/dist/packages/mcp/src/app-help.js +135 -0
- package/dist/packages/mcp/src/index.js +3 -0
- package/dist/packages/mcp/src/server.js +179 -5
- package/dist/packages/mcp/src/spectator-sink.js +72 -0
- package/dist/packages/packs/src/index.js +3 -0
- package/dist/packages/packs/src/skill-backtest.js +300 -0
- package/dist/packages/packs/src/skill-care.js +206 -0
- package/dist/packages/popup/src/pages/spectator.css +45 -0
- package/dist/packages/popup/src/pages/spectator.html +25 -0
- package/dist/packages/popup/src/pages/spectator.js +74 -0
- package/dist/packages/popup/src/selftest.js +19 -0
- package/dist/packages/popup/src/server.js +7 -1
- package/dist/packages/tui/src/index.js +3 -0
- package/dist/packages/tui/src/keymap.js +12 -0
- package/dist/packages/tui/src/raster-font.js +195 -0
- package/dist/packages/tui/src/raster.js +213 -0
- package/dist/packages/voice/src/controller.js +57 -4
- package/dist/packages/voice/src/doctor.js +96 -11
- package/dist/packages/voice/src/download.js +184 -0
- package/dist/packages/voice/src/engines.js +55 -8
- package/dist/packages/voice/src/index.js +9 -3
- package/dist/packages/voice/src/speakable.js +111 -0
- package/dist/packages/voice/src/stt/whisper.js +14 -4
- package/dist/packages/voice/src/tts/espeak.js +131 -0
- package/dist/packages/voice/src/tts/piper-setup.js +229 -0
- package/dist/packages/voice/src/tts/piper.js +449 -0
- package/dist/packages/voice/src/tts/say.js +72 -35
- package/dist/packages/voice/src/tts/voices.js +209 -0
- package/package.json +5 -9
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// task-1397 — `dexterm update` and the quiet startup notice.
|
|
2
|
+
//
|
|
3
|
+
// The check is two anonymous GETs: the npm registry's `latest` for dexterm-cli and GitHub's latest published
|
|
4
|
+
// release of dexterm-xyz/dexterm. Both carry only fixed headers (no version, no OS, no ids, no cookies); nothing
|
|
5
|
+
// about the user or the machine is sent beyond the IP any request has. No telemetry.
|
|
6
|
+
//
|
|
7
|
+
// Startup: a live interactive run reads ~/.dexterm/update-check.json (no network) and shows one toast when a
|
|
8
|
+
// newer version was seen; at most once a day it refreshes that file in the background (never blocks, never in
|
|
9
|
+
// --fixtures/--replay, never with DEXTERM_OFFLINE/CI). Off switches: `dexterm update --off`,
|
|
10
|
+
// DEXTERM_NO_UPDATE_CHECK=1 or NO_UPDATE_NOTIFIER=1. `dexterm update` itself never upgrades anything: it prints
|
|
11
|
+
// the exact command for how this copy was installed (npm, Homebrew, release tarball, git checkout).
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { dirname, join, resolve } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { dextermHome } from "./boot-state.js";
|
|
16
|
+
import { VERSION } from "./version.js";
|
|
17
|
+
export const NPM_PACKAGE = 'dexterm-cli';
|
|
18
|
+
export const GITHUB_REPO = 'dexterm-xyz/dexterm';
|
|
19
|
+
export const NPM_LATEST_URL = `https://registry.npmjs.org/${NPM_PACKAGE}/latest`;
|
|
20
|
+
export const GITHUB_LATEST_URL = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`;
|
|
21
|
+
export const RELEASES_URL = `https://github.com/${GITHUB_REPO}/releases`;
|
|
22
|
+
/** Fixed request headers: the same bytes for every user (GitHub's API requires a user-agent). */
|
|
23
|
+
export const REQUEST_HEADERS = Object.freeze({ accept: 'application/json', 'user-agent': 'dexterm' });
|
|
24
|
+
export const CHECK_EVERY_MS = 24 * 60 * 60 * 1000;
|
|
25
|
+
export const REQUEST_TIMEOUT_MS = 4000;
|
|
26
|
+
export const UPDATE_FILE = 'update-check.json';
|
|
27
|
+
export const UPDATE_USAGE = `usage: dexterm update check npm + GitHub now and print the upgrade command for this install
|
|
28
|
+
dexterm update --off turn the once-a-day startup check off (dexterm update still works)
|
|
29
|
+
dexterm update --on turn it back on`;
|
|
30
|
+
/** `1.2.3`, `v1.2.3`, `1.2.3-rc.1` → parts; anything else → null (never guessed). */
|
|
31
|
+
export function parseVersion(s) {
|
|
32
|
+
if (typeof s !== 'string')
|
|
33
|
+
return null;
|
|
34
|
+
const m = /^v?(\d{1,6})\.(\d{1,6})\.(\d{1,6})(?:-([0-9A-Za-z.-]{1,64}))?(?:\+[0-9A-Za-z.-]{1,64})?$/.exec(s.trim());
|
|
35
|
+
if (!m)
|
|
36
|
+
return null;
|
|
37
|
+
return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]), pre: m[4] ? m[4].split('.') : [] };
|
|
38
|
+
}
|
|
39
|
+
/** Semver precedence: < 0 when a is older than b. Unparseable versions compare equal (no notice on junk). */
|
|
40
|
+
export function compareVersions(a, b) {
|
|
41
|
+
const x = parseVersion(a), y = parseVersion(b);
|
|
42
|
+
if (!x || !y)
|
|
43
|
+
return 0;
|
|
44
|
+
for (const k of ['major', 'minor', 'patch'])
|
|
45
|
+
if (x[k] !== y[k])
|
|
46
|
+
return x[k] - y[k];
|
|
47
|
+
if (!x.pre.length || !y.pre.length)
|
|
48
|
+
return y.pre.length - x.pre.length; // a release outranks its prereleases
|
|
49
|
+
for (let i = 0; i < Math.max(x.pre.length, y.pre.length); i++) {
|
|
50
|
+
const p = x.pre[i], q = y.pre[i];
|
|
51
|
+
if (p === undefined)
|
|
52
|
+
return -1;
|
|
53
|
+
if (q === undefined)
|
|
54
|
+
return 1;
|
|
55
|
+
const pn = /^\d+$/.test(p), qn = /^\d+$/.test(q);
|
|
56
|
+
if (pn && qn) {
|
|
57
|
+
if (Number(p) !== Number(q))
|
|
58
|
+
return Number(p) - Number(q);
|
|
59
|
+
}
|
|
60
|
+
else if (pn !== qn)
|
|
61
|
+
return pn ? -1 : 1;
|
|
62
|
+
else if (p !== q)
|
|
63
|
+
return p < q ? -1 : 1;
|
|
64
|
+
}
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
const bare = (v) => v.trim().replace(/^v/, '');
|
|
68
|
+
/** This module's real path (symlinks resolved): …/node_modules/dexterm-cli/dist/…, …/Cellar/dexterm/…, or a source tree. */
|
|
69
|
+
export function selfPath() {
|
|
70
|
+
const p = fileURLToPath(import.meta.url);
|
|
71
|
+
try {
|
|
72
|
+
return realpathSync(p);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return p;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** The source tree's root for a tarball/checkout install (this file is packages/cli/src/update.ts). */
|
|
79
|
+
export function sourceRoot(path = selfPath()) {
|
|
80
|
+
return resolve(dirname(path), '..', '..', '..');
|
|
81
|
+
}
|
|
82
|
+
export function detectInstall(path = selfPath(), exists = existsSync) {
|
|
83
|
+
const p = path.replace(/\\/g, '/');
|
|
84
|
+
if (p.includes(`/node_modules/${NPM_PACKAGE}/`))
|
|
85
|
+
return 'npm';
|
|
86
|
+
if (/\/Cellar\/dexterm\/|\/opt\/homebrew\/|\/home\/linuxbrew\/\.linuxbrew\//.test(p))
|
|
87
|
+
return 'brew';
|
|
88
|
+
return exists(join(sourceRoot(path), '.git')) ? 'checkout' : 'tarball';
|
|
89
|
+
}
|
|
90
|
+
export const INSTALL_LABEL = {
|
|
91
|
+
npm: `npm (${NPM_PACKAGE})`, brew: 'Homebrew (dexterm-xyz/dexterm tap)', tarball: 'release tarball (install.sh)', checkout: 'git checkout',
|
|
92
|
+
};
|
|
93
|
+
/** The exact commands that move this install to `latest`. Nothing here is ever run by dexterm. */
|
|
94
|
+
export function upgradeCommands(kind, latest, opts = {}) {
|
|
95
|
+
const v = bare(latest);
|
|
96
|
+
if (kind === 'npm')
|
|
97
|
+
return [`npm i -g ${NPM_PACKAGE}@${v}`];
|
|
98
|
+
if (kind === 'brew')
|
|
99
|
+
return ['brew update', 'brew upgrade dexterm'];
|
|
100
|
+
if (kind === 'checkout')
|
|
101
|
+
return [`git -C ${shellQuote(opts.root ?? sourceRoot())} pull --ff-only`];
|
|
102
|
+
const base = `${RELEASES_URL}/download/v${v}`;
|
|
103
|
+
const file = `dexterm-${v}-src.tar.gz`;
|
|
104
|
+
const sum = (opts.platform ?? process.platform) === 'darwin' ? 'shasum -a 256 -c' : 'sha256sum -c';
|
|
105
|
+
return [
|
|
106
|
+
`curl -fL -O ${base}/${file} -O ${base}/SHA256SUMS`,
|
|
107
|
+
`grep ' ${file}$' SHA256SUMS | ${sum}`,
|
|
108
|
+
`tar -xzf ${file} && cd dexterm-${v} && ./install.sh`,
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
function shellQuote(s) {
|
|
112
|
+
return /^[\w@%+=:,./-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`;
|
|
113
|
+
}
|
|
114
|
+
async function getJson(fetchFn, url, timeoutMs, cancel) {
|
|
115
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
116
|
+
const res = await fetchFn(url, { headers: { ...REQUEST_HEADERS }, signal: cancel ? AbortSignal.any([cancel, timeout]) : timeout, redirect: 'follow' });
|
|
117
|
+
if (!res.ok)
|
|
118
|
+
throw new Error(`HTTP ${res.status}`);
|
|
119
|
+
return res.json();
|
|
120
|
+
}
|
|
121
|
+
function why(e) {
|
|
122
|
+
const err = e;
|
|
123
|
+
if (err?.name === 'TimeoutError' || err?.name === 'AbortError')
|
|
124
|
+
return 'timed out';
|
|
125
|
+
return String(err?.message ?? e).slice(0, 120);
|
|
126
|
+
}
|
|
127
|
+
/** Ask both sources. Never throws; each side reports its own error. */
|
|
128
|
+
export async function fetchLatest(fetchFn = fetch, timeoutMs = REQUEST_TIMEOUT_MS, cancel) {
|
|
129
|
+
const npm = getJson(fetchFn, NPM_LATEST_URL, timeoutMs, cancel).then((b) => {
|
|
130
|
+
const v = b?.version;
|
|
131
|
+
return parseVersion(v) ? { ok: true, version: bare(v), url: `https://www.npmjs.com/package/${NPM_PACKAGE}` } : { ok: false, error: 'no version in the registry reply' };
|
|
132
|
+
}, (e) => ({ ok: false, error: why(e) }));
|
|
133
|
+
const github = getJson(fetchFn, GITHUB_LATEST_URL, timeoutMs, cancel).then((b) => {
|
|
134
|
+
const r = b;
|
|
135
|
+
if (r?.draft || r?.prerelease || !parseVersion(r?.tag_name))
|
|
136
|
+
return { ok: false, error: 'no published release in the reply' };
|
|
137
|
+
const tag = r.tag_name;
|
|
138
|
+
return { ok: true, version: bare(tag), url: `${RELEASES_URL}/tag/${tag}` };
|
|
139
|
+
}, (e) => ({ ok: false, error: why(e) }));
|
|
140
|
+
return { npm: await npm, github: await github };
|
|
141
|
+
}
|
|
142
|
+
/** The newest stable version either source reported, or null. */
|
|
143
|
+
export function newestOf(r) {
|
|
144
|
+
let best = null;
|
|
145
|
+
for (const s of [r.npm, r.github]) {
|
|
146
|
+
if (!s.ok || parseVersion(s.version)?.pre.length)
|
|
147
|
+
continue;
|
|
148
|
+
if (best === null || compareVersions(s.version, best) > 0)
|
|
149
|
+
best = s.version;
|
|
150
|
+
}
|
|
151
|
+
return best;
|
|
152
|
+
}
|
|
153
|
+
/** The version this install's channel can actually move to: npm → the registry; the rest → the GitHub release. */
|
|
154
|
+
export function targetFor(kind, r) {
|
|
155
|
+
const primary = kind === 'npm' ? r.npm : r.github;
|
|
156
|
+
return primary.ok ? primary.version : newestOf(r);
|
|
157
|
+
}
|
|
158
|
+
export function updateStatePath(env) {
|
|
159
|
+
return join(dextermHome(env), UPDATE_FILE);
|
|
160
|
+
}
|
|
161
|
+
export function readUpdateState(env) {
|
|
162
|
+
try {
|
|
163
|
+
const j = JSON.parse(readFileSync(updateStatePath(env), 'utf8'));
|
|
164
|
+
return {
|
|
165
|
+
enabled: j.enabled !== false,
|
|
166
|
+
checkedAt: typeof j.checkedAt === 'number' && Number.isFinite(j.checkedAt) ? j.checkedAt : null,
|
|
167
|
+
latest: typeof j.latest === 'string' && parseVersion(j.latest) ? bare(j.latest) : null,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return { enabled: true, checkedAt: null, latest: null };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
export function writeUpdateState(env, s) {
|
|
175
|
+
const p = updateStatePath(env);
|
|
176
|
+
mkdirSync(dirname(p), { recursive: true, mode: 0o700 });
|
|
177
|
+
const tmp = `${p}.${process.pid}.tmp`;
|
|
178
|
+
writeFileSync(tmp, JSON.stringify(s, null, 2) + '\n', { mode: 0o600 });
|
|
179
|
+
renameSync(tmp, p);
|
|
180
|
+
}
|
|
181
|
+
const truthy = (v) => !!v && v !== '0' && v.toLowerCase() !== 'false';
|
|
182
|
+
/** Why the startup check is off, or null when it may run. `dexterm update` itself ignores all but DEXTERM_OFFLINE. */
|
|
183
|
+
export function startupCheckOff(env, s) {
|
|
184
|
+
if (truthy(env.DEXTERM_NO_UPDATE_CHECK))
|
|
185
|
+
return 'DEXTERM_NO_UPDATE_CHECK is set';
|
|
186
|
+
if (truthy(env.NO_UPDATE_NOTIFIER))
|
|
187
|
+
return 'NO_UPDATE_NOTIFIER is set';
|
|
188
|
+
if (truthy(env.DEXTERM_OFFLINE))
|
|
189
|
+
return 'DEXTERM_OFFLINE is set';
|
|
190
|
+
if (truthy(env.CI))
|
|
191
|
+
return 'CI is set';
|
|
192
|
+
if (!s.enabled)
|
|
193
|
+
return 'turned off with dexterm update --off';
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
/** The one-line notice for a cached result, or null. */
|
|
197
|
+
export function noticeFor(current, latest) {
|
|
198
|
+
if (!latest || parseVersion(latest)?.pre.length || compareVersions(latest, current) <= 0)
|
|
199
|
+
return null;
|
|
200
|
+
return `dexterm ${bare(latest)} is available (you have ${bare(current)}) · run dexterm update`;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Startup: the cached notice now, plus a background refresh when the last check is older than a day.
|
|
204
|
+
* The caller decides where the notice goes (the interactive loop shows it as a toast after the boot).
|
|
205
|
+
*/
|
|
206
|
+
export function startupCheck(env, opts = {}) {
|
|
207
|
+
const s = readUpdateState(env);
|
|
208
|
+
const none = () => { };
|
|
209
|
+
if (startupCheckOff(env, s))
|
|
210
|
+
return { notice: null, refresh: null, cancel: none };
|
|
211
|
+
const current = opts.current ?? VERSION;
|
|
212
|
+
const now = opts.now ?? Date.now();
|
|
213
|
+
const notice = noticeFor(current, s.latest);
|
|
214
|
+
if (s.checkedAt !== null && now - s.checkedAt >= 0 && now - s.checkedAt < CHECK_EVERY_MS)
|
|
215
|
+
return { notice, refresh: null, cancel: none };
|
|
216
|
+
const ctl = new AbortController();
|
|
217
|
+
const refresh = fetchLatest(opts.fetchFn, REQUEST_TIMEOUT_MS, ctl.signal).then((r) => {
|
|
218
|
+
if (ctl.signal.aborted || (!r.npm.ok && !r.github.ok))
|
|
219
|
+
return null; // quit / offline / blocked: try again next launch
|
|
220
|
+
const latest = newestOf(r);
|
|
221
|
+
try {
|
|
222
|
+
writeUpdateState(env, { ...s, checkedAt: now, latest });
|
|
223
|
+
}
|
|
224
|
+
catch { /* read-only home: no cache */ }
|
|
225
|
+
// only a notice the cache did not already produce
|
|
226
|
+
const n = noticeFor(current, latest);
|
|
227
|
+
return n && n !== notice ? n : null;
|
|
228
|
+
}).catch(() => null);
|
|
229
|
+
return { notice, refresh, cancel: () => ctl.abort() };
|
|
230
|
+
}
|
|
231
|
+
export async function updateCommand(argv, io, deps = {}) {
|
|
232
|
+
const [arg, ...rest] = argv;
|
|
233
|
+
const env = io.env;
|
|
234
|
+
if (arg === '-h' || arg === '--help' || arg === 'help') {
|
|
235
|
+
io.stdout.write(UPDATE_USAGE + '\n');
|
|
236
|
+
return 0;
|
|
237
|
+
}
|
|
238
|
+
if (rest.length || (arg !== undefined && arg !== '--off' && arg !== '--on')) {
|
|
239
|
+
io.stderr.write(`dexterm update: unknown argument ${JSON.stringify(String(rest[0] ?? arg).slice(0, 20))}\n${UPDATE_USAGE}\n`);
|
|
240
|
+
return 2;
|
|
241
|
+
}
|
|
242
|
+
const state = readUpdateState(env);
|
|
243
|
+
if (arg === '--off' || arg === '--on') {
|
|
244
|
+
try {
|
|
245
|
+
writeUpdateState(env, { ...state, enabled: arg === '--on' });
|
|
246
|
+
}
|
|
247
|
+
catch (e) {
|
|
248
|
+
io.stderr.write(`dexterm update: ${e.message}\n`);
|
|
249
|
+
return 1;
|
|
250
|
+
}
|
|
251
|
+
io.stdout.write(arg === '--off'
|
|
252
|
+
? `startup update check: off (saved to ${updateStatePath(env)}). dexterm update still checks when you run it.\n`
|
|
253
|
+
: `startup update check: on (at most once a day; saved to ${updateStatePath(env)})\n`);
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
if (truthy(env.DEXTERM_OFFLINE)) {
|
|
257
|
+
io.stderr.write('dexterm update: DEXTERM_OFFLINE is set; the check needs the network\n');
|
|
258
|
+
return 2;
|
|
259
|
+
}
|
|
260
|
+
const current = deps.current ?? VERSION;
|
|
261
|
+
const kind = deps.install ?? detectInstall();
|
|
262
|
+
const r = await fetchLatest(deps.fetchFn);
|
|
263
|
+
const line = (label, s) => ` ${label.padEnd(20)} ${s.ok ? `${s.version.padEnd(10)} ${s.url}` : `unavailable (${s.error})`}\n`;
|
|
264
|
+
io.stdout.write(`dexterm ${current} · installed via ${INSTALL_LABEL[kind]}\n`);
|
|
265
|
+
io.stdout.write(line(`npm ${NPM_PACKAGE}`, r.npm));
|
|
266
|
+
io.stdout.write(line('GitHub release', r.github));
|
|
267
|
+
if (!r.npm.ok && !r.github.ok) {
|
|
268
|
+
io.stderr.write(`dexterm update: could not reach npm or GitHub; check your connection, or see ${RELEASES_URL}\n`);
|
|
269
|
+
return 1;
|
|
270
|
+
}
|
|
271
|
+
const latest = newestOf(r);
|
|
272
|
+
try {
|
|
273
|
+
writeUpdateState(env, { ...state, checkedAt: deps.now ?? Date.now(), latest });
|
|
274
|
+
}
|
|
275
|
+
catch { /* read-only home */ }
|
|
276
|
+
const target = targetFor(kind, r);
|
|
277
|
+
if (!target || compareVersions(target, current) <= 0) {
|
|
278
|
+
const ahead = latest && compareVersions(latest, current) > 0;
|
|
279
|
+
io.stdout.write(ahead
|
|
280
|
+
? `You are on the newest version ${kind === 'npm' ? 'npm has' : 'the GitHub release has'} (${current}); ${latest} is out on the other channel and reaches yours shortly.\n`
|
|
281
|
+
: `You are up to date (${current}).\n`);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
io.stdout.write(`\ndexterm ${target} is available. Upgrade with:\n\n`);
|
|
285
|
+
for (const c of upgradeCommands(kind, target, { platform: deps.platform, root: deps.root }))
|
|
286
|
+
io.stdout.write(` ${c}\n`);
|
|
287
|
+
io.stdout.write('\n');
|
|
288
|
+
if (kind === 'tarball')
|
|
289
|
+
io.stdout.write(`Verify the download first: ${RELEASES_URL}/tag/v${target} (docs/verify-your-download.md). Release notes: CHANGELOG.md.\n`);
|
|
290
|
+
}
|
|
291
|
+
const off = startupCheckOff(env, readUpdateState(env));
|
|
292
|
+
io.stdout.write(`startup check: ${off ? `off (${off})` : 'on, at most once a day; dexterm update --off or DEXTERM_NO_UPDATE_CHECK=1 turns it off'}. No telemetry: two anonymous GETs (registry.npmjs.org, api.github.com).\n`);
|
|
293
|
+
return 0;
|
|
294
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
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.
|
|
7
|
+
//
|
|
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.
|
|
10
|
+
import { createInterface } from 'node:readline';
|
|
11
|
+
import { spawn } from 'node:child_process';
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { modelRegistry, downloadModel, modelStatus, resolvePiperBin, pickPlayer, PIPER_MODEL, DEFAULT_PIPER_VOICE, setupPiperRuntime, uninstallPiperRuntime, piperRuntimeStatus, } from '../../voice/src/index.js';
|
|
15
|
+
import { dextermHome } from '../../custom/src/index.js';
|
|
16
|
+
export const VOICE_USAGE = [
|
|
17
|
+
'usage:',
|
|
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',
|
|
21
|
+
' dexterm voice download <model> [--yes] download a voice model (asks first; checksum-verified)',
|
|
22
|
+
'',
|
|
23
|
+
' models: whisper whisper.cpp base.en (speech-to-text, off macOS this is the default)',
|
|
24
|
+
' piper-voice Piper en_GB-alan-medium (neural speech-out, the default speaking voice)',
|
|
25
|
+
' all both of the above',
|
|
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
|
+
}
|
|
45
|
+
function onPath(bin, env) {
|
|
46
|
+
const sep = process.platform === 'win32' ? ';' : ':';
|
|
47
|
+
for (const p of (env.PATH ?? '').split(sep)) {
|
|
48
|
+
if (p && existsSync(join(p, bin)))
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
/** y/N prompt on a TTY. Without a TTY (piped/CI) it returns false — the caller tells the user to pass --yes. */
|
|
54
|
+
function askYesNo(io, question) {
|
|
55
|
+
if (!io.stdin.isTTY)
|
|
56
|
+
return Promise.resolve(false);
|
|
57
|
+
const rl = createInterface({ input: io.stdin, output: io.stdout });
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
rl.question(`${question} [y/N] `, (ans) => { rl.close(); resolve(/^y(es)?$/i.test(ans.trim())); });
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async function printStatus(io) {
|
|
63
|
+
const home = dextermHome(io.env);
|
|
64
|
+
const reg = modelRegistry(home);
|
|
65
|
+
const lines = ['dexterm voice', ''];
|
|
66
|
+
if (process.platform === 'darwin') {
|
|
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
|
+
}
|
|
79
|
+
lines.push(' speech-in Apple Speech (swiftc) preferred, else whisper.cpp');
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
const piperBin = resolvePiperBin();
|
|
83
|
+
const player = pickPlayer();
|
|
84
|
+
const piperReady = piperBin !== null && existsSync(PIPER_MODEL) && player !== null;
|
|
85
|
+
lines.push(` speech-out Piper ${piperReady ? `ready (${DEFAULT_PIPER_VOICE}, plays via ${player})` : 'not ready'}${piperReady ? '' : ` · espeak-ng ${onPath('espeak-ng', io.env) || onPath('espeak', io.env) ? 'ready (baseline)' : 'not installed'}`}`);
|
|
86
|
+
lines.push(` speech-in whisper.cpp (default off macOS) · mic tool ${['arecord', 'sox', 'rec', 'ffmpeg'].find((b) => onPath(b, io.env)) ?? 'none found'}`);
|
|
87
|
+
}
|
|
88
|
+
lines.push('', ' models:');
|
|
89
|
+
for (const spec of Object.values(reg)) {
|
|
90
|
+
const st = await modelStatus(spec);
|
|
91
|
+
lines.push(` ${st.installed ? '✓' : '·'} ${spec.id.padEnd(12)} ${spec.label}${st.installed ? ' (installed)' : ''}`);
|
|
92
|
+
}
|
|
93
|
+
lines.push('', ` download a model: dexterm voice download <whisper|piper-voice|all>`);
|
|
94
|
+
io.stdout.write(lines.join('\n') + '\n');
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
async function runDownload(io, which, yes) {
|
|
98
|
+
const home = dextermHome(io.env);
|
|
99
|
+
const reg = modelRegistry(home);
|
|
100
|
+
const ids = which === 'all' ? Object.keys(reg) : [which];
|
|
101
|
+
const specs = [];
|
|
102
|
+
for (const id of ids) {
|
|
103
|
+
const spec = reg[id];
|
|
104
|
+
if (!spec) {
|
|
105
|
+
io.stderr.write(`dexterm voice: unknown model ${JSON.stringify(id)}\n${VOICE_USAGE}\n`);
|
|
106
|
+
return 2;
|
|
107
|
+
}
|
|
108
|
+
specs.push(spec);
|
|
109
|
+
}
|
|
110
|
+
if (!yes && !io.stdin.isTTY) {
|
|
111
|
+
io.stderr.write('dexterm voice download: no interactive terminal to confirm; re-run with --yes to approve the download.\n');
|
|
112
|
+
return 2;
|
|
113
|
+
}
|
|
114
|
+
let anyFailed = false;
|
|
115
|
+
for (const spec of specs) {
|
|
116
|
+
try {
|
|
117
|
+
const res = await downloadModel(spec, {
|
|
118
|
+
confirm: (prompt) => (yes ? Promise.resolve(true) : askYesNo(io, `\n${prompt}`)),
|
|
119
|
+
log: (s) => io.stdout.write(s + '\n'),
|
|
120
|
+
});
|
|
121
|
+
if (res.status === 'cancelled')
|
|
122
|
+
return 1; // the user said no — stop here
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
io.stderr.write(`dexterm voice download ${spec.id}: ${e.message}\n`);
|
|
126
|
+
anyFailed = true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return anyFailed ? 1 : 0;
|
|
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
|
+
}
|
|
168
|
+
/** `dexterm voice …`. Returns the process exit code. */
|
|
169
|
+
export async function voiceCommand(argv, io) {
|
|
170
|
+
const yes = argv.includes('--yes') || argv.includes('-y');
|
|
171
|
+
const purgeModel = argv.includes('--purge-model');
|
|
172
|
+
const rest = argv.filter((a) => a !== '--yes' && a !== '-y' && a !== '--purge-model');
|
|
173
|
+
const sub = rest[0];
|
|
174
|
+
if (sub === undefined || sub === 'status')
|
|
175
|
+
return printStatus(io);
|
|
176
|
+
if (sub === 'setup')
|
|
177
|
+
return runSetup(io, yes);
|
|
178
|
+
if (sub === 'uninstall' || sub === 'remove')
|
|
179
|
+
return runUninstall(io, purgeModel);
|
|
180
|
+
if (sub === 'download') {
|
|
181
|
+
const which = rest[1];
|
|
182
|
+
if (!which) {
|
|
183
|
+
io.stderr.write(`dexterm voice download: name a model.\n${VOICE_USAGE}\n`);
|
|
184
|
+
return 2;
|
|
185
|
+
}
|
|
186
|
+
return runDownload(io, which, yes);
|
|
187
|
+
}
|
|
188
|
+
if (sub === '--help' || sub === '-h' || sub === 'help') {
|
|
189
|
+
io.stdout.write(VOICE_USAGE + '\n');
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
io.stderr.write(`dexterm voice: unknown subcommand ${JSON.stringify(sub)}\n${VOICE_USAGE}\n`);
|
|
193
|
+
return 2;
|
|
194
|
+
}
|
|
@@ -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 });
|