dexterm-cli 0.1.2 → 0.1.4
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 +7 -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/docs-kb-gen.js +228 -0
- package/dist/packages/cli/src/doctor.js +28 -12
- 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/index.js +1 -1
- package/dist/packages/cli/src/kb-skill-cmd.js +3 -0
- package/dist/packages/cli/src/larp-detected.js +209 -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/token.js +38 -1
- package/dist/packages/cli/src/live/trade.js +24 -1
- package/dist/packages/cli/src/main.js +495 -11
- package/dist/packages/cli/src/mcp-cmd.js +2 -7
- package/dist/packages/cli/src/mcp-follow.js +225 -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 +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 +115 -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/cli/src/x-cmd.js +11 -5
- 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 +39 -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/assets/kb/docs/docs-agents-capability-tiers-enforced-server-side.md +38 -0
- package/dist/packages/mcp/assets/kb/docs/docs-agents-install-in-one-command.md +49 -0
- package/dist/packages/mcp/assets/kb/docs/docs-agents-per-agent-mandates.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-agents-recipes.md +43 -0
- package/dist/packages/mcp/assets/kb/docs/docs-agents-red-team-coverage.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-agents-two-ways-to-connect.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-agents-untrusted-by-default.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-agents-watch-an-agent-work-spectator-view.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-agents.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-answer-style.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-if-claude-is-missing.md +25 -0
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-it-knows-the-app.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-mcp-tools.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-plain-language-filters.md +25 -0
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-skills.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-starting-a-run.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-what-it-can-change-three-tiers.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-what-it-sees.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-what-the-analyst-can-never-do.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-analyst.md +23 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-1-start-dexterm.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-2-open-a-real-coin-by-contract-address.md +43 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-3-read-the-safety-panel.md +40 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-4-read-the-holder-panel.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-5-optionally-ask-the-analyst.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-6-export-or-share-what-you-see.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-7-quit-safely.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-check-the-installation-with-doctor.md +38 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-choose-a-setup-path.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-common-mistakes-and-recovery.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-copy-and-paste-without-surprises.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-data-says-unavailable-stale-or-rate-limited.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-dexterm-says-command-not-found.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-optional-help-is-unavailable.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-paste-does-nothing-or-pastes-twice.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-the-screen-looks-broken.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-you-typed-the-wrong-thing.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-install-node-js-first.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-linux.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-macos-homebrew.md +46 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-macos-or-linux-npm.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-macos.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-open-the-terminal.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-reopen-dexterm-later.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-ten-minute-first-win.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-terminal-shell-prompt-command-output-and-path.md +40 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-where-to-go-next.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-powershell-experimental.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-cli-dexterm-help.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-cli-filter-grammar.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-cli-flags.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-cli-invocations.md +43 -0
- package/dist/packages/mcp/assets/kb/docs/docs-cli-presets.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-cli-scan-commands.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-cli-sort-keys.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-cli-subcommands.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-cli.md +25 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-environment-variables.md +45 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-files-on-disk.md +41 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +44 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-history-recorder.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-packs.md +38 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-price-display-mode.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-push-alerts-and-dexterm-watch.md +45 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-rules-engine.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-the-analyst-s-launch.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-the-optional-free-rpc-key.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-themes-and-keymaps.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-themes-and-look.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-widgets.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-workspaces.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration.md +25 -0
- package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-1-follow-a-wallet-see-what-it-does.md +51 -0
- package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-2-the-one-key-mirror-ticket-a-proposal.md +56 -0
- package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-3-capped-auto-mirror-default-off-less.md +47 -0
- package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-what-is-proven-what-is-not.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-data-sources-cache.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-data-sources-health-registry.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-data-sources-rate-limits-and-pacing.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-data-sources-request-budget-meter.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-data-sources-sources.md +47 -0
- package/dist/packages/mcp/assets/kb/docs/docs-data-sources-the-helius-credit-meter.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-data-sources.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-dexterm-doctor.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-first-run.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +71 -0
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-next.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-running-the-tests.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +44 -0
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-updating.md +28 -0
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-keyboard-global-keys.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-keyboard-keys-handled-by-the-loop.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-keyboard-per-screen-keys.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-keyboard-what-shows.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-keyboard.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +55 -0
- package/dist/packages/mcp/assets/kb/docs/docs-larp-open-link-keys.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-larp-paste-a-coin-on-the-web.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-larp-the-verdict.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-larp-timestamped-follow-up-drafts.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-larp-what-it-looks-at.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-larp.md +24 -0
- package/dist/packages/mcp/assets/kb/docs/docs-login-with-x-degraded-states-you-will-see.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-login-with-x-free-vs-paid-x-api-tiers-your-bill-your-limits.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-login-with-x-kol-lists-and-the-per-handle-filter.md +28 -0
- package/dist/packages/mcp/assets/kb/docs/docs-login-with-x-tests.md +25 -0
- package/dist/packages/mcp/assets/kb/docs/docs-login-with-x-what-is-read-and-when.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-login-with-x-what-you-set-up-once.md +40 -0
- package/dist/packages/mcp/assets/kb/docs/docs-login-with-x-where-the-tokens-live.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-login-with-x.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-a-live-ticket.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-before-you-press-size-vs-caps.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-devnet-signer-mainnet-token.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-jupiter-rate-limits.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-link-a-wallet.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-live-trading-devnet-first.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-mainnet.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-order-plans-and-standing-mandates.md +44 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-panic.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-pnl-cards.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-live-fire-path-devnet-the-fast-wallet.md +41 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-signer-process.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-ticket.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-wallet.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-trade-moments.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-what-the-live-path-keeps.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-your-first-live-trade.md +41 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-bubble-map-popup.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-build-the-native-window.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-config-dexterm-popup-json.md +38 -0
- package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-docking-macos.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-keys-in-the-popup.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-market-cap-axis.md +28 -0
- package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-security-model.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-token-and-feeds-popups.md +38 -0
- package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-what-it-shows.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-popup-chart.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-scan-cost-control.md +28 -0
- package/dist/packages/mcp/assets/kb/docs/docs-scan-keys-flag-command.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-scan-safety-four-layers.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-scan-states.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-scan-the-read.md +25 -0
- package/dist/packages/mcp/assets/kb/docs/docs-scan-triggers-and-score.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-scan.md +24 -0
- package/dist/packages/mcp/assets/kb/docs/docs-scorecard-frozen-canary.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-scorecard-frozen-task-1401-evaluation.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-scorecard-latest-skill-care-run.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-scorecard-links-only-larp-baseline-where-it-is-weak.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-scorecard-not-measured.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-scorecard-rug-verdict-rules.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-scorecard-source-and-rebuild.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-scorecard.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-1-screener.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-2-new-pairs.md +28 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-3-watchlist.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-4-analyst-tab.md +28 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-bubble-map.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-coin-images.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-live-holder-and-rug-signals.md +51 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-order-plans.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-paper-ticket-and-positions.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-the-look.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-token-screen.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-trending-social-and-kol-lists.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-1-the-agent-activity-window-follow-mode.md +56 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-2-the-read-only-stream-spectate.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-3-save-and-replay-record-file-dexterm-spectate.md +41 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-follow-over-mcp-in-the-running-terminal.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-limits.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-quick-start.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-security-model.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-see-also.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-streaming-it.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-three-surfaces.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-what-is-redacted.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-common-questions.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-doctor-lines.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-in-app-messages.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-reporting-a-problem.md +25 -0
- package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting.md +23 -0
- package/dist/packages/mcp/assets/kb/docs/docs-voice-doctor.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-voice-it-clarifies-it-never-guesses.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-voice-latency.md +38 -0
- package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-voice-privacy.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-voice-still-unverified.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-voice-the-voice-neural-by-default.md +48 -0
- package/dist/packages/mcp/assets/kb/docs/docs-voice-what-it-can-never-do.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-voice-what-you-can-say.md +38 -0
- package/dist/packages/mcp/assets/kb/docs/docs-voice.md +25 -0
- package/dist/packages/mcp/src/app-help.js +258 -0
- package/dist/packages/mcp/src/firewall.js +2 -2
- package/dist/packages/mcp/src/index.js +5 -1
- package/dist/packages/mcp/src/server.js +129 -4
- package/dist/packages/mcp/src/spectator-sink.js +144 -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/stt/whisper.js +4 -2
- 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 +6 -5
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
// Piper neural TTS engine (task-1408). Piper is a free, local, no-key neural text-to-speech
|
|
2
|
-
// (rhasspy/piper, MIT) that sounds markedly better than espeak-ng
|
|
3
|
-
// speech-out engine
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// Piper neural TTS engine (task-1408, task-1416). Piper is a free, local, no-key neural text-to-speech
|
|
2
|
+
// engine (rhasspy/piper, MIT) that sounds markedly better than espeak-ng or the built-in macOS voices.
|
|
3
|
+
// It is the PREFERRED speech-out engine EVERYWHERE it is installed — on macOS (via the managed Python
|
|
4
|
+
// runtime, `dexterm voice setup`) as well as off macOS (via the prebuilt binary) — with `say`/espeak-ng
|
|
5
|
+
// kept as the fallback chain (see engines.ts). Verified in Docker on Linux (task-1397/task-1408) and via
|
|
6
|
+
// the managed venv on an Intel Mac (task-1416): the en_GB-alan-medium voice writes a valid, non-silent
|
|
7
|
+
// 22.05 kHz WAV that whisper.cpp transcribes almost word-for-word.
|
|
8
|
+
//
|
|
9
|
+
// Two runtimes, one engine:
|
|
10
|
+
// - macOS (managed): a private virtualenv at `~/.dexterm/piper/venv` with `pip install piper-tts`. The
|
|
11
|
+
// prebuilt macOS tarball from rhasspy is broken (it needs a libespeak-ng dylib it does not bundle),
|
|
12
|
+
// so we run `<venv>/bin/python -m piper` instead. Built by `dexterm voice setup`.
|
|
13
|
+
// - Linux/Windows: the prebuilt `piper` binary extracted into `~/.dexterm/piper/` (or on PATH).
|
|
6
14
|
//
|
|
7
15
|
// It carries the SAME contract as the macOS `say` and espeak engines:
|
|
8
16
|
// - text is SANITISED (sanitizeSpeech strips `[[…]]`, control chars, a leading `-`, caps length)
|
|
@@ -11,21 +19,42 @@
|
|
|
11
19
|
// - the voice is fixed by the downloaded MODEL, never by a macOS voice name (so config.voice.name,
|
|
12
20
|
// which on a Mac is e.g. "Eddy (English (UK))", is NEVER passed verbatim to Piper)
|
|
13
21
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
22
|
+
// Latency (task-1416): loading the 63 MB model into onnxruntime dominates first-audio (~1.4–4 s cold,
|
|
23
|
+
// load-dependent), so we keep a pre-warmed STANDBY process that has already loaded the model and primed
|
|
24
|
+
// onnxruntime. speak() ADOPTS the standby, feeds it the reply SENTENCE BY SENTENCE (each line is a
|
|
25
|
+
// separate utterance → a separate WAV), plays those WAVs in order as they land, and spawns a fresh
|
|
26
|
+
// standby in the background for the next reply. So the first word starts ~0.3–0.5 s after an answer lands
|
|
27
|
+
// instead of waiting on a cold model load. stop() kills the adopted synth process AND the player.
|
|
28
|
+
//
|
|
29
|
+
// Piper has no audio output of its own: it synthesises WAVs, which we play through the platform player
|
|
30
|
+
// (afplay on macOS; aplay / paplay / ffplay off macOS — detected and reported by the doctor). Containers
|
|
31
|
+
// have no audio device, so real playback is unverified there; synthesis-to-file IS verified (synthToFile
|
|
32
|
+
// + whisper), and macOS playback via afplay is verified on a real Mac (task-1416).
|
|
17
33
|
import { spawn } from 'node:child_process';
|
|
18
|
-
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
|
|
34
|
+
import { existsSync, mkdtempSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
19
35
|
import { homedir, tmpdir } from 'node:os';
|
|
20
36
|
import { join } from 'node:path';
|
|
21
37
|
import { sanitizeSpeech } from "./voices.js";
|
|
22
|
-
|
|
38
|
+
import { splitSentences } from "../summary.js";
|
|
39
|
+
/** The Piper voice dexterm defaults to: a British male, medium quality (local, no key). */
|
|
23
40
|
export const DEFAULT_PIPER_VOICE = 'en_GB-alan-medium';
|
|
41
|
+
/** `~/.dexterm`, honouring the DEXTERM_HOME override (tests point it at a temp dir) — matches @dexterm/custom. */
|
|
42
|
+
function dextermHomeDir() {
|
|
43
|
+
const h = process.env.DEXTERM_HOME;
|
|
44
|
+
return h && h.trim() ? h : join(homedir(), '.dexterm');
|
|
45
|
+
}
|
|
24
46
|
/** Where the downloaded Piper voice model lives (see `dexterm voice download piper-voice`). */
|
|
25
47
|
export const PIPER_MODEL = process.env.DEXTERM_PIPER_MODEL
|
|
26
|
-
|| join(
|
|
27
|
-
/**
|
|
28
|
-
export
|
|
48
|
+
|| join(dextermHomeDir(), 'models', `${DEFAULT_PIPER_VOICE}.onnx`);
|
|
49
|
+
/** The managed Python interpreter built by `dexterm voice setup` (macOS). */
|
|
50
|
+
export function managedVenvPython(home = dextermHomeDir()) {
|
|
51
|
+
const bin = process.platform === 'win32' ? join('Scripts', 'python.exe') : join('bin', 'python');
|
|
52
|
+
return join(home, 'piper', 'venv', bin);
|
|
53
|
+
}
|
|
54
|
+
/** Audio players we can hand a WAV to, best-first. afplay ships with macOS; the rest are the Linux/Windows set. */
|
|
55
|
+
export const PIPER_PLAYERS = process.platform === 'darwin'
|
|
56
|
+
? ['afplay', 'ffplay']
|
|
57
|
+
: ['aplay', 'paplay', 'ffplay'];
|
|
29
58
|
function which(bin) {
|
|
30
59
|
if (!bin)
|
|
31
60
|
return null;
|
|
@@ -43,17 +72,43 @@ function which(bin) {
|
|
|
43
72
|
return null;
|
|
44
73
|
}
|
|
45
74
|
/**
|
|
46
|
-
* Resolve the Piper binary
|
|
47
|
-
*
|
|
48
|
-
* `apt install piper` is an UNRELATED gaming-mouse tool, so the doctor steers users to the
|
|
49
|
-
* prebuilt release rather than apt; a wrong `piper` on PATH simply fails to synth and voice
|
|
50
|
-
* to text (it never crashes).
|
|
75
|
+
* Resolve the Piper binary (native-binary runtime only). Kept for the off-macOS doctor/status that only
|
|
76
|
+
* ever ship the prebuilt binary. Looks at an explicit override, the dexterm-local install, then PATH.
|
|
77
|
+
* NOTE: Debian's `apt install piper` is an UNRELATED gaming-mouse tool, so the doctor steers users to the
|
|
78
|
+
* rhasspy prebuilt release rather than apt; a wrong `piper` on PATH simply fails to synth and voice
|
|
79
|
+
* degrades to text (it never crashes).
|
|
51
80
|
*/
|
|
52
81
|
export function resolvePiperBin() {
|
|
53
82
|
return which(process.env.DEXTERM_PIPER_BIN || '')
|
|
54
|
-
|| which(join(
|
|
83
|
+
|| which(join(dextermHomeDir(), 'piper', 'piper'))
|
|
55
84
|
|| which('piper');
|
|
56
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Resolve the Piper RUNTIME for this machine, best-first:
|
|
88
|
+
* 1. DEXTERM_PIPER_BIN — an explicit binary (also the test seam).
|
|
89
|
+
* 2. DEXTERM_PIPER_PYTHON — an explicit `python` to run `-m piper` (test seam for the managed path).
|
|
90
|
+
* 3. the managed venv python (`~/.dexterm/piper/venv/bin/python`) — built by `dexterm voice setup`.
|
|
91
|
+
* 4. the dexterm-local prebuilt binary (`~/.dexterm/piper/piper`) or `piper` on PATH.
|
|
92
|
+
* Returns null when none is installed (voice degrades to the next engine, never crashes).
|
|
93
|
+
*/
|
|
94
|
+
export function resolvePiperRuntime() {
|
|
95
|
+
const binOverride = which(process.env.DEXTERM_PIPER_BIN || '');
|
|
96
|
+
if (binOverride)
|
|
97
|
+
return { cmd: binOverride, prefix: [], kind: 'binary' };
|
|
98
|
+
const pyOverride = process.env.DEXTERM_PIPER_PYTHON;
|
|
99
|
+
if (pyOverride && existsSync(pyOverride))
|
|
100
|
+
return { cmd: pyOverride, prefix: ['-m', 'piper'], kind: 'python' };
|
|
101
|
+
const venv = managedVenvPython();
|
|
102
|
+
if (existsSync(venv))
|
|
103
|
+
return { cmd: venv, prefix: ['-m', 'piper'], kind: 'python' };
|
|
104
|
+
const localBin = which(join(dextermHomeDir(), 'piper', 'piper'));
|
|
105
|
+
if (localBin)
|
|
106
|
+
return { cmd: localBin, prefix: [], kind: 'binary' };
|
|
107
|
+
const pathBin = which('piper');
|
|
108
|
+
if (pathBin)
|
|
109
|
+
return { cmd: pathBin, prefix: [], kind: 'binary' };
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
57
112
|
/** The player that will play Piper's WAV, or null when none is installed. */
|
|
58
113
|
export function pickPlayer() {
|
|
59
114
|
const override = process.env.DEXTERM_AUDIO_PLAYER;
|
|
@@ -66,52 +121,90 @@ export function pickPlayer() {
|
|
|
66
121
|
}
|
|
67
122
|
/** Argv to play a finished WAV through the given player (quiet, exits on end, no window). */
|
|
68
123
|
function playerArgs(player, wav) {
|
|
69
|
-
|
|
124
|
+
const base = player.replace(/\.exe$/i, '').split(/[\\/]/).pop() ?? player;
|
|
125
|
+
switch (base) {
|
|
126
|
+
case 'afplay': return [wav];
|
|
70
127
|
case 'ffplay': return ['-nodisp', '-autoexit', '-loglevel', 'quiet', wav];
|
|
71
128
|
case 'paplay': return [wav];
|
|
72
129
|
case 'aplay':
|
|
73
130
|
default: return ['-q', wav];
|
|
74
131
|
}
|
|
75
132
|
}
|
|
133
|
+
/** Map a words-per-minute rate to Piper's --length_scale (lower rate → slower → larger scale). */
|
|
134
|
+
function lengthScaleFor(rate) {
|
|
135
|
+
if (typeof rate !== 'number' || !(rate > 0))
|
|
136
|
+
return undefined;
|
|
137
|
+
return Math.max(0.5, Math.min(2, 190 / rate));
|
|
138
|
+
}
|
|
139
|
+
/** WAV basenames in a dir, oldest-first (Piper names them by an increasing timestamp). */
|
|
140
|
+
function listWavs(dir) {
|
|
141
|
+
let names;
|
|
142
|
+
try {
|
|
143
|
+
names = readdirSync(dir).filter((f) => f.endsWith('.wav'));
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
return names.sort((a, b) => {
|
|
149
|
+
try {
|
|
150
|
+
return statSync(join(dir, a)).mtimeMs - statSync(join(dir, b)).mtimeMs || a.localeCompare(b);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return a.localeCompare(b);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/** Sanitise, then split into one line per sentence for sentence-by-sentence streaming (never empty lines). */
|
|
158
|
+
function splitForSpeech(text) {
|
|
159
|
+
const safe = sanitizeSpeech(text);
|
|
160
|
+
if (!safe)
|
|
161
|
+
return [];
|
|
162
|
+
const parts = splitSentences(safe).map((s) => s.trim()).filter(Boolean);
|
|
163
|
+
return parts.length ? parts : [safe];
|
|
164
|
+
}
|
|
76
165
|
export class PiperTts {
|
|
77
166
|
name = 'piper';
|
|
78
167
|
label = 'Piper (neural, local, no key)';
|
|
79
168
|
model;
|
|
80
169
|
/** The utterance currently playing. A new speak() stops it first, so there is only ever one. */
|
|
81
170
|
current = null;
|
|
171
|
+
/** A pre-warmed process waiting to be adopted by the next speak() (task-1416 latency). */
|
|
172
|
+
standby = null;
|
|
173
|
+
/** Every live warm process, so dispose() can never leak one. */
|
|
174
|
+
live = new Set();
|
|
82
175
|
constructor(model = PIPER_MODEL) { this.model = model; }
|
|
83
176
|
/** The voice id this engine speaks with (the model basename), for the status line. */
|
|
84
177
|
get voiceName() { return DEFAULT_PIPER_VOICE; }
|
|
85
|
-
/** True when Piper can speak ALOUD here:
|
|
178
|
+
/** True when Piper can speak ALOUD here: a runtime, the voice model and a player are all present. */
|
|
86
179
|
async available() {
|
|
87
|
-
return
|
|
180
|
+
return resolvePiperRuntime() !== null && existsSync(this.model) && pickPlayer() !== null;
|
|
88
181
|
}
|
|
89
|
-
/** True when Piper can SYNTHESISE here (
|
|
182
|
+
/** True when Piper can SYNTHESISE here (runtime + model) even with no audio device (used for the proof). */
|
|
90
183
|
canSynth() {
|
|
91
|
-
return
|
|
184
|
+
return resolvePiperRuntime() !== null && existsSync(this.model);
|
|
92
185
|
}
|
|
93
186
|
/** Piper's voice is the model; there is no per-machine installed-voice list to cycle. */
|
|
94
187
|
async listVoices() { return [DEFAULT_PIPER_VOICE]; }
|
|
95
188
|
/**
|
|
96
|
-
* Synthesise `text` to a WAV at `outPath`. Resolves on success, rejects on a missing
|
|
189
|
+
* Synthesise `text` to a WAV at `outPath`. Resolves on success, rejects on a missing runtime/model or a
|
|
97
190
|
* non-zero exit. This is the engine's testable, audio-device-free core — the Docker proof and the unit
|
|
98
191
|
* tests drive it directly. The text is sanitised and delivered on stdin, exactly as speak() does.
|
|
99
192
|
*/
|
|
100
193
|
synthToFile(text, outPath, opts = {}) {
|
|
101
|
-
const
|
|
194
|
+
const runtime = resolvePiperRuntime();
|
|
102
195
|
const safe = sanitizeSpeech(text);
|
|
103
196
|
return new Promise((resolve, reject) => {
|
|
104
|
-
if (!
|
|
105
|
-
reject(new Error('piper
|
|
197
|
+
if (!runtime) {
|
|
198
|
+
reject(new Error('piper runtime not found'));
|
|
106
199
|
return;
|
|
107
200
|
}
|
|
108
|
-
const args = ['--model', this.model, '--output_file', outPath];
|
|
201
|
+
const args = [...runtime.prefix, '--model', this.model, '--output_file', outPath];
|
|
109
202
|
if (typeof opts.lengthScale === 'number' && Number.isFinite(opts.lengthScale)) {
|
|
110
203
|
args.push('--length_scale', String(opts.lengthScale)); // >1 = slower speech
|
|
111
204
|
}
|
|
112
205
|
let child;
|
|
113
206
|
try {
|
|
114
|
-
child = spawn(
|
|
207
|
+
child = spawn(runtime.cmd, args, { stdio: ['pipe', 'ignore', 'pipe'] });
|
|
115
208
|
}
|
|
116
209
|
catch (e) {
|
|
117
210
|
reject(e);
|
|
@@ -131,66 +224,209 @@ export class PiperTts {
|
|
|
131
224
|
: reject(new Error(`piper exit ${code}: ${err.slice(-200)}`)));
|
|
132
225
|
});
|
|
133
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Pre-load the model into a standby process so the first reply starts speaking quickly. Safe to call
|
|
229
|
+
* repeatedly and before anything is spoken (the controller calls it when voice is enabled). No-op when
|
|
230
|
+
* Piper is not installed here or a standby for this rate already exists.
|
|
231
|
+
*/
|
|
232
|
+
warmup(rate) {
|
|
233
|
+
const scale = lengthScaleFor(rate);
|
|
234
|
+
if (this.standby && !this.standby.disposed && this.standby.scale === scale)
|
|
235
|
+
return;
|
|
236
|
+
if (this.standby) {
|
|
237
|
+
this.standby.dispose();
|
|
238
|
+
this.standby = null;
|
|
239
|
+
}
|
|
240
|
+
this.standby = this.spawnWarm(scale);
|
|
241
|
+
}
|
|
242
|
+
/** Spawn a warm process in output-dir mode and prime the model with one discarded utterance. */
|
|
243
|
+
spawnWarm(scale) {
|
|
244
|
+
const runtime = resolvePiperRuntime();
|
|
245
|
+
if (!runtime || !existsSync(this.model))
|
|
246
|
+
return null;
|
|
247
|
+
const dir = mkdtempSync(join(tmpdir(), 'dexterm-piper-'));
|
|
248
|
+
const args = [...runtime.prefix, '--model', this.model, '--output_dir', dir];
|
|
249
|
+
if (scale !== undefined)
|
|
250
|
+
args.push('--length_scale', String(scale));
|
|
251
|
+
let child;
|
|
252
|
+
try {
|
|
253
|
+
child = spawn(runtime.cmd, args, { stdio: ['pipe', 'ignore', 'ignore'] });
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
try {
|
|
257
|
+
rmSync(dir, { recursive: true, force: true });
|
|
258
|
+
}
|
|
259
|
+
catch { /* ignore */ }
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
const seen = new Set();
|
|
263
|
+
const warm = {
|
|
264
|
+
child, dir, seen, scale, disposed: false,
|
|
265
|
+
ready: new Promise((resolve, reject) => {
|
|
266
|
+
const poll = setInterval(() => {
|
|
267
|
+
if (warm.disposed) {
|
|
268
|
+
clearInterval(poll);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
for (const f of listWavs(dir))
|
|
272
|
+
if (!seen.has(f)) {
|
|
273
|
+
seen.add(f);
|
|
274
|
+
clearInterval(poll);
|
|
275
|
+
resolve();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
}, 20);
|
|
279
|
+
child.on('error', () => { clearInterval(poll); reject(new Error('piper warm process error')); });
|
|
280
|
+
child.on('close', () => { clearInterval(poll); reject(new Error('piper warm process exited')); });
|
|
281
|
+
}),
|
|
282
|
+
dispose: () => {
|
|
283
|
+
if (warm.disposed)
|
|
284
|
+
return;
|
|
285
|
+
warm.disposed = true;
|
|
286
|
+
this.live.delete(warm);
|
|
287
|
+
try {
|
|
288
|
+
child.kill('SIGTERM');
|
|
289
|
+
}
|
|
290
|
+
catch { /* already gone */ }
|
|
291
|
+
try {
|
|
292
|
+
rmSync(dir, { recursive: true, force: true });
|
|
293
|
+
}
|
|
294
|
+
catch { /* ignore */ }
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
this.live.add(warm);
|
|
298
|
+
// Swallow the rejection so a dead warm process never becomes an unhandled rejection; adopters await it.
|
|
299
|
+
warm.ready.catch(() => { });
|
|
300
|
+
// A short priming utterance makes onnxruntime load the model now; its WAV lands in `seen` and is never
|
|
301
|
+
// played. stdin stays OPEN so speak() can adopt this process and feed the real sentences.
|
|
302
|
+
try {
|
|
303
|
+
child.stdin?.on('error', () => { });
|
|
304
|
+
child.stdin?.write('ready\n');
|
|
305
|
+
}
|
|
306
|
+
catch { /* child gone */ }
|
|
307
|
+
return warm;
|
|
308
|
+
}
|
|
309
|
+
/** Take the standby if it matches the rate; otherwise discard it and spawn a fresh warm process. */
|
|
310
|
+
takeWarm(scale) {
|
|
311
|
+
if (this.standby && !this.standby.disposed && this.standby.scale === scale) {
|
|
312
|
+
const w = this.standby;
|
|
313
|
+
this.standby = null;
|
|
314
|
+
return w;
|
|
315
|
+
}
|
|
316
|
+
if (this.standby) {
|
|
317
|
+
this.standby.dispose();
|
|
318
|
+
this.standby = null;
|
|
319
|
+
}
|
|
320
|
+
return this.spawnWarm(scale);
|
|
321
|
+
}
|
|
134
322
|
speak(text, opts = {}) {
|
|
135
323
|
this.stopCurrent(); // single queue: never two speakers at once
|
|
136
324
|
const player = pickPlayer();
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
let child = null;
|
|
325
|
+
const scale = lengthScaleFor(opts.rate);
|
|
326
|
+
const sentences = splitForSpeech(text);
|
|
140
327
|
let stopped = false;
|
|
141
328
|
let resolve;
|
|
142
329
|
const done = new Promise((r) => { resolve = r; });
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const cleanup = () => {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
330
|
+
let warm = null;
|
|
331
|
+
let playing = null;
|
|
332
|
+
const cleanup = () => { if (warm) {
|
|
333
|
+
warm.dispose();
|
|
334
|
+
warm = null;
|
|
335
|
+
} };
|
|
336
|
+
const settle = () => {
|
|
150
337
|
if (stopped)
|
|
151
338
|
return;
|
|
152
339
|
stopped = true;
|
|
153
340
|
if (this.current === handle)
|
|
154
341
|
this.current = null;
|
|
342
|
+
try {
|
|
343
|
+
playing?.kill('SIGTERM');
|
|
344
|
+
}
|
|
345
|
+
catch { /* ignore */ }
|
|
155
346
|
cleanup();
|
|
156
347
|
resolve();
|
|
157
348
|
};
|
|
158
|
-
const handle = {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
349
|
+
const handle = { done, stop: settle };
|
|
350
|
+
this.current = handle;
|
|
351
|
+
// Nothing to say, or we cannot play aloud → degrade to text (resolve, never throw).
|
|
352
|
+
if (!player || sentences.length === 0 || !this.canSynth()) {
|
|
353
|
+
queueMicrotask(settle);
|
|
354
|
+
return handle;
|
|
355
|
+
}
|
|
356
|
+
// Adopt a warm process for the right rate (respawn if the rate changed), then refill the standby.
|
|
357
|
+
warm = this.takeWarm(scale);
|
|
358
|
+
if (!warm) {
|
|
359
|
+
queueMicrotask(settle);
|
|
360
|
+
return handle;
|
|
361
|
+
} // no runtime/model → text
|
|
362
|
+
this.standby = this.spawnWarm(scale); // keep one warm for the NEXT reply
|
|
363
|
+
const queue = [];
|
|
364
|
+
let synthClosed = false;
|
|
365
|
+
let firstFired = false;
|
|
366
|
+
const adopted = warm;
|
|
367
|
+
const startPlayback = () => {
|
|
368
|
+
const poll = setInterval(() => {
|
|
369
|
+
if (stopped) {
|
|
370
|
+
clearInterval(poll);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
for (const f of listWavs(adopted.dir))
|
|
374
|
+
if (!adopted.seen.has(f)) {
|
|
375
|
+
adopted.seen.add(f);
|
|
376
|
+
queue.push(join(adopted.dir, f));
|
|
377
|
+
}
|
|
378
|
+
}, 20);
|
|
379
|
+
adopted.child.on('close', () => { synthClosed = true; });
|
|
380
|
+
const playNext = () => {
|
|
381
|
+
if (stopped) {
|
|
382
|
+
clearInterval(poll);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
// A WAV is safe to play once a LATER one exists or the synth has exited (so it is fully written).
|
|
386
|
+
const ready = queue.length > 1 || (queue.length === 1 && synthClosed);
|
|
387
|
+
if (!ready) {
|
|
388
|
+
if (synthClosed && queue.length === 0) {
|
|
389
|
+
clearInterval(poll);
|
|
390
|
+
settle();
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
setTimeout(playNext, 20);
|
|
162
394
|
return;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
this.current = null;
|
|
395
|
+
}
|
|
396
|
+
const wav = queue.shift();
|
|
166
397
|
try {
|
|
167
|
-
|
|
398
|
+
playing = spawn(player, playerArgs(player, wav), { stdio: 'ignore' });
|
|
399
|
+
playing.on('spawn', () => { if (!stopped && !firstFired) {
|
|
400
|
+
firstFired = true;
|
|
401
|
+
opts.onFirstAudio?.();
|
|
402
|
+
} });
|
|
403
|
+
playing.on('error', () => { if (!stopped)
|
|
404
|
+
setTimeout(playNext, 0); });
|
|
405
|
+
playing.on('close', () => { if (!stopped)
|
|
406
|
+
setTimeout(playNext, 0); });
|
|
168
407
|
}
|
|
169
|
-
catch {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
408
|
+
catch {
|
|
409
|
+
if (!stopped)
|
|
410
|
+
setTimeout(playNext, 0);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
playNext();
|
|
173
414
|
};
|
|
174
|
-
|
|
175
|
-
if (!player) {
|
|
176
|
-
queueMicrotask(finish);
|
|
177
|
-
return handle;
|
|
178
|
-
} // no player → can't speak aloud; degrade to text
|
|
179
|
-
// Synthesise, then play. Piper's RTF is ~0.16, so synth is fast; first audio is the player spawn.
|
|
180
|
-
this.synthToFile(text, wav, lengthScale !== undefined ? { lengthScale } : {}).then(() => {
|
|
415
|
+
adopted.ready.then(() => {
|
|
181
416
|
if (stopped)
|
|
182
417
|
return;
|
|
418
|
+
// Feed the reply one sentence per line; end stdin so the process exits after the last WAV.
|
|
183
419
|
try {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
child.on('error', finish);
|
|
188
|
-
child.on('close', finish);
|
|
420
|
+
for (const s of sentences)
|
|
421
|
+
adopted.child.stdin?.write(s + '\n');
|
|
422
|
+
adopted.child.stdin?.end();
|
|
189
423
|
}
|
|
190
424
|
catch {
|
|
191
|
-
|
|
425
|
+
settle();
|
|
426
|
+
return;
|
|
192
427
|
}
|
|
193
|
-
|
|
428
|
+
startPlayback();
|
|
429
|
+
}).catch(settle); // the warm process died before it was ready → degrade to text
|
|
194
430
|
return handle;
|
|
195
431
|
}
|
|
196
432
|
stopCurrent() {
|
|
@@ -203,4 +439,11 @@ export class PiperTts {
|
|
|
203
439
|
catch { /* ignore */ }
|
|
204
440
|
}
|
|
205
441
|
}
|
|
442
|
+
/** Release the standby and any live warm processes (called when voice is disabled). */
|
|
443
|
+
dispose() {
|
|
444
|
+
this.stopCurrent();
|
|
445
|
+
for (const w of [...this.live])
|
|
446
|
+
w.dispose();
|
|
447
|
+
this.standby = null;
|
|
448
|
+
}
|
|
206
449
|
}
|
|
@@ -13,8 +13,9 @@ import { spawn } from 'node:child_process';
|
|
|
13
13
|
import { execFile } from 'node:child_process';
|
|
14
14
|
import { parseSayVoices, sanitizeSpeech } from "./voices.js";
|
|
15
15
|
export const SAY_BIN = process.env.DEXTERM_SAY_BIN || 'say';
|
|
16
|
-
/** Consistent speaking rate (words-per-minute-ish) for every reply path.
|
|
17
|
-
|
|
16
|
+
/** Consistent speaking rate (words-per-minute-ish) for every reply path. Brisk by default (task-1411:
|
|
17
|
+
* the old delivery was reported "way too slow"); the user can nudge it live with < / > or set voice.rate. */
|
|
18
|
+
export const DEFAULT_RATE = 205;
|
|
18
19
|
export class SayTts {
|
|
19
20
|
name = 'say';
|
|
20
21
|
label = 'macOS say (built-in, no key)';
|
|
@@ -58,12 +58,15 @@ export function parseSayVoices(stdout) {
|
|
|
58
58
|
*/
|
|
59
59
|
export const BRITISH_MALE_PREFERENCE = ['eddy', 'daniel', 'oliver', 'arthur'];
|
|
60
60
|
/**
|
|
61
|
-
* Per-voice delivery defaults, applied only when the user has not saved their own rate/pitch.
|
|
62
|
-
*
|
|
61
|
+
* Per-voice delivery defaults, applied only when the user has not saved their own rate/pitch.
|
|
62
|
+
*
|
|
63
|
+
* task-1411: the old Eddy override (rate 168 + pbas 30) came from an audition sample the maintainers never
|
|
64
|
+
* explicitly picked; it was reported as "crazy bad" and "way too slow", and the pitch shift distorted
|
|
65
|
+
* the voice. It is removed: every voice now uses the engine-wide DEFAULT_RATE (a brisk ~205 wpm) and NO
|
|
66
|
+
* pitch override unless the user sets `voice.pitch` themselves. Keep this map empty rather than deleting
|
|
67
|
+
* the hook, so a future per-voice tweak has a home and the public `voiceStyleDefaults` export is stable.
|
|
63
68
|
*/
|
|
64
|
-
export const VOICE_STYLE_DEFAULTS = {
|
|
65
|
-
eddy: { rate: 168, pitch: 30 },
|
|
66
|
-
};
|
|
69
|
+
export const VOICE_STYLE_DEFAULTS = {};
|
|
67
70
|
/** The delivery defaults for a resolved voice name, or undefined to use the engine-wide defaults. */
|
|
68
71
|
export function voiceStyleDefaults(name) {
|
|
69
72
|
return name ? VOICE_STYLE_DEFAULTS[normalizeVoiceName(name)] : undefined;
|
package/package.json
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dexterm-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"mcpName": "io.github.dexterm-xyz/dexterm",
|
|
4
5
|
"description": "dexterm: terminal DEX screener with a local Claude CLI analyst",
|
|
5
6
|
"author": "The dexterm authors",
|
|
6
7
|
"license": "AGPL-3.0-only",
|
|
7
|
-
"repository": {
|
|
8
|
-
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/dexterm-xyz/dexterm.git"
|
|
10
|
-
},
|
|
11
8
|
"type": "module",
|
|
12
9
|
"bin": {
|
|
13
10
|
"dexterm": "./dist/packages/cli/bin/dexterm.mjs"
|
|
@@ -36,5 +33,9 @@
|
|
|
36
33
|
"@dexterm/recorder": "0.0.1",
|
|
37
34
|
"@dexterm/tui": "0.0.1",
|
|
38
35
|
"@dexterm/voice": "0.0.1"
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/dexterm-xyz/dexterm.git"
|
|
39
40
|
}
|
|
40
41
|
}
|