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.
Files changed (327) hide show
  1. package/dist/packages/agents/src/bus.js +37 -1
  2. package/dist/packages/agents/src/follow.js +105 -0
  3. package/dist/packages/agents/src/index.js +5 -0
  4. package/dist/packages/agents/src/spectator-server.js +153 -0
  5. package/dist/packages/agents/src/spectator.js +212 -0
  6. package/dist/packages/cli/assets/claude/analyst-system.md +7 -1
  7. package/dist/packages/cli/src/agent-cmd.js +71 -7
  8. package/dist/packages/cli/src/agent-spectator.js +37 -0
  9. package/dist/packages/cli/src/alerts/format.js +25 -0
  10. package/dist/packages/cli/src/app-help-gen.js +365 -0
  11. package/dist/packages/cli/src/docs-kb-gen.js +228 -0
  12. package/dist/packages/cli/src/doctor.js +28 -12
  13. package/dist/packages/cli/src/follow-cmd.js +107 -1
  14. package/dist/packages/cli/src/help-cmd.js +48 -0
  15. package/dist/packages/cli/src/index.js +1 -1
  16. package/dist/packages/cli/src/kb-skill-cmd.js +3 -0
  17. package/dist/packages/cli/src/larp-detected.js +209 -0
  18. package/dist/packages/cli/src/live/index.js +64 -3
  19. package/dist/packages/cli/src/live/screens/token-live.js +24 -0
  20. package/dist/packages/cli/src/live/signals.js +109 -0
  21. package/dist/packages/cli/src/live/token.js +38 -1
  22. package/dist/packages/cli/src/live/trade.js +24 -1
  23. package/dist/packages/cli/src/main.js +495 -11
  24. package/dist/packages/cli/src/mcp-cmd.js +2 -7
  25. package/dist/packages/cli/src/mcp-follow.js +225 -0
  26. package/dist/packages/cli/src/pnl-app.js +221 -0
  27. package/dist/packages/cli/src/pnl-card.js +615 -0
  28. package/dist/packages/cli/src/record-cmd.js +455 -0
  29. package/dist/packages/cli/src/rules.js +50 -2
  30. package/dist/packages/cli/src/screens/follows.js +91 -2
  31. package/dist/packages/cli/src/screens/overlay.js +3 -0
  32. package/dist/packages/cli/src/screens/positions.js +1 -1
  33. package/dist/packages/cli/src/screens/rules.js +1 -1
  34. package/dist/packages/cli/src/spectate-cmd.js +115 -0
  35. package/dist/packages/cli/src/state.js +1 -1
  36. package/dist/packages/cli/src/version.js +1 -1
  37. package/dist/packages/cli/src/voice-cmd.js +86 -11
  38. package/dist/packages/cli/src/voice-hud.js +1 -1
  39. package/dist/packages/cli/src/x-cmd.js +11 -5
  40. package/dist/packages/custom/src/rules.js +2 -1
  41. package/dist/packages/data/src/analyst-actions.js +130 -3
  42. package/dist/packages/data/src/fast-lane.js +16 -2
  43. package/dist/packages/data/src/index.js +1 -0
  44. package/dist/packages/data/src/intel/live-signals.js +185 -0
  45. package/dist/packages/data/src/trade/copy/index.js +1 -1
  46. package/dist/packages/data/src/trade/copy/stats.js +36 -2
  47. package/dist/packages/data/src/trade/copy/tracker.js +3 -2
  48. package/dist/packages/mcp/assets/kb/app-help/app-help-command.md +35 -0
  49. package/dist/packages/mcp/assets/kb/app-help/ask-the-analyst.md +43 -0
  50. package/dist/packages/mcp/assets/kb/app-help/backtest.md +33 -0
  51. package/dist/packages/mcp/assets/kb/app-help/bubble-map.md +35 -0
  52. package/dist/packages/mcp/assets/kb/app-help/calm-mode.md +31 -0
  53. package/dist/packages/mcp/assets/kb/app-help/change-voice-speed.md +31 -0
  54. package/dist/packages/mcp/assets/kb/app-help/change-voice.md +30 -0
  55. package/dist/packages/mcp/assets/kb/app-help/chart-popup-selftest.md +32 -0
  56. package/dist/packages/mcp/assets/kb/app-help/coin-images.md +29 -0
  57. package/dist/packages/mcp/assets/kb/app-help/configure-rpc-key.md +28 -0
  58. package/dist/packages/mcp/assets/kb/app-help/connect-an-agent-registry.md +35 -0
  59. package/dist/packages/mcp/assets/kb/app-help/connect-an-agent.md +39 -0
  60. package/dist/packages/mcp/assets/kb/app-help/custom-keymap.md +32 -0
  61. package/dist/packages/mcp/assets/kb/app-help/edit-kb-or-skill.md +36 -0
  62. package/dist/packages/mcp/assets/kb/app-help/edit-rules.md +34 -0
  63. package/dist/packages/mcp/assets/kb/app-help/export-a-view.md +36 -0
  64. package/dist/packages/mcp/assets/kb/app-help/follow-a-wallet.md +33 -0
  65. package/dist/packages/mcp/assets/kb/app-help/getting-started.md +37 -0
  66. package/dist/packages/mcp/assets/kb/app-help/kol-lists.md +34 -0
  67. package/dist/packages/mcp/assets/kb/app-help/larp-check.md +36 -0
  68. package/dist/packages/mcp/assets/kb/app-help/link-a-wallet.md +41 -0
  69. package/dist/packages/mcp/assets/kb/app-help/login-with-x.md +33 -0
  70. package/dist/packages/mcp/assets/kb/app-help/narrative-board.md +32 -0
  71. package/dist/packages/mcp/assets/kb/app-help/open-a-coin.md +43 -0
  72. package/dist/packages/mcp/assets/kb/app-help/open-chart-popup.md +33 -0
  73. package/dist/packages/mcp/assets/kb/app-help/order-plans.md +33 -0
  74. package/dist/packages/mcp/assets/kb/app-help/paper-trade.md +36 -0
  75. package/dist/packages/mcp/assets/kb/app-help/price-or-marketcap.md +38 -0
  76. package/dist/packages/mcp/assets/kb/app-help/record-headless.md +34 -0
  77. package/dist/packages/mcp/assets/kb/app-help/recorder-history.md +39 -0
  78. package/dist/packages/mcp/assets/kb/app-help/rug-check.md +39 -0
  79. package/dist/packages/mcp/assets/kb/app-help/run-doctor.md +32 -0
  80. package/dist/packages/mcp/assets/kb/app-help/scan-mode.md +37 -0
  81. package/dist/packages/mcp/assets/kb/app-help/screen-analyst.md +181 -0
  82. package/dist/packages/mcp/assets/kb/app-help/screen-kol.md +190 -0
  83. package/dist/packages/mcp/assets/kb/app-help/screen-newpairs.md +181 -0
  84. package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +180 -0
  85. package/dist/packages/mcp/assets/kb/app-help/screen-screener.md +199 -0
  86. package/dist/packages/mcp/assets/kb/app-help/screen-token.md +240 -0
  87. package/dist/packages/mcp/assets/kb/app-help/screen-trending.md +192 -0
  88. package/dist/packages/mcp/assets/kb/app-help/screen-watchlist.md +181 -0
  89. package/dist/packages/mcp/assets/kb/app-help/set-a-filter.md +36 -0
  90. package/dist/packages/mcp/assets/kb/app-help/setup-neural-voice.md +36 -0
  91. package/dist/packages/mcp/assets/kb/app-help/share-packs.md +34 -0
  92. package/dist/packages/mcp/assets/kb/app-help/sort-the-screener.md +34 -0
  93. package/dist/packages/mcp/assets/kb/app-help/spectate-an-agent.md +39 -0
  94. package/dist/packages/mcp/assets/kb/app-help/standing-mandates.md +33 -0
  95. package/dist/packages/mcp/assets/kb/app-help/switch-theme.md +42 -0
  96. package/dist/packages/mcp/assets/kb/app-help/turn-on-alerts.md +37 -0
  97. package/dist/packages/mcp/assets/kb/app-help/update-dexterm.md +30 -0
  98. package/dist/packages/mcp/assets/kb/app-help/use-presets.md +42 -0
  99. package/dist/packages/mcp/assets/kb/app-help/use-voice-mode.md +36 -0
  100. package/dist/packages/mcp/assets/kb/app-help/watch-a-coin.md +34 -0
  101. package/dist/packages/mcp/assets/kb/app-help/watch-headless.md +35 -0
  102. package/dist/packages/mcp/assets/kb/app-help/what-dexterm-shows-agents.md +31 -0
  103. package/dist/packages/mcp/assets/kb/app-help/workspaces.md +38 -0
  104. package/dist/packages/mcp/assets/kb/docs/docs-agents-capability-tiers-enforced-server-side.md +38 -0
  105. package/dist/packages/mcp/assets/kb/docs/docs-agents-install-in-one-command.md +49 -0
  106. package/dist/packages/mcp/assets/kb/docs/docs-agents-per-agent-mandates.md +35 -0
  107. package/dist/packages/mcp/assets/kb/docs/docs-agents-recipes.md +43 -0
  108. package/dist/packages/mcp/assets/kb/docs/docs-agents-red-team-coverage.md +30 -0
  109. package/dist/packages/mcp/assets/kb/docs/docs-agents-two-ways-to-connect.md +37 -0
  110. package/dist/packages/mcp/assets/kb/docs/docs-agents-untrusted-by-default.md +36 -0
  111. package/dist/packages/mcp/assets/kb/docs/docs-agents-watch-an-agent-work-spectator-view.md +35 -0
  112. package/dist/packages/mcp/assets/kb/docs/docs-agents.md +26 -0
  113. package/dist/packages/mcp/assets/kb/docs/docs-analyst-answer-style.md +35 -0
  114. package/dist/packages/mcp/assets/kb/docs/docs-analyst-if-claude-is-missing.md +25 -0
  115. package/dist/packages/mcp/assets/kb/docs/docs-analyst-it-knows-the-app.md +33 -0
  116. package/dist/packages/mcp/assets/kb/docs/docs-analyst-mcp-tools.md +33 -0
  117. package/dist/packages/mcp/assets/kb/docs/docs-analyst-plain-language-filters.md +25 -0
  118. package/dist/packages/mcp/assets/kb/docs/docs-analyst-skills.md +31 -0
  119. package/dist/packages/mcp/assets/kb/docs/docs-analyst-starting-a-run.md +33 -0
  120. package/dist/packages/mcp/assets/kb/docs/docs-analyst-what-it-can-change-three-tiers.md +33 -0
  121. package/dist/packages/mcp/assets/kb/docs/docs-analyst-what-it-sees.md +26 -0
  122. package/dist/packages/mcp/assets/kb/docs/docs-analyst-what-the-analyst-can-never-do.md +31 -0
  123. package/dist/packages/mcp/assets/kb/docs/docs-analyst.md +23 -0
  124. package/dist/packages/mcp/assets/kb/docs/docs-beginners-1-start-dexterm.md +35 -0
  125. package/dist/packages/mcp/assets/kb/docs/docs-beginners-2-open-a-real-coin-by-contract-address.md +43 -0
  126. package/dist/packages/mcp/assets/kb/docs/docs-beginners-3-read-the-safety-panel.md +40 -0
  127. package/dist/packages/mcp/assets/kb/docs/docs-beginners-4-read-the-holder-panel.md +35 -0
  128. package/dist/packages/mcp/assets/kb/docs/docs-beginners-5-optionally-ask-the-analyst.md +39 -0
  129. package/dist/packages/mcp/assets/kb/docs/docs-beginners-6-export-or-share-what-you-see.md +34 -0
  130. package/dist/packages/mcp/assets/kb/docs/docs-beginners-7-quit-safely.md +34 -0
  131. package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +33 -0
  132. package/dist/packages/mcp/assets/kb/docs/docs-beginners-check-the-installation-with-doctor.md +38 -0
  133. package/dist/packages/mcp/assets/kb/docs/docs-beginners-choose-a-setup-path.md +30 -0
  134. package/dist/packages/mcp/assets/kb/docs/docs-beginners-common-mistakes-and-recovery.md +30 -0
  135. package/dist/packages/mcp/assets/kb/docs/docs-beginners-copy-and-paste-without-surprises.md +39 -0
  136. package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-data-says-unavailable-stale-or-rate-limited.md +35 -0
  137. package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-dexterm-says-command-not-found.md +36 -0
  138. package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-optional-help-is-unavailable.md +33 -0
  139. package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-paste-does-nothing-or-pastes-twice.md +35 -0
  140. package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-the-screen-looks-broken.md +33 -0
  141. package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-you-typed-the-wrong-thing.md +33 -0
  142. package/dist/packages/mcp/assets/kb/docs/docs-beginners-install-node-js-first.md +36 -0
  143. package/dist/packages/mcp/assets/kb/docs/docs-beginners-linux.md +30 -0
  144. package/dist/packages/mcp/assets/kb/docs/docs-beginners-macos-homebrew.md +46 -0
  145. package/dist/packages/mcp/assets/kb/docs/docs-beginners-macos-or-linux-npm.md +39 -0
  146. package/dist/packages/mcp/assets/kb/docs/docs-beginners-macos.md +30 -0
  147. package/dist/packages/mcp/assets/kb/docs/docs-beginners-open-the-terminal.md +29 -0
  148. package/dist/packages/mcp/assets/kb/docs/docs-beginners-reopen-dexterm-later.md +36 -0
  149. package/dist/packages/mcp/assets/kb/docs/docs-beginners-ten-minute-first-win.md +30 -0
  150. package/dist/packages/mcp/assets/kb/docs/docs-beginners-terminal-shell-prompt-command-output-and-path.md +40 -0
  151. package/dist/packages/mcp/assets/kb/docs/docs-beginners-where-to-go-next.md +32 -0
  152. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +29 -0
  153. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-powershell-experimental.md +39 -0
  154. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows.md +30 -0
  155. package/dist/packages/mcp/assets/kb/docs/docs-beginners.md +29 -0
  156. package/dist/packages/mcp/assets/kb/docs/docs-cli-dexterm-help.md +31 -0
  157. package/dist/packages/mcp/assets/kb/docs/docs-cli-filter-grammar.md +37 -0
  158. package/dist/packages/mcp/assets/kb/docs/docs-cli-flags.md +27 -0
  159. package/dist/packages/mcp/assets/kb/docs/docs-cli-invocations.md +43 -0
  160. package/dist/packages/mcp/assets/kb/docs/docs-cli-presets.md +26 -0
  161. package/dist/packages/mcp/assets/kb/docs/docs-cli-scan-commands.md +26 -0
  162. package/dist/packages/mcp/assets/kb/docs/docs-cli-sort-keys.md +27 -0
  163. package/dist/packages/mcp/assets/kb/docs/docs-cli-subcommands.md +26 -0
  164. package/dist/packages/mcp/assets/kb/docs/docs-cli.md +25 -0
  165. package/dist/packages/mcp/assets/kb/docs/docs-configuration-environment-variables.md +45 -0
  166. package/dist/packages/mcp/assets/kb/docs/docs-configuration-files-on-disk.md +41 -0
  167. package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +44 -0
  168. package/dist/packages/mcp/assets/kb/docs/docs-configuration-history-recorder.md +33 -0
  169. package/dist/packages/mcp/assets/kb/docs/docs-configuration-packs.md +38 -0
  170. package/dist/packages/mcp/assets/kb/docs/docs-configuration-price-display-mode.md +35 -0
  171. package/dist/packages/mcp/assets/kb/docs/docs-configuration-push-alerts-and-dexterm-watch.md +45 -0
  172. package/dist/packages/mcp/assets/kb/docs/docs-configuration-rules-engine.md +36 -0
  173. package/dist/packages/mcp/assets/kb/docs/docs-configuration-the-analyst-s-launch.md +27 -0
  174. package/dist/packages/mcp/assets/kb/docs/docs-configuration-the-optional-free-rpc-key.md +42 -0
  175. package/dist/packages/mcp/assets/kb/docs/docs-configuration-themes-and-keymaps.md +29 -0
  176. package/dist/packages/mcp/assets/kb/docs/docs-configuration-themes-and-look.md +27 -0
  177. package/dist/packages/mcp/assets/kb/docs/docs-configuration-widgets.md +31 -0
  178. package/dist/packages/mcp/assets/kb/docs/docs-configuration-workspaces.md +30 -0
  179. package/dist/packages/mcp/assets/kb/docs/docs-configuration.md +25 -0
  180. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-1-follow-a-wallet-see-what-it-does.md +51 -0
  181. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-2-the-one-key-mirror-ticket-a-proposal.md +56 -0
  182. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-3-capped-auto-mirror-default-off-less.md +47 -0
  183. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-what-is-proven-what-is-not.md +36 -0
  184. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +31 -0
  185. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-cache.md +32 -0
  186. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-health-registry.md +29 -0
  187. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-rate-limits-and-pacing.md +32 -0
  188. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-request-budget-meter.md +30 -0
  189. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-sources.md +47 -0
  190. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-the-helius-credit-meter.md +30 -0
  191. package/dist/packages/mcp/assets/kb/docs/docs-data-sources.md +27 -0
  192. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-dexterm-doctor.md +33 -0
  193. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-first-run.md +34 -0
  194. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +71 -0
  195. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-next.md +32 -0
  196. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +35 -0
  197. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-running-the-tests.md +34 -0
  198. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +44 -0
  199. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-updating.md +28 -0
  200. package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +27 -0
  201. package/dist/packages/mcp/assets/kb/docs/docs-keyboard-global-keys.md +26 -0
  202. package/dist/packages/mcp/assets/kb/docs/docs-keyboard-keys-handled-by-the-loop.md +29 -0
  203. package/dist/packages/mcp/assets/kb/docs/docs-keyboard-per-screen-keys.md +26 -0
  204. package/dist/packages/mcp/assets/kb/docs/docs-keyboard-what-shows.md +27 -0
  205. package/dist/packages/mcp/assets/kb/docs/docs-keyboard.md +26 -0
  206. package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +55 -0
  207. package/dist/packages/mcp/assets/kb/docs/docs-larp-open-link-keys.md +26 -0
  208. package/dist/packages/mcp/assets/kb/docs/docs-larp-paste-a-coin-on-the-web.md +35 -0
  209. package/dist/packages/mcp/assets/kb/docs/docs-larp-the-verdict.md +29 -0
  210. package/dist/packages/mcp/assets/kb/docs/docs-larp-timestamped-follow-up-drafts.md +35 -0
  211. package/dist/packages/mcp/assets/kb/docs/docs-larp-what-it-looks-at.md +33 -0
  212. package/dist/packages/mcp/assets/kb/docs/docs-larp.md +24 -0
  213. package/dist/packages/mcp/assets/kb/docs/docs-login-with-x-degraded-states-you-will-see.md +36 -0
  214. package/dist/packages/mcp/assets/kb/docs/docs-login-with-x-free-vs-paid-x-api-tiers-your-bill-your-limits.md +37 -0
  215. package/dist/packages/mcp/assets/kb/docs/docs-login-with-x-kol-lists-and-the-per-handle-filter.md +28 -0
  216. package/dist/packages/mcp/assets/kb/docs/docs-login-with-x-tests.md +25 -0
  217. package/dist/packages/mcp/assets/kb/docs/docs-login-with-x-what-is-read-and-when.md +35 -0
  218. package/dist/packages/mcp/assets/kb/docs/docs-login-with-x-what-you-set-up-once.md +40 -0
  219. package/dist/packages/mcp/assets/kb/docs/docs-login-with-x-where-the-tokens-live.md +27 -0
  220. package/dist/packages/mcp/assets/kb/docs/docs-login-with-x.md +26 -0
  221. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-a-live-ticket.md +42 -0
  222. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-before-you-press-size-vs-caps.md +34 -0
  223. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-devnet-signer-mainnet-token.md +35 -0
  224. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-jupiter-rate-limits.md +33 -0
  225. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-link-a-wallet.md +32 -0
  226. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-live-trading-devnet-first.md +30 -0
  227. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-mainnet.md +31 -0
  228. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-order-plans-and-standing-mandates.md +44 -0
  229. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-panic.md +31 -0
  230. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-pnl-cards.md +37 -0
  231. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-live-fire-path-devnet-the-fast-wallet.md +41 -0
  232. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-signer-process.md +34 -0
  233. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-ticket.md +31 -0
  234. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-wallet.md +34 -0
  235. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-trade-moments.md +32 -0
  236. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-what-the-live-path-keeps.md +37 -0
  237. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-your-first-live-trade.md +41 -0
  238. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +32 -0
  239. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-bubble-map-popup.md +27 -0
  240. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-build-the-native-window.md +33 -0
  241. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-config-dexterm-popup-json.md +38 -0
  242. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-docking-macos.md +27 -0
  243. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-keys-in-the-popup.md +30 -0
  244. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-market-cap-axis.md +28 -0
  245. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-security-model.md +33 -0
  246. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-token-and-feeds-popups.md +38 -0
  247. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-what-it-shows.md +30 -0
  248. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart.md +27 -0
  249. package/dist/packages/mcp/assets/kb/docs/docs-scan-cost-control.md +28 -0
  250. package/dist/packages/mcp/assets/kb/docs/docs-scan-keys-flag-command.md +34 -0
  251. package/dist/packages/mcp/assets/kb/docs/docs-scan-safety-four-layers.md +32 -0
  252. package/dist/packages/mcp/assets/kb/docs/docs-scan-states.md +27 -0
  253. package/dist/packages/mcp/assets/kb/docs/docs-scan-the-read.md +25 -0
  254. package/dist/packages/mcp/assets/kb/docs/docs-scan-triggers-and-score.md +37 -0
  255. package/dist/packages/mcp/assets/kb/docs/docs-scan.md +24 -0
  256. package/dist/packages/mcp/assets/kb/docs/docs-scorecard-frozen-canary.md +30 -0
  257. package/dist/packages/mcp/assets/kb/docs/docs-scorecard-frozen-task-1401-evaluation.md +35 -0
  258. package/dist/packages/mcp/assets/kb/docs/docs-scorecard-latest-skill-care-run.md +33 -0
  259. package/dist/packages/mcp/assets/kb/docs/docs-scorecard-links-only-larp-baseline-where-it-is-weak.md +35 -0
  260. package/dist/packages/mcp/assets/kb/docs/docs-scorecard-not-measured.md +29 -0
  261. package/dist/packages/mcp/assets/kb/docs/docs-scorecard-rug-verdict-rules.md +33 -0
  262. package/dist/packages/mcp/assets/kb/docs/docs-scorecard-source-and-rebuild.md +31 -0
  263. package/dist/packages/mcp/assets/kb/docs/docs-scorecard.md +27 -0
  264. package/dist/packages/mcp/assets/kb/docs/docs-screens-1-screener.md +35 -0
  265. package/dist/packages/mcp/assets/kb/docs/docs-screens-2-new-pairs.md +28 -0
  266. package/dist/packages/mcp/assets/kb/docs/docs-screens-3-watchlist.md +27 -0
  267. package/dist/packages/mcp/assets/kb/docs/docs-screens-4-analyst-tab.md +28 -0
  268. package/dist/packages/mcp/assets/kb/docs/docs-screens-bubble-map.md +32 -0
  269. package/dist/packages/mcp/assets/kb/docs/docs-screens-coin-images.md +32 -0
  270. package/dist/packages/mcp/assets/kb/docs/docs-screens-live-holder-and-rug-signals.md +51 -0
  271. package/dist/packages/mcp/assets/kb/docs/docs-screens-order-plans.md +26 -0
  272. package/dist/packages/mcp/assets/kb/docs/docs-screens-paper-ticket-and-positions.md +29 -0
  273. package/dist/packages/mcp/assets/kb/docs/docs-screens-the-look.md +29 -0
  274. package/dist/packages/mcp/assets/kb/docs/docs-screens-token-screen.md +36 -0
  275. package/dist/packages/mcp/assets/kb/docs/docs-screens-trending-social-and-kol-lists.md +34 -0
  276. package/dist/packages/mcp/assets/kb/docs/docs-screens.md +26 -0
  277. package/dist/packages/mcp/assets/kb/docs/docs-spectator-1-the-agent-activity-window-follow-mode.md +56 -0
  278. package/dist/packages/mcp/assets/kb/docs/docs-spectator-2-the-read-only-stream-spectate.md +42 -0
  279. package/dist/packages/mcp/assets/kb/docs/docs-spectator-3-save-and-replay-record-file-dexterm-spectate.md +41 -0
  280. package/dist/packages/mcp/assets/kb/docs/docs-spectator-follow-over-mcp-in-the-running-terminal.md +39 -0
  281. package/dist/packages/mcp/assets/kb/docs/docs-spectator-limits.md +36 -0
  282. package/dist/packages/mcp/assets/kb/docs/docs-spectator-quick-start.md +37 -0
  283. package/dist/packages/mcp/assets/kb/docs/docs-spectator-security-model.md +35 -0
  284. package/dist/packages/mcp/assets/kb/docs/docs-spectator-see-also.md +29 -0
  285. package/dist/packages/mcp/assets/kb/docs/docs-spectator-streaming-it.md +32 -0
  286. package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-three-surfaces.md +37 -0
  287. package/dist/packages/mcp/assets/kb/docs/docs-spectator-what-is-redacted.md +35 -0
  288. package/dist/packages/mcp/assets/kb/docs/docs-spectator.md +29 -0
  289. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-common-questions.md +33 -0
  290. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-doctor-lines.md +36 -0
  291. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-in-app-messages.md +42 -0
  292. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-reporting-a-problem.md +25 -0
  293. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting.md +23 -0
  294. package/dist/packages/mcp/assets/kb/docs/docs-voice-doctor.md +26 -0
  295. package/dist/packages/mcp/assets/kb/docs/docs-voice-it-clarifies-it-never-guesses.md +36 -0
  296. package/dist/packages/mcp/assets/kb/docs/docs-voice-latency.md +38 -0
  297. package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +32 -0
  298. package/dist/packages/mcp/assets/kb/docs/docs-voice-privacy.md +26 -0
  299. package/dist/packages/mcp/assets/kb/docs/docs-voice-still-unverified.md +27 -0
  300. package/dist/packages/mcp/assets/kb/docs/docs-voice-the-voice-neural-by-default.md +48 -0
  301. package/dist/packages/mcp/assets/kb/docs/docs-voice-what-it-can-never-do.md +27 -0
  302. package/dist/packages/mcp/assets/kb/docs/docs-voice-what-you-can-say.md +38 -0
  303. package/dist/packages/mcp/assets/kb/docs/docs-voice.md +25 -0
  304. package/dist/packages/mcp/src/app-help.js +258 -0
  305. package/dist/packages/mcp/src/firewall.js +2 -2
  306. package/dist/packages/mcp/src/index.js +5 -1
  307. package/dist/packages/mcp/src/server.js +129 -4
  308. package/dist/packages/mcp/src/spectator-sink.js +144 -0
  309. package/dist/packages/packs/src/index.js +2 -0
  310. package/dist/packages/packs/src/skill-care.js +206 -0
  311. package/dist/packages/popup/src/pages/spectator.css +45 -0
  312. package/dist/packages/popup/src/pages/spectator.html +25 -0
  313. package/dist/packages/popup/src/pages/spectator.js +74 -0
  314. package/dist/packages/popup/src/selftest.js +19 -0
  315. package/dist/packages/popup/src/server.js +7 -1
  316. package/dist/packages/tui/src/keymap.js +10 -0
  317. package/dist/packages/voice/src/controller.js +42 -1
  318. package/dist/packages/voice/src/doctor.js +25 -8
  319. package/dist/packages/voice/src/engines.js +8 -1
  320. package/dist/packages/voice/src/index.js +3 -1
  321. package/dist/packages/voice/src/speakable.js +111 -0
  322. package/dist/packages/voice/src/stt/whisper.js +4 -2
  323. package/dist/packages/voice/src/tts/piper-setup.js +229 -0
  324. package/dist/packages/voice/src/tts/piper.js +309 -66
  325. package/dist/packages/voice/src/tts/say.js +3 -2
  326. package/dist/packages/voice/src/tts/voices.js +8 -5
  327. 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 engine
