dexterm-cli 0.0.0-stage → 0.1.2

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 (377) hide show
  1. package/LICENSE +661 -0
  2. package/dist/packages/agents/src/audit.js +122 -0
  3. package/dist/packages/agents/src/bus.js +394 -0
  4. package/dist/packages/agents/src/executor.js +202 -0
  5. package/dist/packages/agents/src/gate.js +179 -0
  6. package/dist/packages/agents/src/identity.js +218 -0
  7. package/dist/packages/agents/src/index.js +15 -0
  8. package/dist/packages/agents/src/proposals.js +153 -0
  9. package/dist/packages/agents/src/schema.js +87 -0
  10. package/dist/packages/agents/src/sdk.js +140 -0
  11. package/dist/packages/agents/src/tiers.js +119 -0
  12. package/dist/packages/agents/src/tools.js +72 -0
  13. package/dist/packages/agents/src/ws.js +158 -0
  14. package/dist/packages/cli/assets/claude/analyst-system.md +76 -0
  15. package/dist/packages/cli/assets/claude/plugin/.claude-plugin/plugin.json +5 -0
  16. package/dist/packages/cli/assets/claude/plugin/skills/REGISTRY.json +16 -0
  17. package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +75 -0
  18. package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +74 -0
  19. package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +63 -0
  20. package/dist/packages/cli/assets/claude/plugin/skills/trench-scan/SKILL.md +44 -0
  21. package/dist/packages/cli/assets/claude/schemas/larp.schema.json +117 -0
  22. package/dist/packages/cli/assets/claude/schemas/trench-scan.schema.json +71 -0
  23. package/dist/packages/cli/assets/claude/schemas/verdict.schema.json +115 -0
  24. package/dist/packages/cli/bin/dexterm.mjs +11 -0
  25. package/dist/packages/cli/src/agent-cmd.js +336 -0
  26. package/dist/packages/cli/src/agent-roster.js +26 -0
  27. package/dist/packages/cli/src/alerts/adapters/desktop.js +40 -0
  28. package/dist/packages/cli/src/alerts/adapters/discord.js +36 -0
  29. package/dist/packages/cli/src/alerts/adapters/telegram.js +43 -0
  30. package/dist/packages/cli/src/alerts/adapters/toast.js +16 -0
  31. package/dist/packages/cli/src/alerts/creds.js +83 -0
  32. package/dist/packages/cli/src/alerts/format.js +41 -0
  33. package/dist/packages/cli/src/alerts/index.js +10 -0
  34. package/dist/packages/cli/src/alerts/notifier.js +53 -0
  35. package/dist/packages/cli/src/alerts/route.js +19 -0
  36. package/dist/packages/cli/src/alerts/types.js +1 -0
  37. package/dist/packages/cli/src/alerts-cmd.js +212 -0
  38. package/dist/packages/cli/src/analyst/backend.js +526 -0
  39. package/dist/packages/cli/src/analyst/results.js +38 -0
  40. package/dist/packages/cli/src/analyst/session.js +319 -0
  41. package/dist/packages/cli/src/audit.js +51 -0
  42. package/dist/packages/cli/src/backtest-cmd.js +208 -0
  43. package/dist/packages/cli/src/boot-state.js +62 -0
  44. package/dist/packages/cli/src/coin-target.js +2 -0
  45. package/dist/packages/cli/src/custom.js +261 -0
  46. package/dist/packages/cli/src/display.js +191 -0
  47. package/dist/packages/cli/src/doctor.js +375 -0
  48. package/dist/packages/cli/src/export.js +235 -0
  49. package/dist/packages/cli/src/feed.js +634 -0
  50. package/dist/packages/cli/src/fixtures.js +275 -0
  51. package/dist/packages/cli/src/follow-cmd.js +221 -0
  52. package/dist/packages/cli/src/images-config.js +59 -0
  53. package/dist/packages/cli/src/index.js +31 -0
  54. package/dist/packages/cli/src/kb-skill-cmd.js +240 -0
  55. package/dist/packages/cli/src/larp-record.js +21 -0
  56. package/dist/packages/cli/src/live/copy.js +80 -0
  57. package/dist/packages/cli/src/live/flow.js +233 -0
  58. package/dist/packages/cli/src/live/index.js +511 -0
  59. package/dist/packages/cli/src/live/intel.js +227 -0
  60. package/dist/packages/cli/src/live/newpairs.js +334 -0
  61. package/dist/packages/cli/src/live/screens/newpairs-live.js +219 -0
  62. package/dist/packages/cli/src/live/screens/token-live.js +459 -0
  63. package/dist/packages/cli/src/live/screens/watchlist-live.js +149 -0
  64. package/dist/packages/cli/src/live/token.js +585 -0
  65. package/dist/packages/cli/src/live/trade.js +929 -0
  66. package/dist/packages/cli/src/live/watchlist.js +178 -0
  67. package/dist/packages/cli/src/main.js +5785 -0
  68. package/dist/packages/cli/src/mcp-cmd.js +326 -0
  69. package/dist/packages/cli/src/narrative-cmd.js +76 -0
  70. package/dist/packages/cli/src/pack-cmd.js +323 -0
  71. package/dist/packages/cli/src/plan-cmd.js +278 -0
  72. package/dist/packages/cli/src/popup.js +810 -0
  73. package/dist/packages/cli/src/rules-cmd.js +131 -0
  74. package/dist/packages/cli/src/rules.js +358 -0
  75. package/dist/packages/cli/src/scan/command.js +57 -0
  76. package/dist/packages/cli/src/scan/engine.js +419 -0
  77. package/dist/packages/cli/src/scan/observe.js +64 -0
  78. package/dist/packages/cli/src/scan/ui.js +98 -0
  79. package/dist/packages/cli/src/screener/filter.js +241 -0
  80. package/dist/packages/cli/src/screener/presets.js +80 -0
  81. package/dist/packages/cli/src/screener/rows.js +146 -0
  82. package/dist/packages/cli/src/screener/rug.js +50 -0
  83. package/dist/packages/cli/src/screener/sort.js +52 -0
  84. package/dist/packages/cli/src/screener/view.js +183 -0
  85. package/dist/packages/cli/src/screens/analyst-session.js +343 -0
  86. package/dist/packages/cli/src/screens/analyst.js +326 -0
  87. package/dist/packages/cli/src/screens/boot.js +74 -0
  88. package/dist/packages/cli/src/screens/bubbles.js +255 -0
  89. package/dist/packages/cli/src/screens/coin.js +33 -0
  90. package/dist/packages/cli/src/screens/follows.js +201 -0
  91. package/dist/packages/cli/src/screens/kol.js +90 -0
  92. package/dist/packages/cli/src/screens/narrative.js +90 -0
  93. package/dist/packages/cli/src/screens/newpairs.js +150 -0
  94. package/dist/packages/cli/src/screens/overlay.js +257 -0
  95. package/dist/packages/cli/src/screens/plans.js +126 -0
  96. package/dist/packages/cli/src/screens/positions.js +176 -0
  97. package/dist/packages/cli/src/screens/rules.js +170 -0
  98. package/dist/packages/cli/src/screens/screener.js +459 -0
  99. package/dist/packages/cli/src/screens/social-panel.js +107 -0
  100. package/dist/packages/cli/src/screens/ticket.js +342 -0
  101. package/dist/packages/cli/src/screens/token.js +312 -0
  102. package/dist/packages/cli/src/screens/trending.js +181 -0
  103. package/dist/packages/cli/src/screens/watchlist.js +104 -0
  104. package/dist/packages/cli/src/skill-eval-build.js +109 -0
  105. package/dist/packages/cli/src/social/demo.js +96 -0
  106. package/dist/packages/cli/src/social/view.js +250 -0
  107. package/dist/packages/cli/src/state.js +463 -0
  108. package/dist/packages/cli/src/theme-cmd.js +126 -0
  109. package/dist/packages/cli/src/trade.js +319 -0
  110. package/dist/packages/cli/src/update.js +294 -0
  111. package/dist/packages/cli/src/version.js +3 -0
  112. package/dist/packages/cli/src/voice-cmd.js +119 -0
  113. package/dist/packages/cli/src/voice-hud.js +42 -0
  114. package/dist/packages/cli/src/wallet-cmd.js +440 -0
  115. package/dist/packages/cli/src/wallet-command.js +31 -0
  116. package/dist/packages/cli/src/watch-cmd.js +190 -0
  117. package/dist/packages/cli/src/widgets.js +208 -0
  118. package/dist/packages/cli/src/workspace-cmd.js +68 -0
  119. package/dist/packages/cli/src/x-cmd.js +154 -0
  120. package/dist/packages/custom/src/config.js +213 -0
  121. package/dist/packages/custom/src/index.js +10 -0
  122. package/dist/packages/custom/src/keymap.js +107 -0
  123. package/dist/packages/custom/src/layout.js +235 -0
  124. package/dist/packages/custom/src/packs.js +633 -0
  125. package/dist/packages/custom/src/rules.js +704 -0
  126. package/dist/packages/custom/src/schema.js +538 -0
  127. package/dist/packages/custom/src/themes.js +176 -0
  128. package/dist/packages/custom/src/widget-html.js +222 -0
  129. package/dist/packages/custom/src/workspace.js +141 -0
  130. package/dist/packages/data/src/analyst-actions.js +133 -0
  131. package/dist/packages/data/src/budget.js +88 -0
  132. package/dist/packages/data/src/cache.js +142 -0
  133. package/dist/packages/data/src/candles.js +168 -0
  134. package/dist/packages/data/src/clock.js +64 -0
  135. package/dist/packages/data/src/coin-target.js +82 -0
  136. package/dist/packages/data/src/credits.js +137 -0
  137. package/dist/packages/data/src/detail.js +97 -0
  138. package/dist/packages/data/src/fast-lane.js +321 -0
  139. package/dist/packages/data/src/fixtures.js +135 -0
  140. package/dist/packages/data/src/health.js +245 -0
  141. package/dist/packages/data/src/http.js +277 -0
  142. package/dist/packages/data/src/images/cache.js +141 -0
  143. package/dist/packages/data/src/images/decode-worker.js +28 -0
  144. package/dist/packages/data/src/images/decode.js +81 -0
  145. package/dist/packages/data/src/images/fetch.js +162 -0
  146. package/dist/packages/data/src/images/gif.js +214 -0
  147. package/dist/packages/data/src/images/index.js +10 -0
  148. package/dist/packages/data/src/images/jpeg.js +544 -0
  149. package/dist/packages/data/src/images/png.js +310 -0
  150. package/dist/packages/data/src/images/service.js +334 -0
  151. package/dist/packages/data/src/images/types.js +27 -0
  152. package/dist/packages/data/src/index.js +211 -0
  153. package/dist/packages/data/src/intel/helius-history.js +70 -0
  154. package/dist/packages/data/src/intel/holder-class.js +125 -0
  155. package/dist/packages/data/src/intel/rules.js +421 -0
  156. package/dist/packages/data/src/intel/scan.js +170 -0
  157. package/dist/packages/data/src/larp/evidence.js +288 -0
  158. package/dist/packages/data/src/larp/link-baseline.js +196 -0
  159. package/dist/packages/data/src/larp/links.js +137 -0
  160. package/dist/packages/data/src/larp/robots.js +90 -0
  161. package/dist/packages/data/src/larp/safe-fetch.js +389 -0
  162. package/dist/packages/data/src/limiter.js +341 -0
  163. package/dist/packages/data/src/model.js +12 -0
  164. package/dist/packages/data/src/paper.js +331 -0
  165. package/dist/packages/data/src/rpc-config.js +126 -0
  166. package/dist/packages/data/src/rpc-pool.js +166 -0
  167. package/dist/packages/data/src/social/chain.js +83 -0
  168. package/dist/packages/data/src/social/dedupe.js +205 -0
  169. package/dist/packages/data/src/social/feed.js +462 -0
  170. package/dist/packages/data/src/social/firewall.js +110 -0
  171. package/dist/packages/data/src/social/index.js +11 -0
  172. package/dist/packages/data/src/social/kol.js +269 -0
  173. package/dist/packages/data/src/social/model.js +11 -0
  174. package/dist/packages/data/src/social/rank.js +184 -0
  175. package/dist/packages/data/src/social/rss.js +113 -0
  176. package/dist/packages/data/src/social/summary.js +104 -0
  177. package/dist/packages/data/src/social/venues.js +186 -0
  178. package/dist/packages/data/src/sources/dexscreener.js +184 -0
  179. package/dist/packages/data/src/sources/geckoterminal.js +133 -0
  180. package/dist/packages/data/src/sources/goplus.js +161 -0
  181. package/dist/packages/data/src/sources/jupiter-swap.js +236 -0
  182. package/dist/packages/data/src/sources/jupiter.js +198 -0
  183. package/dist/packages/data/src/sources/pda.js +113 -0
  184. package/dist/packages/data/src/sources/pump-curve.js +58 -0
  185. package/dist/packages/data/src/sources/pumpportal.js +252 -0
  186. package/dist/packages/data/src/sources/rugcheck.js +71 -0
  187. package/dist/packages/data/src/sources/solana-rpc.js +299 -0
  188. package/dist/packages/data/src/sources/solana-ws.js +287 -0
  189. package/dist/packages/data/src/store.js +289 -0
  190. package/dist/packages/data/src/trade/bridge.js +464 -0
  191. package/dist/packages/data/src/trade/caps.js +323 -0
  192. package/dist/packages/data/src/trade/copy/auto.js +385 -0
  193. package/dist/packages/data/src/trade/copy/follows.js +103 -0
  194. package/dist/packages/data/src/trade/copy/index.js +9 -0
  195. package/dist/packages/data/src/trade/copy/mirror.js +91 -0
  196. package/dist/packages/data/src/trade/copy/stats.js +120 -0
  197. package/dist/packages/data/src/trade/copy/tracker.js +158 -0
  198. package/dist/packages/data/src/trade/copy/trades.js +87 -0
  199. package/dist/packages/data/src/trade/copy/types.js +6 -0
  200. package/dist/packages/data/src/trade/executor.js +933 -0
  201. package/dist/packages/data/src/trade/kill-switch.js +47 -0
  202. package/dist/packages/data/src/trade/link-store.js +52 -0
  203. package/dist/packages/data/src/trade/plans/confirm.js +157 -0
  204. package/dist/packages/data/src/trade/plans/engine.js +784 -0
  205. package/dist/packages/data/src/trade/plans/executor-live.js +220 -0
  206. package/dist/packages/data/src/trade/plans/executor-paper.js +44 -0
  207. package/dist/packages/data/src/trade/plans/executor.js +11 -0
  208. package/dist/packages/data/src/trade/plans/index.js +12 -0
  209. package/dist/packages/data/src/trade/plans/journal.js +77 -0
  210. package/dist/packages/data/src/trade/plans/mandate.js +129 -0
  211. package/dist/packages/data/src/trade/plans/plan.js +179 -0
  212. package/dist/packages/data/src/trade/plans/service.js +168 -0
  213. package/dist/packages/data/src/trade/plans/types.js +6 -0
  214. package/dist/packages/data/src/trade/rpc-exec.js +229 -0
  215. package/dist/packages/data/src/trade/signer/client.js +197 -0
  216. package/dist/packages/data/src/trade/signer/fast-wallet.js +154 -0
  217. package/dist/packages/data/src/trade/signer/host.js +97 -0
  218. package/dist/packages/data/src/trade/signer/main.js +121 -0
  219. package/dist/packages/data/src/trade/signer/probe-builder.js +85 -0
  220. package/dist/packages/data/src/trade/signer/protocol.js +92 -0
  221. package/dist/packages/data/src/trade/signer/service.js +400 -0
  222. package/dist/packages/data/src/trade/signer/sol-usd-feed.js +98 -0
  223. package/dist/packages/data/src/trade/sol-usd.js +86 -0
  224. package/dist/packages/data/src/trade/swap-build.js +108 -0
  225. package/dist/packages/data/src/trade/token-delta.js +201 -0
  226. package/dist/packages/data/src/trade/tx-decode.js +632 -0
  227. package/dist/packages/data/src/util.js +24 -0
  228. package/dist/packages/data/src/watchlist.js +176 -0
  229. package/dist/packages/data/src/x/api.js +256 -0
  230. package/dist/packages/data/src/x/budget.js +66 -0
  231. package/dist/packages/data/src/x/config.js +49 -0
  232. package/dist/packages/data/src/x/index.js +10 -0
  233. package/dist/packages/data/src/x/mock-server.js +217 -0
  234. package/dist/packages/data/src/x/oauth.js +242 -0
  235. package/dist/packages/data/src/x/pkce.js +21 -0
  236. package/dist/packages/data/src/x/source.js +285 -0
  237. package/dist/packages/data/src/x/token-store.js +134 -0
  238. package/dist/packages/data/src/x/transport.js +71 -0
  239. package/dist/packages/data/vendor/solana-web3/LICENSE +20 -0
  240. package/dist/packages/data/vendor/solana-web3/NOTICE.md +13 -0
  241. package/dist/packages/data/vendor/solana-web3/index.iife.min.js +20 -0
  242. package/dist/packages/mcp/assets/cases.json +1194 -0
  243. package/dist/packages/mcp/assets/kb/entries/cabal-and-coordinated-launches.md +28 -0
  244. package/dist/packages/mcp/assets/kb/entries/common-rug-patterns.md +40 -0
  245. package/dist/packages/mcp/assets/kb/entries/dbc-mechanics.md +30 -0
  246. package/dist/packages/mcp/assets/kb/entries/dev-wallet-behaviour.md +33 -0
  247. package/dist/packages/mcp/assets/kb/entries/glossary.md +35 -0
  248. package/dist/packages/mcp/assets/kb/entries/holder-concentration.md +30 -0
  249. package/dist/packages/mcp/assets/kb/entries/jito-bundles.md +29 -0
  250. package/dist/packages/mcp/assets/kb/entries/launchlab-curve.md +31 -0
  251. package/dist/packages/mcp/assets/kb/entries/lp-burn-vs-lock.md +31 -0
  252. package/dist/packages/mcp/assets/kb/entries/metadata-and-copycats.md +29 -0
  253. package/dist/packages/mcp/assets/kb/entries/mint-freeze-authority.md +31 -0
  254. package/dist/packages/mcp/assets/kb/entries/narrative-lifecycle.md +32 -0
  255. package/dist/packages/mcp/assets/kb/entries/pump-bonding-curve.md +33 -0
  256. package/dist/packages/mcp/assets/kb/entries/pumpswap-migration.md +29 -0
  257. package/dist/packages/mcp/assets/kb/entries/sniping-and-snipers.md +29 -0
  258. package/dist/packages/mcp/assets/kb/entries/token2022-extensions-risk.md +32 -0
  259. package/dist/packages/mcp/assets/kb/entries/wash-trading-fake-volume.md +33 -0
  260. package/dist/packages/mcp/assets/skills/rug-check.md +52 -0
  261. package/dist/packages/mcp/assets/skills/token-dossier.md +57 -0
  262. package/dist/packages/mcp/assets/skills/trench-scan.md +40 -0
  263. package/dist/packages/mcp/bin/dexterm-mcp.mjs +16 -0
  264. package/dist/packages/mcp/src/dossier.js +78 -0
  265. package/dist/packages/mcp/src/firewall.js +21 -0
  266. package/dist/packages/mcp/src/index.js +22 -0
  267. package/dist/packages/mcp/src/server.js +936 -0
  268. package/dist/packages/mcp/src/user-content.js +434 -0
  269. package/dist/packages/packs/src/backtest.js +183 -0
  270. package/dist/packages/packs/src/digest.js +95 -0
  271. package/dist/packages/packs/src/firewall.js +57 -0
  272. package/dist/packages/packs/src/index.js +20 -0
  273. package/dist/packages/packs/src/larp-eval.js +100 -0
  274. package/dist/packages/packs/src/narrative.js +309 -0
  275. package/dist/packages/packs/src/reason.js +135 -0
  276. package/dist/packages/packs/src/rug-model.js +137 -0
  277. package/dist/packages/packs/src/schema.js +54 -0
  278. package/dist/packages/packs/src/skill-backtest.js +300 -0
  279. package/dist/packages/packs/src/token-dossier.js +174 -0
  280. package/dist/packages/packs/src/verdicts.js +17 -0
  281. package/dist/packages/packs/src/wallet-dossier.js +105 -0
  282. package/dist/packages/popup/src/config.js +71 -0
  283. package/dist/packages/popup/src/dock.js +271 -0
  284. package/dist/packages/popup/src/host.js +210 -0
  285. package/dist/packages/popup/src/index.js +12 -0
  286. package/dist/packages/popup/src/loopback.js +53 -0
  287. package/dist/packages/popup/src/open.js +183 -0
  288. package/dist/packages/popup/src/pages/bubbles-lib.js +160 -0
  289. package/dist/packages/popup/src/pages/bubbles.css +23 -0
  290. package/dist/packages/popup/src/pages/bubbles.html +36 -0
  291. package/dist/packages/popup/src/pages/bubbles.js +214 -0
  292. package/dist/packages/popup/src/pages/chart-lib.js +262 -0
  293. package/dist/packages/popup/src/pages/chart.css +50 -0
  294. package/dist/packages/popup/src/pages/chart.html +56 -0
  295. package/dist/packages/popup/src/pages/chart.js +554 -0
  296. package/dist/packages/popup/src/pages/common.css +34 -0
  297. package/dist/packages/popup/src/pages/common.js +187 -0
  298. package/dist/packages/popup/src/pages/feeds.html +20 -0
  299. package/dist/packages/popup/src/pages/feeds.js +77 -0
  300. package/dist/packages/popup/src/pages/narrative.html +20 -0
  301. package/dist/packages/popup/src/pages/narrative.js +61 -0
  302. package/dist/packages/popup/src/pages/plans.css +30 -0
  303. package/dist/packages/popup/src/pages/plans.html +26 -0
  304. package/dist/packages/popup/src/pages/plans.js +82 -0
  305. package/dist/packages/popup/src/pages/roster.css +41 -0
  306. package/dist/packages/popup/src/pages/roster.html +25 -0
  307. package/dist/packages/popup/src/pages/roster.js +75 -0
  308. package/dist/packages/popup/src/pages/token.html +20 -0
  309. package/dist/packages/popup/src/pages/token.js +112 -0
  310. package/dist/packages/popup/src/pages/widget-sdk.js +224 -0
  311. package/dist/packages/popup/src/pages/widget.css +5 -0
  312. package/dist/packages/popup/src/pages/widget.html +21 -0
  313. package/dist/packages/popup/src/pages/widget.js +85 -0
  314. package/dist/packages/popup/src/protocol.js +31 -0
  315. package/dist/packages/popup/src/selftest.js +156 -0
  316. package/dist/packages/popup/src/server.js +582 -0
  317. package/dist/packages/popup/src/sinkproxy.js +167 -0
  318. package/dist/packages/popup/src/widget-html.js +231 -0
  319. package/dist/packages/popup/vendor/lightweight-charts/LICENSE +201 -0
  320. package/dist/packages/popup/vendor/lightweight-charts/NOTICE +2 -0
  321. package/dist/packages/popup/vendor/lightweight-charts/README.md +30 -0
  322. package/dist/packages/popup/vendor/lightweight-charts/VERSION +1 -0
  323. package/dist/packages/popup/vendor/lightweight-charts/lightweight-charts.standalone.production.js +7 -0
  324. package/dist/packages/recorder/bin/dexterm-recorder.mjs +11 -0
  325. package/dist/packages/recorder/src/cli.js +113 -0
  326. package/dist/packages/recorder/src/export.js +71 -0
  327. package/dist/packages/recorder/src/index.js +20 -0
  328. package/dist/packages/recorder/src/labels.js +151 -0
  329. package/dist/packages/recorder/src/open.js +29 -0
  330. package/dist/packages/recorder/src/recorder.js +553 -0
  331. package/dist/packages/recorder/src/schedule.js +43 -0
  332. package/dist/packages/recorder/src/schema.js +135 -0
  333. package/dist/packages/recorder/src/snapshot.js +191 -0
  334. package/dist/packages/recorder/src/sweep.js +52 -0
  335. package/dist/packages/recorder/src/verdicts.js +52 -0
  336. package/dist/packages/tui/src/bubbles.js +415 -0
  337. package/dist/packages/tui/src/canvas.js +370 -0
  338. package/dist/packages/tui/src/chrome.js +85 -0
  339. package/dist/packages/tui/src/color.js +133 -0
  340. package/dist/packages/tui/src/flush.js +62 -0
  341. package/dist/packages/tui/src/fmt.js +188 -0
  342. package/dist/packages/tui/src/fx-trade.js +278 -0
  343. package/dist/packages/tui/src/fx.js +527 -0
  344. package/dist/packages/tui/src/images/blocks.js +141 -0
  345. package/dist/packages/tui/src/images/coin.js +22 -0
  346. package/dist/packages/tui/src/images/detect.js +140 -0
  347. package/dist/packages/tui/src/images/index.js +7 -0
  348. package/dist/packages/tui/src/images/layer.js +146 -0
  349. package/dist/packages/tui/src/images/pixels.js +157 -0
  350. package/dist/packages/tui/src/images/protocols.js +87 -0
  351. package/dist/packages/tui/src/index.js +18 -0
  352. package/dist/packages/tui/src/keymap.js +357 -0
  353. package/dist/packages/tui/src/layout.js +11 -0
  354. package/dist/packages/tui/src/raster-font.js +195 -0
  355. package/dist/packages/tui/src/raster.js +213 -0
  356. package/dist/packages/tui/src/sound.js +177 -0
  357. package/dist/packages/tui/src/theme.js +182 -0
  358. package/dist/packages/tui/src/widgets.js +598 -0
  359. package/dist/packages/voice/src/controller.js +407 -0
  360. package/dist/packages/voice/src/doctor.js +131 -0
  361. package/dist/packages/voice/src/download.js +184 -0
  362. package/dist/packages/voice/src/engines.js +84 -0
  363. package/dist/packages/voice/src/index.js +20 -0
  364. package/dist/packages/voice/src/router.js +502 -0
  365. package/dist/packages/voice/src/stt/apple-helper.swift.js +103 -0
  366. package/dist/packages/voice/src/stt/apple.js +179 -0
  367. package/dist/packages/voice/src/stt/fake.js +34 -0
  368. package/dist/packages/voice/src/stt/whisper.js +112 -0
  369. package/dist/packages/voice/src/summary.js +44 -0
  370. package/dist/packages/voice/src/tts/espeak.js +131 -0
  371. package/dist/packages/voice/src/tts/fake.js +30 -0
  372. package/dist/packages/voice/src/tts/piper.js +206 -0
  373. package/dist/packages/voice/src/tts/say.js +132 -0
  374. package/dist/packages/voice/src/tts/voices.js +206 -0
  375. package/dist/packages/voice/src/types.js +3 -0
  376. package/package.json +38 -4
  377. package/README.md +0 -3
@@ -0,0 +1,206 @@
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.
6
+ //
7
+ // It carries the SAME contract as the macOS `say` and espeak engines:
8
+ // - text is SANITISED (sanitizeSpeech strips `[[…]]`, control chars, a leading `-`, caps length)
9
+ // - text is delivered on STDIN, never as an argv word
10
+ // - a new speak() cleanly INTERRUPTS the current one — one queue, never two voices at once
11
+ // - the voice is fixed by the downloaded MODEL, never by a macOS voice name (so config.voice.name,
12
+ // which on a Mac is e.g. "Eddy (English (UK))", is NEVER passed verbatim to Piper)
13
+ //
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).
17
+ import { spawn } from 'node:child_process';
18
+ import { existsSync, mkdtempSync, rmSync } from 'node:fs';
19
+ import { homedir, tmpdir } from 'node:os';
20
+ import { join } from 'node:path';
21
+ import { sanitizeSpeech } from "./voices.js";
22
+ /** The Piper voice dexterm defaults to off macOS: a British male, medium quality (local, no key). */
23
+ export const DEFAULT_PIPER_VOICE = 'en_GB-alan-medium';
24
+ /** Where the downloaded Piper voice model lives (see `dexterm voice download piper-voice`). */
25
+ 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'];
29
+ function which(bin) {
30
+ if (!bin)
31
+ return null;
32
+ if (bin.includes('/') || bin.includes('\\'))
33
+ return existsSync(bin) ? bin : null;
34
+ const sep = process.platform === 'win32' ? ';' : ':';
35
+ for (const p of (process.env.PATH ?? '').split(sep)) {
36
+ if (!p)
37
+ continue;
38
+ if (existsSync(join(p, bin)))
39
+ return join(p, bin);
40
+ if (process.platform === 'win32' && existsSync(join(p, `${bin}.exe`)))
41
+ return join(p, `${bin}.exe`);
42
+ }
43
+ return null;
44
+ }
45
+ /**
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).
51
+ */
52
+ export function resolvePiperBin() {
53
+ return which(process.env.DEXTERM_PIPER_BIN || '')
54
+ || which(join(homedir(), '.dexterm', 'piper', 'piper'))
55
+ || which('piper');
56
+ }
57
+ /** The player that will play Piper's WAV, or null when none is installed. */
58
+ export function pickPlayer() {
59
+ const override = process.env.DEXTERM_AUDIO_PLAYER;
60
+ if (override)
61
+ return which(override) ? override : null;
62
+ for (const p of PIPER_PLAYERS)
63
+ if (which(p))
64
+ return p;
65
+ return null;
66
+ }
67
+ /** Argv to play a finished WAV through the given player (quiet, exits on end, no window). */
68
+ function playerArgs(player, wav) {
69
+ switch (player) {
70
+ case 'ffplay': return ['-nodisp', '-autoexit', '-loglevel', 'quiet', wav];
71
+ case 'paplay': return [wav];
72
+ case 'aplay':
73
+ default: return ['-q', wav];
74
+ }
75
+ }
76
+ export class PiperTts {
77
+ name = 'piper';
78
+ label = 'Piper (neural, local, no key)';
79
+ model;
80
+ /** The utterance currently playing. A new speak() stops it first, so there is only ever one. */
81
+ current = null;
82
+ constructor(model = PIPER_MODEL) { this.model = model; }
83
+ /** The voice id this engine speaks with (the model basename), for the status line. */
84
+ 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. */
86
+ async available() {
87
+ return resolvePiperBin() !== null && existsSync(this.model) && pickPlayer() !== null;
88
+ }
89
+ /** True when Piper can SYNTHESISE here (binary + model) even with no audio device (used for the proof). */
90
+ canSynth() {
91
+ return resolvePiperBin() !== null && existsSync(this.model);
92
+ }
93
+ /** Piper's voice is the model; there is no per-machine installed-voice list to cycle. */
94
+ async listVoices() { return [DEFAULT_PIPER_VOICE]; }
95
+ /**
96
+ * Synthesise `text` to a WAV at `outPath`. Resolves on success, rejects on a missing binary/model or a
97
+ * non-zero exit. This is the engine's testable, audio-device-free core — the Docker proof and the unit
98
+ * tests drive it directly. The text is sanitised and delivered on stdin, exactly as speak() does.
99
+ */
100
+ synthToFile(text, outPath, opts = {}) {
101
+ const bin = resolvePiperBin();
102
+ const safe = sanitizeSpeech(text);
103
+ return new Promise((resolve, reject) => {
104
+ if (!bin) {
105
+ reject(new Error('piper binary not found'));
106
+ return;
107
+ }
108
+ const args = ['--model', this.model, '--output_file', outPath];
109
+ if (typeof opts.lengthScale === 'number' && Number.isFinite(opts.lengthScale)) {
110
+ args.push('--length_scale', String(opts.lengthScale)); // >1 = slower speech
111
+ }
112
+ let child;
113
+ try {
114
+ child = spawn(bin, args, { stdio: ['pipe', 'ignore', 'pipe'] });
115
+ }
116
+ catch (e) {
117
+ reject(e);
118
+ return;
119
+ }
120
+ let err = '';
121
+ child.stderr?.on('data', (d) => { err += d; });
122
+ child.stdin?.on('error', () => { });
123
+ // Piper reads one line of text from stdin; the sanitiser already collapsed newlines to one line.
124
+ try {
125
+ child.stdin?.end(safe + '\n');
126
+ }
127
+ catch { /* child gone */ }
128
+ child.on('error', reject);
129
+ child.on('close', (code) => code === 0
130
+ ? resolve()
131
+ : reject(new Error(`piper exit ${code}: ${err.slice(-200)}`)));
132
+ });
133
+ }
134
+ speak(text, opts = {}) {
135
+ this.stopCurrent(); // single queue: never two speakers at once
136
+ 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;
140
+ let stopped = false;
141
+ let resolve;
142
+ 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 = () => {
150
+ if (stopped)
151
+ return;
152
+ stopped = true;
153
+ if (this.current === handle)
154
+ this.current = null;
155
+ cleanup();
156
+ resolve();
157
+ };
158
+ const handle = {
159
+ done,
160
+ stop: () => {
161
+ if (stopped)
162
+ return;
163
+ stopped = true;
164
+ if (this.current === handle)
165
+ this.current = null;
166
+ try {
167
+ child?.kill('SIGTERM');
168
+ }
169
+ catch { /* already gone */ }
170
+ cleanup();
171
+ resolve();
172
+ },
173
+ };
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(() => {
181
+ if (stopped)
182
+ return;
183
+ 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);
189
+ }
190
+ catch {
191
+ finish();
192
+ }
193
+ }).catch(finish); // synth failed (bad binary/model) → degrade to text
194
+ return handle;
195
+ }
196
+ stopCurrent() {
197
+ const h = this.current;
198
+ if (h) {
199
+ this.current = null;
200
+ try {
201
+ h.stop();
202
+ }
203
+ catch { /* ignore */ }
204
+ }
205
+ }
206
+ }
@@ -0,0 +1,132 @@
1
+ // macOS `say` TTS engine (task-1282, hardened task-1393). `say` ships with macOS, needs no key and no
2
+ // network, so it is the free/portable default (SPEC: macOS say first). It streams to the audio device,
3
+ // so time-to-first-audio is close to process spawn. speak() is interruptible via stop() (SIGTERM to the
4
+ // child), which is how a key or the user starting to talk cuts a reply short.
5
+ //
6
+ // task-1393 hardening:
7
+ // - text is SANITISED (no `[[…]]` embedded commands, no leading-`-` flag, addresses shortened, capped)
8
+ // - text is delivered on STDIN (`say -f -`), never as the last argv word
9
+ // - a new speak() cleanly INTERRUPTS the current one, so two `say` processes never overlap (one queue)
10
+ // - the voice is always EXPLICIT (resolved to a deep British male upstream); it never falls back to the
11
+ // machine's system voice
12
+ import { spawn } from 'node:child_process';
13
+ import { execFile } from 'node:child_process';
14
+ import { parseSayVoices, sanitizeSpeech } from "./voices.js";
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;
18
+ export class SayTts {
19
+ name = 'say';
20
+ label = 'macOS say (built-in, no key)';
21
+ defaultVoice;
22
+ /** The utterance currently playing. A new speak() stops it first, so there is only ever one. */
23
+ current = null;
24
+ constructor(defaultVoice) { this.defaultVoice = defaultVoice; }
25
+ /** The resolved default voice this engine speaks with — used to seed the controller / status line. */
26
+ get voiceName() { return this.defaultVoice; }
27
+ async available() {
28
+ if (process.platform !== 'darwin')
29
+ return false;
30
+ return new Promise((resolve) => {
31
+ try {
32
+ const child = execFile(SAY_BIN, ['-v', '?'], { timeout: 4000 }, (err) => resolve(!err || err.code !== 'ENOENT'));
33
+ child.on('error', () => resolve(false));
34
+ }
35
+ catch {
36
+ resolve(false);
37
+ }
38
+ });
39
+ }
40
+ /** Installed voices with their locale. Robust against the single-space padding on long voice names. */
41
+ async listVoicesDetailed() {
42
+ return new Promise((resolve) => {
43
+ try {
44
+ execFile(SAY_BIN, ['-v', '?'], { timeout: 5000, maxBuffer: 1 << 20 }, (err, stdout) => {
45
+ resolve(err || !stdout ? [] : parseSayVoices(stdout));
46
+ });
47
+ }
48
+ catch {
49
+ resolve([]);
50
+ }
51
+ });
52
+ }
53
+ /** Selectable voice names (the TtsEngine contract). */
54
+ async listVoices() {
55
+ return (await this.listVoicesDetailed()).map((v) => v.name);
56
+ }
57
+ speak(text, opts = {}) {
58
+ // One queue: cut off whatever is playing so two `say` processes never overlap (clean interrupt).
59
+ this.stopCurrent();
60
+ const voice = opts.voice ?? this.defaultVoice;
61
+ const rate = opts.rate ?? DEFAULT_RATE;
62
+ // Sanitise the UNTRUSTED text first, then prepend our own TRUSTED pitch command. Order matters: the
63
+ // sanitiser strips any `[[…]]` the text tried to smuggle, so only our validated `[[pbas N]]` survives.
64
+ let safe = sanitizeSpeech(text);
65
+ if (typeof opts.pitch === 'number' && Number.isFinite(opts.pitch)) {
66
+ const pbas = Math.max(0, Math.min(100, Math.round(opts.pitch)));
67
+ safe = `[[pbas ${pbas}]] ${safe}`;
68
+ }
69
+ const args = [];
70
+ if (voice)
71
+ args.push('-v', voice);
72
+ args.push('-r', String(rate));
73
+ args.push('-f', '-'); // read the utterance from stdin — the text is NEVER an argv word
74
+ let child = null;
75
+ let stopped = false;
76
+ let resolve;
77
+ const done = new Promise((r) => { resolve = r; });
78
+ const finish = () => { if (!stopped) {
79
+ stopped = true;
80
+ if (this.current === handle)
81
+ this.current = null;
82
+ resolve();
83
+ } };
84
+ try {
85
+ child = spawn(SAY_BIN, args, { stdio: ['pipe', 'ignore', 'ignore'] });
86
+ // Nothing is spoken until stdin is closed; write the sanitised text and end it.
87
+ child.stdin?.on('error', () => { });
88
+ try {
89
+ child.stdin?.end(safe);
90
+ }
91
+ catch { /* child gone */ }
92
+ // `say` has no first-sample hook; the spawn event is the closest honest lower bound and is what the
93
+ // latency table reports as "first spoken word" for this engine.
94
+ child.on('spawn', () => { if (!stopped)
95
+ opts.onFirstAudio?.(); });
96
+ child.on('error', finish);
97
+ child.on('close', finish);
98
+ }
99
+ catch {
100
+ // No `say` on this machine: resolve immediately so the caller degrades to text.
101
+ queueMicrotask(finish);
102
+ }
103
+ const handle = {
104
+ done,
105
+ stop: () => {
106
+ if (stopped)
107
+ return;
108
+ stopped = true;
109
+ if (this.current === handle)
110
+ this.current = null;
111
+ try {
112
+ child?.kill('SIGTERM');
113
+ }
114
+ catch { /* already gone */ }
115
+ resolve();
116
+ },
117
+ };
118
+ this.current = handle;
119
+ return handle;
120
+ }
121
+ /** Stop the currently-playing utterance, if any. */
122
+ stopCurrent() {
123
+ const h = this.current;
124
+ if (h) {
125
+ this.current = null;
126
+ try {
127
+ h.stop();
128
+ }
129
+ catch { /* ignore */ }
130
+ }
131
+ }
132
+ }
@@ -0,0 +1,206 @@
1
+ // Voice resolution, the novelty ban-list, and speech-text sanitisation (task-1393). All pure and
2
+ // dependency-free so every rule is unit-testable without spawning `say`.
3
+ //
4
+ // Why this exists: SayTts used to be constructed with NO default voice, so macOS fell back to the
5
+ // machine's *system* voice — which differs per machine and can be a novelty/odd voice (a maintainer heard a
6
+ // "terrifying" one). This module makes the default an EXPLICIT deep British male and guarantees a
7
+ // novelty voice (Zarvox, Bad News, …) can never be auto-selected and the system default is never used
8
+ // silently: if nothing safe is installed, voice degrades to text (voice === null), never to "whatever
9
+ // the OS picks".
10
+ /**
11
+ * Novelty / "scary" voices that must never be auto-selected, normalised to lowercase base names (the
12
+ * locale qualifier like " (English (UK))" is stripped first — see normalizeVoiceName). The list is the
13
+ * classic MacinTalk novelty set plus the names the task named explicitly ("and similar"). The resolver
14
+ * also uses POSITIVE allow-lists, so this is defence in depth; the ban test asserts no resolved voice
15
+ * is ever on it.
16
+ */
17
+ export const BANNED_VOICE_NAMES = new Set([
18
+ 'albert', 'bad news', 'bahh', 'bells', 'boing', 'bubbles', 'cellos', 'deranged', 'fred',
19
+ 'good news', 'hysterical', 'jester', 'junior', 'organ', 'pipe organ', 'princess', 'ralph',
20
+ 'superstar', 'trinoids', 'whisper', 'wobble', 'zarvox',
21
+ ]);
22
+ /**
23
+ * Strip the locale qualifier macOS appends to the newer voices ("Eddy (English (UK))" → "Eddy"),
24
+ * collapse whitespace, lowercase. Used for ban/allow matching so "Eddy (English (US))" and
25
+ * "Eddy (English (UK))" share one base name.
26
+ */
27
+ export function normalizeVoiceName(name) {
28
+ return name.replace(/\s*\(.*\)\s*$/, '').trim().toLowerCase();
29
+ }
30
+ /** True when a voice is on the novelty ban-list (matched on its normalised base name). */
31
+ export function isBannedVoice(name) {
32
+ return BANNED_VOICE_NAMES.has(normalizeVoiceName(name));
33
+ }
34
+ /**
35
+ * Parse the output of `say -v '?'`. Each line is `<name> <locale> # <sample sentence>`, but the name
36
+ * column is only padded to a fixed width, so a LONG name ("Eddy (English (UK))") leaves a SINGLE space
37
+ * before the locale — the old regex required two and silently dropped exactly the en_GB voices we care
38
+ * about. This parser anchors on the trailing locale token instead, so it never loses a voice.
39
+ */
40
+ export function parseSayVoices(stdout) {
41
+ const voices = [];
42
+ for (const raw of stdout.split('\n')) {
43
+ if (!raw.trim())
44
+ continue;
45
+ const hash = raw.indexOf('#');
46
+ const left = (hash >= 0 ? raw.slice(0, hash) : raw).trimEnd();
47
+ // name = everything up to the last whitespace run; locale = the final token (en, en_GB, zh-CN…).
48
+ const m = left.match(/^(.*\S)\s+([a-z]{2}(?:[_-][A-Za-z0-9]+)?)$/);
49
+ if (m)
50
+ voices.push({ name: m[1].trim(), locale: m[2] });
51
+ }
52
+ return voices;
53
+ }
54
+ /**
55
+ * Deep British male voices, best-to-worst. Eddy (the maintainers' pick, 2026-10-15) is the dexterm default; Daniel is the canonical deep British male that
56
+ * ships with every macOS and is the next fallback. Oliver/Arthur are the newer en_GB male voices (present only if the
57
+ * user downloaded them). Matched on normalised base name.
58
+ */
59
+ export const BRITISH_MALE_PREFERENCE = ['eddy', 'daniel', 'oliver', 'arthur'];
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).
63
+ */
64
+ export const VOICE_STYLE_DEFAULTS = {
65
+ eddy: { rate: 168, pitch: 30 },
66
+ };
67
+ /** The delivery defaults for a resolved voice name, or undefined to use the engine-wide defaults. */
68
+ export function voiceStyleDefaults(name) {
69
+ return name ? VOICE_STYLE_DEFAULTS[normalizeVoiceName(name)] : undefined;
70
+ }
71
+ /**
72
+ * en_GB voices that read masculine / neutral and are acceptable British fallbacks when no first-class
73
+ * British male is installed (Grandpa is explicitly a male voice; Eddy/Reed/Rocko are the modern
74
+ * expressive voices under audition). Matched on normalised base name, en_GB locale only.
75
+ */
76
+ export const BRITISH_FALLBACK = ['grandpa', 'eddy', 'reed', 'rocko'];
77
+ /** Safe English male voices in any English locale, used only when nothing British is installed. */
78
+ export const ENGLISH_MALE_ALLOW = ['daniel', 'oliver', 'arthur', 'alex', 'tom', 'aaron', 'rishi'];
79
+ const isEnglish = (locale) => /^en([_-]|$)/i.test(locale);
80
+ const isBritish = (locale) => /^en[_-]gb$/i.test(locale);
81
+ /** Pick the best installed match for a base name, preferring an Enhanced/Premium variant and the given locale. */
82
+ function bestMatch(installed, base, preferLocale) {
83
+ const matches = installed.filter((v) => normalizeVoiceName(v.name) === base && !isBannedVoice(v.name));
84
+ if (!matches.length)
85
+ return null;
86
+ const score = (v) => {
87
+ let s = 0;
88
+ if (/\((enhanced|premium)\)/i.test(v.name))
89
+ s += 4; // higher-quality download, if present
90
+ if (preferLocale?.(v.locale))
91
+ s += 2;
92
+ return s;
93
+ };
94
+ return matches.slice().sort((a, b) => score(b) - score(a))[0];
95
+ }
96
+ /**
97
+ * Find the installed voice a saved name refers to. The `,` key saves the FULL name ("Eddy (English (UK))"),
98
+ * and Eddy/Reed/Rocko each exist in many languages, so an exact full-name match must win. Only when the
99
+ * saved name is a bare base name ("Eddy") do we choose among same-base voices: en_GB first, then any
100
+ * English, then anything else (a verifier found "Reed (English (UK))" resolving to the German Reed).
101
+ */
102
+ function findConfiguredVoice(installed, configured) {
103
+ const exact = installed.find((v) => v.name === configured && !isBannedVoice(v.name));
104
+ if (exact)
105
+ return exact;
106
+ const folded = configured.toLowerCase();
107
+ const ci = installed.find((v) => v.name.toLowerCase() === folded && !isBannedVoice(v.name));
108
+ if (ci)
109
+ return ci;
110
+ const base = normalizeVoiceName(configured);
111
+ const rank = (v) => (isBritish(v.locale) ? 0 : isEnglish(v.locale) ? 1 : 2);
112
+ return installed
113
+ .filter((v) => normalizeVoiceName(v.name) === base && !isBannedVoice(v.name))
114
+ .sort((a, b) => rank(a) - rank(b))[0];
115
+ }
116
+ /**
117
+ * Resolve the voice `say` should use. The fallback chain, in order:
118
+ * 1. the user's configured voice (if installed and not banned)
119
+ * 2. the chosen default: a British male (Eddy → Daniel → Oliver → Arthur), Enhanced/Premium preferred
120
+ * 3. the next British voice (en_GB Grandpa/Eddy/Reed/Rocko)
121
+ * 4. any safe English male voice (Alex/Tom/…)
122
+ * 5. null → replies stay text-only
123
+ * A banned (novelty) voice is NEVER returned, and the OS system default is NEVER used silently.
124
+ */
125
+ export function resolveVoice(opts) {
126
+ const installed = opts.installed ?? [];
127
+ const configured = opts.configured?.trim();
128
+ // 1. Configured voice wins when it is actually installed and safe.
129
+ if (configured) {
130
+ const hit = findConfiguredVoice(installed, configured);
131
+ if (hit && !isBannedVoice(hit.name)) {
132
+ return { voice: hit.name, source: 'config', reason: `your configured voice (${hit.name})` };
133
+ }
134
+ // Configured but missing or banned: fall through to the safe default rather than honour a bad value.
135
+ }
136
+ // 2. Chosen default: British male (Eddy → Daniel → …). en_GB only: Eddy also exists in other locales.
137
+ const britishInstalled = installed.filter((v) => isBritish(v.locale));
138
+ for (const base of BRITISH_MALE_PREFERENCE) {
139
+ const v = bestMatch(britishInstalled, base, isBritish);
140
+ if (v) {
141
+ const isDefault = base === BRITISH_MALE_PREFERENCE[0];
142
+ return {
143
+ voice: v.name,
144
+ source: 'default',
145
+ reason: isDefault ? `${v.name} — deep British male (dexterm default)` : `${v.name} — British male voice`,
146
+ };
147
+ }
148
+ }
149
+ // 3. Next British voice (en_GB, masculine/neutral).
150
+ for (const base of BRITISH_FALLBACK) {
151
+ const v = installed.find((x) => normalizeVoiceName(x.name) === base && isBritish(x.locale) && !isBannedVoice(x.name));
152
+ if (v)
153
+ return { voice: v.name, source: 'british', reason: `${v.name} — British voice (no Daniel installed)` };
154
+ }
155
+ // 4. Any safe English male voice, any English locale.
156
+ for (const base of ENGLISH_MALE_ALLOW) {
157
+ const v = installed.find((x) => normalizeVoiceName(x.name) === base && isEnglish(x.locale) && !isBannedVoice(x.name));
158
+ if (v)
159
+ return { voice: v.name, source: 'english', reason: `${v.name} — English voice (no British male installed)` };
160
+ }
161
+ // 5. Nothing safe: text only. Never the system default.
162
+ return { voice: null, source: 'none', reason: 'no safe voice installed — spoken replies are off (text still works)' };
163
+ }
164
+ // ------------------------------------------------------------------ speech-text sanitisation
165
+ /** Default cap on spoken length. Long analyst answers are summarised upstream; this is a hard backstop. */
166
+ export const MAX_SPEAK_CHARS = 600;
167
+ const BASE58 = '1-9A-HJ-NP-Za-km-z';
168
+ /** A Solana mint / base58 address (32-44 chars) or a 0x… EVM address — never read 44 characters aloud. */
169
+ const ADDRESS_RE = new RegExp(`\\b(?:0x[0-9a-fA-F]{40}|[${BASE58}]{32,44})\\b`, 'g');
170
+ /** Short, speakable form of an address: "7xKX…Dgkp" reads as a few characters, not forty-four. */
171
+ function shortAddress(addr) {
172
+ const body = addr.startsWith('0x') ? addr.slice(2) : addr;
173
+ return `${body.slice(0, 4)}…${body.slice(-4)}`;
174
+ }
175
+ /**
176
+ * Make arbitrary text safe to hand to `say`. Untrusted text (token names, socials, analyst output) can
177
+ * contain `say`'s own `[[…]]` embedded speech commands — `[[slnc 5000]]`, `[[rate 10]]`, even a voice
178
+ * switch — which `say` INTERPRETS whether the text arrives on argv or stdin. We also never want a
179
+ * leading `-` parsed as a flag. This:
180
+ * - defuses `[[` / `]]` so no embedded command is ever executed (kept readable, not deleted)
181
+ * - neutralises a leading `-`/`+` (defence in depth; we also deliver via stdin, not argv)
182
+ * - collapses control characters and whitespace
183
+ * - shortens contract addresses to a 4…4 form instead of reading 44 characters
184
+ * - caps the length on a word boundary
185
+ */
186
+ export function sanitizeSpeech(text, opts = {}) {
187
+ const maxChars = opts.maxChars ?? MAX_SPEAK_CHARS;
188
+ let s = String(text ?? '');
189
+ // Defuse `say` embedded commands: a split `[ [` / `] ]` is no longer a command token.
190
+ s = s.replace(/\[\[/g, '[ [').replace(/\]\]/g, '] ]');
191
+ // Strip control characters (NUL, ESC, etc.) that could confuse the child or the terminal.
192
+ s = s.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, ' ');
193
+ // Shorten addresses so we don't read 44 characters aloud.
194
+ s = s.replace(ADDRESS_RE, (m) => shortAddress(m));
195
+ // Collapse whitespace.
196
+ s = s.replace(/\s+/g, ' ').trim();
197
+ // A leading dash/plus could be read as a flag if it ever reached argv; neutralise it.
198
+ s = s.replace(/^[-+]+\s*/, '');
199
+ // Cap length on a word boundary.
200
+ if (s.length > maxChars) {
201
+ const cut = s.slice(0, maxChars);
202
+ const sp = cut.lastIndexOf(' ');
203
+ s = (sp > maxChars * 0.6 ? cut.slice(0, sp) : cut).trim();
204
+ }
205
+ return s;
206
+ }
@@ -0,0 +1,3 @@
1
+ // Shared types for @dexterm/voice (task-1282). Everything here is UI- and engine-agnostic so the
2
+ // controller can be driven headlessly with fakes in tests and with the real macOS engines in the app.
3
+ export {};
package/package.json CHANGED
@@ -1,6 +1,40 @@
1
1
  {
2
2
  "name": "dexterm-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.2",
4
+ "description": "dexterm: terminal DEX screener with a local Claude CLI analyst",
5
+ "author": "The dexterm authors",
6
+ "license": "AGPL-3.0-only",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/dexterm-xyz/dexterm.git"
10
+ },
11
+ "type": "module",
12
+ "bin": {
13
+ "dexterm": "./dist/packages/cli/bin/dexterm.mjs"
14
+ },
15
+ "exports": {
16
+ ".": "./dist/packages/cli/src/index.js"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "LICENSE"
21
+ ],
22
+ "scripts": {
23
+ "build": "node scripts/build-dist.mjs",
24
+ "prepack": "node scripts/build-dist.mjs"
25
+ },
26
+ "engines": {
27
+ "node": ">=22.18"
28
+ },
29
+ "devDependencies": {
30
+ "@dexterm/agents": "0.0.1",
31
+ "@dexterm/custom": "0.0.1",
32
+ "@dexterm/data": "0.0.1",
33
+ "@dexterm/mcp": "0.0.1",
34
+ "@dexterm/packs": "0.0.1",
35
+ "@dexterm/popup": "0.0.1",
36
+ "@dexterm/recorder": "0.0.1",
37
+ "@dexterm/tui": "0.0.1",
38
+ "@dexterm/voice": "0.0.1"
39
+ }
40
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.