2
- // (rhasspy/piper, MIT) that sounds markedly better than espeak-ng. Off macOS it is the PREFERRED
3
- // speech-out engine; espeak-ng stays the last-resort baseline (see engines.ts). Verified in Docker
4
- // (task-1397/task-1408): the prebuilt Linux binary + the en_GB-alan-medium voice write a valid,
5
- // non-silent 22.05 kHz WAV that whisper.cpp transcribes almost word-for-word.
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
- // Piper has no audio output of its own: it synthesises to a WAV, which we then play through the
15
- // platform player (aplay / paplay / ffplay — detected and reported by the doctor). Containers have no
16
- // audio device, so real playback is unverified; synthesis-to-file IS verified (synthToFile + whisper).
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
- /** The Piper voice dexterm defaults to off macOS: a British male, medium quality (local, no key). */
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(homedir(), '.dexterm', 'models', `${DEFAULT_PIPER_VOICE}.onnx`);
27
- /** Audio players we can hand a WAV to, best-first. Reported by the doctor so the user knows which plays. */
28
- export const PIPER_PLAYERS = ['aplay', 'paplay', 'ffplay'];
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. We look at an explicit override, then the dexterm-local install
47
- * (`~/.dexterm/piper/piper`, where the prebuilt release is extracted), then PATH. NOTE: Debian's
48
- * `apt install piper` is an UNRELATED gaming-mouse tool, so the doctor steers users to the rhasspy
49
- * prebuilt release rather than apt; a wrong `piper` on PATH simply fails to synth and voice degrades
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(homedir(), '.dexterm', 'piper', 'piper'))
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
- switch (player) {
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: the binary, the voice model and a player are all present. */
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 resolvePiperBin() !== null && existsSync(this.model) && pickPlayer() !== null;
180
+ return resolvePiperRuntime() !== null && existsSync(this.model) && pickPlayer() !== null;
88
181
  }
89
- /** True when Piper can SYNTHESISE here (binary + model) even with no audio device (used for the proof). */
182
+ /** True when Piper can SYNTHESISE here (runtime + model) even with no audio device (used for the proof). */
90
183
  canSynth() {
91
- return resolvePiperBin() !== null && existsSync(this.model);
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 binary/model or a
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 bin = resolvePiperBin();
194
+ const runtime = resolvePiperRuntime();
102
195
  const safe = sanitizeSpeech(text);
103
196
  return new Promise((resolve, reject) => {
104
- if (!bin) {
105
- reject(new Error('piper binary not found'));
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(bin, args, { stdio: ['pipe', 'ignore', 'pipe'] });
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
- // rate: map the words-per-minute feel to Piper's length_scale (lower rate → slower → larger scale).
138
- const lengthScale = typeof opts.rate === 'number' && opts.rate > 0 ? Math.max(0.5, Math.min(2, 190 / opts.rate)) : undefined;
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
- const dir = mkdtempSync(join(tmpdir(), 'dexterm-piper-'));
144
- const wav = join(dir, 'speech.wav');
145
- const cleanup = () => { try {
146
- rmSync(dir, { recursive: true, force: true });
147
- }
148
- catch { /* ignore */ } };
149
- const finish = () => {
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
- done,
160
- stop: () => {
161
- if (stopped)
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
- stopped = true;
164
- if (this.current === handle)
165
- this.current = null;
395
+ }
396
+ const wav = queue.shift();
166
397
  try {
167
- child?.kill('SIGTERM');
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 { /* already gone */ }
170
- cleanup();
171
- resolve();
172
- },
408
+ catch {
409
+ if (!stopped)
410
+ setTimeout(playNext, 0);
411
+ }
412
+ };
413
+ playNext();
173
414
  };
174
- this.current = handle;
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
- child = spawn(player, playerArgs(player, wav), { stdio: 'ignore' });
185
- child.on('spawn', () => { if (!stopped)
186
- opts.onFirstAudio?.(); });
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
- finish();
425
+ settle();
426
+ return;
192
427
  }
193
- }).catch(finish); // synth failed (bad binary/model) → degrade to text
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
- export const DEFAULT_RATE = 190;
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. Eddy's are
62
- * the values of the audition sample chosen (2026-10-15: 06-eddy-uk-deep-slow = rate 168, pbas 30).
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.2",
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
  }