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,502 @@
1
+ // Deterministic, LLM-free voice router (task-1282). Ported and trimmed from the spike
2
+ // (dexterm/research/jarvis/src/router.mjs) to the in-app safety contract:
3
+ //
4
+ // Voice may ONLY ask the analyst a question or trigger the analyst's existing SAFE actions —
5
+ // navigate a screen, open a token, set a filter, add to the watchlist, or OPEN a pre-filled PAPER
6
+ // ticket. Voice may NEVER confirm, sign, or execute a trade. A spoken "buy"/"sell" resolves at most
7
+ // to `open-ticket`, which opens the ticket in its editing phase; the keypress gate still stands.
8
+ //
9
+ // Every return path is one of the closed VoiceDecision kinds below; none can execute a trade. The
10
+ // exported `isSafe()` invariant and its test assert that property holds for every decision.
11
+ //
12
+ // Pure, zero-dependency. Runs in well under a millisecond (no model in the loop).
13
+ /** The decision kinds voice is allowed to produce. A trade can only ever reach `open-ticket`. */
14
+ const SAFE_KINDS = new Set(['ignore', 'clarify', 'navigate', 'open-token', 'filter', 'watchlist', 'open-ticket', 'propose-plan', 'popup', 'workspace', 'analyst']);
15
+ /**
16
+ * The global safety invariant: a voice decision never executes a trade. `open-ticket` only opens the
17
+ * editing ticket (the keypress gate still applies); no decision carries a confirm/arm/execute flag.
18
+ * Any caller can assert this before acting; the router test asserts it for every utterance.
19
+ */
20
+ export function isSafe(d) {
21
+ if (!SAFE_KINDS.has(d.kind))
22
+ return false;
23
+ // Defence in depth: reject anything that smuggled an execution-shaped field onto the decision.
24
+ const o = d;
25
+ if ('execute' in o || 'confirm' in o || 'armed' in o || 'arm' in o || 'sign' in o || 'send' in o || 'mode' in o || 'caps' in o || 'live' in o)
26
+ return false;
27
+ return true;
28
+ }
29
+ const SCREENS = {
30
+ screener: ['screener', 'the screen', 'main screen', 'the board', 'the list', 'top runners', 'runners', 'movers', 'gainers', 'trending'],
31
+ newpairs: ['new pairs', 'new pools', 'fresh pairs', 'just launched', 'new launches', 'newpairs'],
32
+ watchlist: ['watchlist', 'my watchlist', 'my list'],
33
+ analyst: ['analyst', 'the analyst', 'chat'],
34
+ positions: ['positions', 'my positions', 'my bags', 'portfolio'],
35
+ };
36
+ // Nav verbs that mean "put this on screen" rather than a trade or a question.
37
+ const NAV_LEAD = /^(show|open|go to|goto|switch to|take me to|pull up|bring up|show me)\b/;
38
+ const BUY_WORDS = ['buy', 'ape', 'long', 'grab', 'snipe', 'cop'];
39
+ const SELL_WORDS = ['sell', 'dump', 'close', 'exit', 'take profit', 'offload'];
40
+ // Homophones STT commonly confuses with the trade verbs. A homophone with no clear buy/sell word
41
+ // present is UNCERTAIN: the router clarifies rather than opening the wrong-side ticket.
42
+ const ACTION_HOMOPHONES = { cell: 'sell', sail: 'sell', by: 'buy', bye: 'buy' };
43
+ const ANALYSIS_HINTS = ['is it safe', 'rug', 'analyze', 'analyse', 'should i', 'whats the deal',
44
+ "what's the deal", 'thoughts on', 'look into', 'holders', 'whos holding', "who's holding",
45
+ 'tell me about', 'summary of', 'how is', 'why is', 'explain', 'is this', 'safe to', 'what do you think'];
46
+ const clean = (s) => s.toLowerCase().normalize('NFKC').replace(/[.,!?]/g, '').replace(/\s+/g, ' ').trim();
47
+ const has = (text, phrases) => phrases.find((p) => text.includes(p));
48
+ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
49
+ // Whole-word match. Trade verbs MUST match on word boundaries so "copy the mint" is not a COPY buy
50
+ // ("cop"), and "escape the shape" is not an ESCAPE buy ("ape") — task-1282 rework D4.
51
+ const wordHas = (text, phrases) => phrases.find((p) => new RegExp(`(?:^|\\s)${escapeRe(p)}(?:\\s|$)`).test(text));
52
+ // ---------- tiny amount parser (for filters) ----------
53
+ const SCALE = { k: 1e3, thousand: 1e3, grand: 1e3, m: 1e6, mil: 1e6, million: 1e6, b: 1e9, bil: 1e9, billion: 1e9 };
54
+ function parseAmount(fragment) {
55
+ const f = fragment.toLowerCase().replace(/[,$]/g, ' ').replace(/\s+/g, ' ').trim();
56
+ const m = f.match(/(\d+(?:\.\d+)?)\s*(k|m|b|thousand|grand|mil|million|bil|billion)?/);
57
+ if (!m)
58
+ return null;
59
+ const n = parseFloat(m[1]);
60
+ const scaleWord = m[2];
61
+ if (scaleWord)
62
+ return { value: n * (SCALE[scaleWord] ?? 1), scaleExplicit: true };
63
+ return { value: n, scaleExplicit: false };
64
+ }
65
+ function fmtShort(n) {
66
+ if (n >= 1e9)
67
+ return (n / 1e9) + 'b';
68
+ if (n >= 1e6)
69
+ return (n / 1e6) + 'm';
70
+ if (n >= 1e3)
71
+ return (n / 1e3) + 'k';
72
+ return String(n);
73
+ }
74
+ function compileFilter(expr) {
75
+ const e = expr.toLowerCase();
76
+ const amt = parseAmount(e);
77
+ if (/liquid/.test(e) && amt)
78
+ return `liq${/over|above|>|more than/.test(e) ? '>' : '<'}${fmtShort(amt.value)}`;
79
+ if (/(age|old|minutes|new)/.test(e) && amt)
80
+ return `age${/under|below|less than|</.test(e) ? '<' : '>'}${amt.value}m`;
81
+ if (/(mcap|market cap)/.test(e) && amt)
82
+ return `mc${/over|above|>/.test(e) ? '>' : '<'}${fmtShort(amt.value)}`;
83
+ if (/(vol|volume)/.test(e) && amt)
84
+ return `vol24${/over|above|>|more than/.test(e) ? '>' : '<'}${fmtShort(amt.value)}`;
85
+ return null;
86
+ }
87
+ /** A workspace switch phrase → the workspace name, or null. Names are lowercase letters/digits/dashes only. */
88
+ function workspaceOf(text) {
89
+ if (/\b(show me everything|show everything|everything on|morning (hud|mode|view|workspace)|the trenches are hot|jack me in)\b/.test(text))
90
+ return 'morning-hud';
91
+ if (/\b(default|normal|plain) (workspace|view|layout)\b/.test(text) || /\b(close|reset) (the )?workspace\b/.test(text))
92
+ return 'default';
93
+ const m = /\b(?:workspace|layout) (?:called |named )?([a-z][a-z0-9-]{0,39})\b/.exec(text) ?? /\b([a-z][a-z0-9-]{0,39}) (?:workspace|layout)\b/.exec(text);
94
+ if (m && !['the', 'a', 'my', 'this', 'that', 'default', 'morning'].includes(m[1]))
95
+ return m[1];
96
+ return null;
97
+ }
98
+ const STOP = new Set([...BUY_WORDS, ...SELL_WORDS, 'the', 'a', 'of', 'at', 'if', 'it', 'to', 'and', 'then', 'me',
99
+ 'some', 'half', 'all', 'this', 'that', 'one', 'coin', 'token', 'show', 'open', 'go', 'into', 'in', 'my', 'now',
100
+ 'please', 'runners', 'pairs', 'screen', 'ignore', 'previous', 'instructions', 'add', 'watch', 'watchlist',
101
+ // task-1339: words that ride along with a trade verb but are never a ticker. "buy it and confirm" means
102
+ // the selected coin, not a token called CONFIRM; "show me the paper trades" is not a token called PAPER.
103
+ 'confirm', 'confirmed', 'send', 'execute', 'sign', 'yes', 'ok', 'okay', 'paper', 'live', 'trade', 'trades',
104
+ 'ticket', 'order', 'chart', 'popup', 'window', 'position', 'positions', 'bag', 'bags', 'everything', 'max', 'rest']);
105
+ const PRONOUN = /\b(this one|this coin|that one|the selected|selected row|it)\b/;
106
+ // ---------- spoken number-words (task-1339 F1) ----------
107
+ // STT returns sizes as words far more often than digits ("ten sol", "a hundred bucks", "fifty
108
+ // percent"), so a digit-only size detector silently drops them. These let `detectSizeSpec` catch the
109
+ // word forms too and clarify, exactly like the digit forms. Pure, allocation-light, no model.
110
+ const NUM_ONES = {
111
+ zero: 0, one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8, nine: 9, ten: 10,
112
+ eleven: 11, twelve: 12, thirteen: 13, fourteen: 14, fifteen: 15, sixteen: 16, seventeen: 17,
113
+ eighteen: 18, nineteen: 19,
114
+ };
115
+ const NUM_TENS = { twenty: 20, thirty: 30, forty: 40, fifty: 50, sixty: 60, seventy: 70, eighty: 80, ninety: 90 };
116
+ // Every word that can appear inside a spoken quantity. Used to build the match regex below.
117
+ const NUM_WORD = '(?:zero|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|hundred|thousand)';
118
+ // A spoken quantity phrase: "ten", "twenty five", "a hundred", "a couple", "two point five".
119
+ const QTY = `(?:a[\\s-]+(?:hundred|thousand|couple|few)|${NUM_WORD}(?:[\\s-]+${NUM_WORD})*(?:[\\s-]+point[\\s-]+${NUM_WORD}(?:[\\s-]+${NUM_WORD})*)?)`;
120
+ /** Convert a spoken quantity phrase to a number, or null if it is not a clean quantity. */
121
+ function wordsToNumber(phrase) {
122
+ const toks = phrase.toLowerCase().split(/[\s-]+/).filter(Boolean);
123
+ let total = 0, cur = 0, seen = false, i = 0;
124
+ for (; i < toks.length; i++) {
125
+ const t = toks[i];
126
+ if (t === 'point')
127
+ break;
128
+ if (t === 'a' || t === 'an')
129
+ continue; // article in "a hundred" — carries no value of its own
130
+ if (t === 'couple') {
131
+ cur += 2;
132
+ seen = true;
133
+ continue;
134
+ }
135
+ if (t === 'few') {
136
+ cur += 3;
137
+ seen = true;
138
+ continue;
139
+ }
140
+ if (t in NUM_ONES) {
141
+ cur += NUM_ONES[t];
142
+ seen = true;
143
+ continue;
144
+ }
145
+ if (t in NUM_TENS) {
146
+ cur += NUM_TENS[t];
147
+ seen = true;
148
+ continue;
149
+ }
150
+ if (t === 'hundred') {
151
+ cur = (cur || 1) * 100;
152
+ seen = true;
153
+ continue;
154
+ }
155
+ if (t === 'thousand') {
156
+ total += (cur || 1) * 1000;
157
+ cur = 0;
158
+ seen = true;
159
+ continue;
160
+ }
161
+ return null; // an unexpected token — not a clean number
162
+ }
163
+ let value = total + cur;
164
+ if (i < toks.length && toks[i] === 'point') {
165
+ let frac = '';
166
+ for (i++; i < toks.length; i++) {
167
+ const d = NUM_ONES[toks[i]];
168
+ if (d === undefined || d > 9)
169
+ break; // only single spoken digits after "point"
170
+ frac += String(d);
171
+ }
172
+ if (frac)
173
+ value = parseFloat(`${value}.${frac}`);
174
+ }
175
+ return seen ? value : null;
176
+ }
177
+ /** A spoken quantity formatted for the clarify line: the parsed number, else the raw words. */
178
+ function fmtQty(phrase) {
179
+ const v = wordsToNumber(phrase);
180
+ return v !== null ? String(v) : phrase.trim().replace(/\s+/g, ' ');
181
+ }
182
+ /**
183
+ * A spoken trade SIZE / amount / slippage. Voice must NOT set these on a ticket silently — size and
184
+ * slippage are money parameters the user confirms on the ticket itself. When one is heard the router
185
+ * CLARIFIES instead of dropping it (task-1339 re-verify: "buy 10 sol of bonk", "buy bonk with max size"
186
+ * and "buy bonk 50 percent slippage" used to open a plain ticket with the size discarded and unmentioned,
187
+ * which breaks the "it clarifies; it never guesses" contract). task-1339 F1: STT usually gives these as
188
+ * WORDS, not digits ("ten sol", "half", "a hundred bucks", "fifty percent"); those must clarify too,
189
+ * never fall through and drop the size. Returns a short human phrase, or null.
190
+ */
191
+ function detectSizeSpec(text) {
192
+ let m;
193
+ // --- digit forms ---
194
+ if ((m = text.match(/\b(\d+(?:\.\d+)?)\s*sol\b/)))
195
+ return `${m[1]} SOL`;
196
+ if ((m = text.match(/\$\s*(\d+(?:\.\d+)?)/)) || (m = text.match(/\b(\d+(?:\.\d+)?)\s*(?:dollars?|bucks|usd|usdc)\b/)))
197
+ return `$${m[1]}`;
198
+ if ((m = text.match(/\b(\d+(?:\.\d+)?)\s*(?:%|percent|pct)\b/)))
199
+ return /slippage/.test(text) ? `${m[1]}% slippage` : `${m[1]}%`;
200
+ // --- spoken word-number forms (F1): a quantity in words, with or without a unit ---
201
+ if ((m = text.match(new RegExp(`\\b(${QTY})[\\s-]+sols?\\b`))))
202
+ return `${fmtQty(m[1])} SOL`;
203
+ if ((m = text.match(new RegExp(`\\b(${QTY})[\\s-]+(?:dollars?|bucks|usd|usdc)\\b`))))
204
+ return `$${fmtQty(m[1])}`;
205
+ if ((m = text.match(new RegExp(`\\b(${QTY})[\\s-]+(?:percent|pct)\\b`))))
206
+ return /slippage/.test(text) ? `${fmtQty(m[1])}% slippage` : `${fmtQty(m[1])}%`;
207
+ // a bare spoken quantity of a token ("buy twenty of bonk", "a hundred of wif") — size without a unit
208
+ if ((m = text.match(new RegExp(`\\b(${QTY})[\\s-]+of\\b`))))
209
+ return `a size of ${fmtQty(m[1])}`;
210
+ // --- fraction of a position ("half", "a quarter", "two thirds") — a size voice must not set silently ---
211
+ if ((m = text.match(/\b(?:(?:a|one|two|three)[\s-]+)?(half|quarter|quarters|third|thirds)\b/)))
212
+ return m[0].trim().replace(/\s+/g, ' ');
213
+ // --- qualifier forms ---
214
+ if (/\bslippage\b/.test(text))
215
+ return 'a slippage setting';
216
+ if (/\b(max|full|whole)\s+(size|position|bag|stack)\b/.test(text))
217
+ return 'max size';
218
+ if (/\ball[- ]?in\b/.test(text))
219
+ return 'an all-in size';
220
+ return null;
221
+ }
222
+ /**
223
+ * Best-effort ticker from an utterance; the APP resolves it to a mint (the router never guesses one).
224
+ *
225
+ * An explicitly NAMED ticker always beats a pronoun: "buy BONK, confirm it" with WIF selected must
226
+ * resolve to BONK, never the selected row (task-1282 rework D1). A pronoun ("it" / "this one") only
227
+ * wins when no ticker word was spoken. The app still resolves the symbol against the live
228
+ * screener / watchlist / open token and clarifies if it is unknown or ambiguous — the router never
229
+ * silently substitutes the selected coin for a named one.
230
+ *
231
+ * task-1339 re-verify defects 1 & 2:
232
+ * - Tickers that are ALSO command words (LIVE, SIGN, MAX, SEND, ...) are matched against the visible
233
+ * symbols BEFORE the stop-word filter. Previously "buy live" (with LIVE on screen) had `live`
234
+ * stripped as a stop word, so no ticker matched and it silently fell back to the highlighted row.
235
+ * A lone visible ticker that is also a command word is itself ambiguous → clarify.
236
+ * - More than one on-screen ticker in one breath ("buy wif and bonk") is ambiguous → clarify, instead
237
+ * of taking the first and silently dropping the rest.
238
+ */
239
+ function resolveTarget(text, visible) {
240
+ const vis = new Set(visible.map((s) => s.toLowerCase()));
241
+ const m = text.match(/\$?[a-z][a-z0-9]{1,9}\b/g);
242
+ if (!m)
243
+ return PRONOUN.test(text) ? { kind: 'selected' } : { kind: 'none' };
244
+ // An explicit $TICKER is unambiguous; two different ones must be disambiguated.
245
+ const dollars = [...new Set(m.filter((w) => w.startsWith('$')).map((w) => w.slice(1).toUpperCase()))].filter(Boolean);
246
+ if (dollars.length > 1)
247
+ return { kind: 'ambiguous', reason: 'multi', symbols: dollars };
248
+ if (dollars.length === 1)
249
+ return { kind: 'symbol', symbol: dollars[0] };
250
+ const raw = m.map((w) => w.replace('$', ''));
251
+ // Words the user actually spoke that are TICKERS ON SCREEN — checked BEFORE the stop-word filter.
252
+ const visWords = [...new Set(raw.filter((w) => vis.has(w)))];
253
+ if (visWords.length > 1)
254
+ return { kind: 'ambiguous', reason: 'multi', symbols: visWords.map((w) => w.toUpperCase()) };
255
+ if (visWords.length === 1) {
256
+ const w = visWords[0];
257
+ if (STOP.has(w))
258
+ return { kind: 'ambiguous', reason: 'collision', symbols: [w.toUpperCase()] };
259
+ return { kind: 'symbol', symbol: w.toUpperCase() };
260
+ }
261
+ // No on-screen ticker was spoken. Fall back to the first non-stop word (the app resolves/clarifies an
262
+ // unknown symbol), or the selected row for a pure pronoun.
263
+ const words = raw.filter((w) => !STOP.has(w) && !/^\d+$/.test(w));
264
+ if (words.length === 0 && PRONOUN.test(text))
265
+ return { kind: 'selected' };
266
+ if (words[0])
267
+ return { kind: 'symbol', symbol: words[0].toUpperCase() };
268
+ return PRONOUN.test(text) ? { kind: 'selected' } : { kind: 'none' };
269
+ }
270
+ /** The spoken clarifying question for an ambiguous/colliding ticker, phrased for the action verb. */
271
+ function clarifyTokenQ(t, verb) {
272
+ const names = t.symbols.join(' and ');
273
+ return t.reason === 'multi'
274
+ ? `I heard more than one token — ${names}. Which one should I ${verb}?`
275
+ : `Did you mean the token ${names}? Say "${verb} ${t.symbols[0]}" if so, or name another token.`;
276
+ }
277
+ function detectAction(text) {
278
+ const words = text.split(' ');
279
+ const buyHit = wordHas(text, BUY_WORDS);
280
+ const sellHit = wordHas(text, SELL_WORDS);
281
+ let homophone = null;
282
+ for (const w of words)
283
+ if (w in ACTION_HOMOPHONES)
284
+ homophone = { surface: w, maps: ACTION_HOMOPHONES[w] };
285
+ const side = buyHit && sellHit ? 'both' : buyHit ? 'buy' : sellHit ? 'sell' : (homophone ? homophone.maps : null);
286
+ if (!side)
287
+ return { isTrade: false };
288
+ if (side === 'both')
289
+ return { isTrade: true, uncertain: true };
290
+ const uncertain = !!homophone && !buyHit && !sellHit;
291
+ return { isTrade: true, side, uncertain, surface: homophone?.surface, verb: buyHit ?? sellHit };
292
+ }
293
+ // ── Order-plan grammar (task-1355): recognise a conditional entry + take-profit ladder + moon bag +
294
+ // stop. Every market-cap number must carry an EXPLICIT scale (k/m/b); a bare "300" clarifies rather than
295
+ // guessing 300k vs 3M. Spoken-word amounts that carry no digit also clarify. Voice never fills a number
296
+ // silently — the §6.13 300k-vs-3M trap is caught here, not by luck. ──
297
+ const PLAN_CUE = /\b(dips? to|drops? to|falls? to|if it (?:dips|drops|falls|hits|gets|reaches)|when it (?:hits|reaches|gets)|take[\s-]?profit|moon[\s-]?bag|leave (?:a|the) bag|keep (?:a|the) bag|sell (?:half|a quarter|a third|\d+\s*(?:%|percent))|stop (?:loss )?(?:at|below|under))\b/;
298
+ function parseFraction(frag) {
299
+ if (/\bhalf\b/.test(frag))
300
+ return 0.5;
301
+ if (/\bquarter\b/.test(frag))
302
+ return 0.25;
303
+ if (/\bthird\b/.test(frag))
304
+ return 1 / 3;
305
+ const pct = frag.match(/(\d+(?:\.\d+)?)\s*(?:%|percent)\b/);
306
+ if (pct)
307
+ return Number(pct[1]) / 100;
308
+ return null;
309
+ }
310
+ function parsePlanSpec(utterance) {
311
+ // Normalise but KEEP the decimal point: the router's global clean() strips '.', which would turn
312
+ // "1.2m" into "12m" — a 10x error. Commas become spaces so clause boundaries stay word-boundaries.
313
+ const text = utterance.toLowerCase().normalize('NFKC').replace(/[,!?]/g, ' ').replace(/\s+/g, ' ').trim();
314
+ const exits = [];
315
+ let entry = null;
316
+ let stopMcUsd = null;
317
+ const moonBag = /\bmoon[\s-]?bag\b|\bleave (?:a|the) bag\b|\bkeep (?:a|the) bag\b/.test(text);
318
+ const scaleQ = (what, raw) => ({ ok: false, reason: 'ambiguous market-cap scale', question: `Did you mean ${raw}k or ${raw} million for the ${what}? Say the scale, e.g. "${raw}k".` });
319
+ const em = text.match(/(?:dips?|drops?|falls?)\s+(?:down\s+)?to\s+([a-z0-9.\s]+?)(?=[,;]|$|\bsell\b|\btake\b|\bstop\b|\bkeep\b|\bleave\b|\band\b)/)
320
+ || text.match(/\bif it (?:dips|drops|falls|hits|gets to|reaches)\s+(?:to\s+)?([a-z0-9.\s]+?)(?=[,;]|$|\bsell\b|\btake\b|\bstop\b|\bkeep\b|\bleave\b|\band\b)/);
321
+ if (em) {
322
+ const a = parseAmount(em[1]);
323
+ if (!a)
324
+ return { ok: false, reason: 'entry unparsable', question: 'What market cap should it dip to? e.g. "300k".' };
325
+ if (!a.scaleExplicit)
326
+ return scaleQ('entry', a.value);
327
+ entry = { mcUsd: a.value };
328
+ }
329
+ const tpRe = /\b(?:sell|take[\s-]?profit(?:\s+of)?)\s+(half|a quarter|a third|\d+\s*(?:%|percent))\s+(?:at|when it (?:hits|reaches))\s+([a-z0-9.\s]+?)(?=[,;]|$|\bsell\b|\btake\b|\bstop\b|\bkeep\b|\bleave\b|\band\b)/g;
330
+ let tm;
331
+ while ((tm = tpRe.exec(text)) !== null) {
332
+ const frac = parseFraction(tm[1]);
333
+ if (frac == null)
334
+ return { ok: false, reason: 'take-profit fraction unclear', question: 'What fraction should I sell at each level? e.g. "sell half".' };
335
+ const a = parseAmount(tm[2]);
336
+ if (!a)
337
+ return { ok: false, reason: 'take-profit level unparsable', question: 'At what market cap should I take profit? e.g. "600k".' };
338
+ if (!a.scaleExplicit)
339
+ return scaleQ('take-profit', a.value);
340
+ exits.push({ mcUsd: a.value, fraction: frac });
341
+ }
342
+ const sm = text.match(/\bstop\s+(?:loss\s+)?(?:at|below|under)\s+([a-z0-9.\s]+?)(?=[,;]|$|\band\b)/);
343
+ if (sm) {
344
+ const a = parseAmount(sm[1]);
345
+ if (!a)
346
+ return { ok: false, reason: 'stop unparsable', question: 'At what market cap should the stop trigger? e.g. "150k".' };
347
+ if (!a.scaleExplicit)
348
+ return scaleQ('stop', a.value);
349
+ stopMcUsd = a.value;
350
+ }
351
+ return { ok: true, entry, exits, stopMcUsd, moonBag };
352
+ }
353
+ /**
354
+ * Route one transcribed utterance. `prov` carries the push-to-talk / diarization / echo facts that
355
+ * gate the decision before any language understanding runs.
356
+ */
357
+ export function route(utterance, prov, screen = {}) {
358
+ // --- Gate 0: audio provenance. Guards against stray room audio ever reaching an action. ---
359
+ if (!prov.pttActive)
360
+ return { kind: 'ignore', reason: 'push-to-talk not active; audio ignored' };
361
+ if (prov.speakerCount > 1)
362
+ return { kind: 'ignore', reason: `multiple speakers (${prov.speakerCount}); ignoring` };
363
+ if (prov.echoFlagged)
364
+ return { kind: 'ignore', reason: 'echo/TV audio detected; ignoring' };
365
+ const text = clean(utterance);
366
+ if (!text)
367
+ return { kind: 'ignore', reason: 'empty utterance' };
368
+ if (prov.sttConfidence < 0.45)
369
+ return { kind: 'clarify', reason: 'low STT confidence', question: "I didn't catch that — say it again?" };
370
+ const visible = screen.visibleSymbols ?? [];
371
+ // --- Popup panels (task-1339): "show the chart", "pop the chart", "close the chart". Decided before
372
+ // the trade verbs so "close the chart" can never become a SELL of a token called CHART. ---
373
+ const chart = /\b(the |my |a )?(chart|chart popup|popup chart|chart window)\b/.test(text);
374
+ if (chart) {
375
+ if (/\b(close|hide|kill|dismiss|shut)\b/.test(text))
376
+ return { kind: 'popup', panel: 'chart', open: false, say: 'closing the chart' };
377
+ if (/\b(show|open|pop|pull up|bring up|popup|pop up|give me)\b/.test(text))
378
+ return { kind: 'popup', panel: 'chart', open: true, say: 'opening the chart' };
379
+ }
380
+ // --- Workspaces (task-1358): decided before the trade verbs and the screen names so "show me everything"
381
+ // is the morning HUD, not a navigation to a token called EVERYTHING. Never combined with a trade. ---
382
+ const ws = workspaceOf(text);
383
+ if (ws && !detectAction(text).isTrade)
384
+ return { kind: 'workspace', name: ws, say: ws === 'morning-hud' ? 'morning HUD' : ws === 'default' ? 'default workspace' : `${ws} workspace` };
385
+ const trade = detectAction(text);
386
+ const navLead = NAV_LEAD.test(text);
387
+ const navScreen = (() => { for (const [scr, phrases] of Object.entries(SCREENS))
388
+ if (has(text, phrases))
389
+ return scr; return null; })();
390
+ // --- Compound / injected-audio pattern: a navigation command AND a trade verb in one breath
391
+ // ("show runners and sell all my bonk"). Never split it silently; ask which was meant. ---
392
+ if ((navLead || navScreen) && trade.isTrade) {
393
+ return { kind: 'clarify', reason: 'compound command (navigation + trade in one utterance)',
394
+ question: 'I heard a screen command and a trade in one breath — say just one.' };
395
+ }
396
+ // --- Navigation (instant, no model): "show me the runners", "go to watchlist". ---
397
+ if ((navLead || navScreen) && !trade.isTrade) {
398
+ if (navScreen)
399
+ return { kind: 'navigate', screen: navScreen, say: navScreen };
400
+ // "open <ticker>" / "pull up <ticker>" with no screen name -> open a token.
401
+ const tgt = resolveTarget(text.replace(NAV_LEAD, '').trim(), visible);
402
+ if (tgt.kind === 'ambiguous')
403
+ return { kind: 'clarify', reason: `ambiguous token (${tgt.reason})`, question: clarifyTokenQ(tgt, 'open') };
404
+ if (tgt.kind === 'symbol')
405
+ return { kind: 'open-token', target: tgt.symbol, say: tgt.symbol };
406
+ if (tgt.kind === 'selected')
407
+ return { kind: 'open-token', target: '__selected__', say: 'this one' };
408
+ }
409
+ // --- Filter: "filter liquidity over 50k", "only show age under 10 minutes". ---
410
+ const filterMatch = text.match(/\b(filter|set filter|only show|show only)\b\s*(.*)$/);
411
+ if (filterMatch && filterMatch[2] && !trade.isTrade) {
412
+ const expr = compileFilter(filterMatch[2]);
413
+ if (expr)
414
+ return { kind: 'filter', expr, say: `filter ${expr}` };
415
+ return { kind: 'clarify', reason: 'unparsable filter', question: 'Filter on what? e.g. "liquidity over 50k" or "age under 10 minutes".' };
416
+ }
417
+ // --- Watchlist: "watch this one", "add bonk to my watchlist". ---
418
+ if (/\b(watch|add to (my )?watchlist|watchlist)\b/.test(text) && !trade.isTrade) {
419
+ const tgt = resolveTarget(text, visible);
420
+ if (tgt.kind === 'ambiguous')
421
+ return { kind: 'clarify', reason: `ambiguous token (${tgt.reason})`, question: clarifyTokenQ(tgt, 'watch') };
422
+ const sym = tgt.kind === 'symbol' ? tgt.symbol : '__selected__';
423
+ return { kind: 'watchlist', target: sym, say: `watch ${sym === '__selected__' ? 'this one' : sym}` };
424
+ }
425
+ // --- Order-plan PROPOSAL (task-1355): a trade verb with a conditional/ladder cue ("buy this if it
426
+ // dips to 300k, sell half at 600k, moon bag"). Voice PROPOSES only; arming needs a physical keypress.
427
+ // Decided before the plain ticket so "buy if it dips to 300k" is a plan, not an instant buy ticket.
428
+ // Any mishear — unscaled number, ambiguous token, unclear fraction — clarifies, never guesses. ---
429
+ if (trade.isTrade && PLAN_CUE.test(text)) {
430
+ // A plan legitimately names both sides ("BUY the entry, SELL the ladder"), so the side comes from the
431
+ // LEADING verb, not detectAction's both-sides uncertainty. An unclear lead clarifies.
432
+ const buyLead = new RegExp(`^(?:${BUY_WORDS.join('|')})\\b`).test(text);
433
+ const sellLead = new RegExp(`^(?:${SELL_WORDS.join('|')})\\b`).test(text);
434
+ const planSide = buyLead ? 'buy' : (sellLead || (wordHas(text, SELL_WORDS) && !wordHas(text, BUY_WORDS))) ? 'sell' : null;
435
+ if (planSide) {
436
+ // A plan binds to a MINT: an explicitly-named visible symbol, else the open/selected token. We do
437
+ // NOT run the general ticker resolver here — a filler word ("dips") must never become a ticker.
438
+ const named = visible.find((s) => new RegExp(`\\b${s.toLowerCase().replace(/[^a-z0-9]/g, '')}\\b`).test(text));
439
+ // task-1355 verify fix: a ticker the user NAMED right after the verb ("buy zzqq if it dips…") that is
440
+ // not on screen must clarify — never silently become the highlighted row (the ticket path's "a named
441
+ // ticker beats a pronoun; no fuzzy match" rule applies to plans too). Only the verb's object word is
442
+ // considered, so a filler word later in the sentence ("dips") can still never become a ticker.
443
+ if (!named) {
444
+ const obj = text.match(new RegExp(`^(?:${[...BUY_WORDS, ...SELL_WORDS].join('|')})\\s+(?:into\\s+|some\\s+|more\\s+)?\\$?([a-z][a-z0-9]{1,9})\\b`))?.[1];
445
+ if (obj && !STOP.has(obj) && !PRONOUN.test(obj) && !/^\d+$/.test(obj)) {
446
+ return { kind: 'clarify', reason: 'order plan names a token not on screen', question: `I don't see a token called ${obj.toUpperCase()} — open it first, then say the plan.` };
447
+ }
448
+ }
449
+ const sym = named ?? (screen.selected ? '__selected__' : null);
450
+ if (!sym)
451
+ return { kind: 'clarify', reason: 'order plan with no target token', question: 'Set a plan on which token? Open it or say the ticker.' };
452
+ const spec = parsePlanSpec(utterance);
453
+ if (!spec.ok)
454
+ return { kind: 'clarify', reason: spec.reason, question: spec.question };
455
+ if (spec.entry || spec.exits.length > 0 || spec.stopMcUsd !== null) {
456
+ const who = sym === '__selected__' ? 'this token' : sym;
457
+ return { kind: 'propose-plan', side: planSide, target: sym, entry: spec.entry, exits: spec.exits, stopMcUsd: spec.stopMcUsd, moonBag: spec.moonBag,
458
+ say: `drafting an order plan for ${who} — review the card and hold to arm (I can't arm it by voice)` };
459
+ }
460
+ }
461
+ // a cue word but no clear side / nothing structured parsed: fall through to the normal ticket path.
462
+ }
463
+ // --- Interrogative phrasing is an ANALYSIS question even if it contains a trade word ("should I
464
+ // ape this?"). Imperative trade commands ("buy ...", "sell ...") stay on the ticket path. ---
465
+ const interrogative = /^(is|are|should|what|whats|why|how|who|does|do|can|could|would|tell|thoughts)\b/.test(text) || utterance.trim().endsWith('?');
466
+ const imperativeTrade = new RegExp(`^(${[...BUY_WORDS, ...SELL_WORDS].join('|')})\\b`).test(text);
467
+ if (interrogative && !imperativeTrade)
468
+ return { kind: 'analyst', question: utterance.trim() };
469
+ // --- Trade verb -> OPEN a PAPER ticket only (never confirm). ---
470
+ if (trade.isTrade) {
471
+ if (trade.uncertain) {
472
+ const q = trade.side
473
+ ? `Did you mean ${trade.side}? I heard "${trade.surface ?? ''}". Say "${trade.side}" or the other side.`
474
+ : 'I heard both buy and sell — say just one.';
475
+ return { kind: 'clarify', reason: 'trade action uncertain', question: q };
476
+ }
477
+ // task-1339 re-verify defect 3: a spoken size / amount / slippage is a money parameter voice must
478
+ // not set silently. Clarify and send the user to the ticket to set it there.
479
+ const size = detectSizeSpec(text);
480
+ if (size)
481
+ return { kind: 'clarify', reason: 'trade size/slippage spoken',
482
+ question: `I heard ${size}, but I don't set trade size or slippage by voice — open the ticket and set it there, or just say "${trade.side} <ticker>".` };
483
+ const tgt = resolveTarget(text, visible);
484
+ // A ticker that is also a command word (LIVE/SIGN/...) or more than one ticker in one breath is
485
+ // ambiguous: clarify instead of silently trading the highlighted row or the first ticker (defects 1 & 2).
486
+ if (tgt.kind === 'ambiguous')
487
+ return { kind: 'clarify', reason: `ambiguous ticker (${tgt.reason})`, question: clarifyTokenQ(tgt, trade.side) };
488
+ // "close" / "exit" with nothing named and no pronoun is ambiguous ("close the window"? "exit"?):
489
+ // clarify rather than open a SELL for whatever happens to be highlighted (task-1339, N6).
490
+ if (tgt.kind === 'none' && (trade.verb === 'close' || trade.verb === 'exit')) {
491
+ return { kind: 'clarify', reason: 'bare close/exit with no target', question: `${trade.verb === 'close' ? 'Close' : 'Exit'} what? Say "sell <ticker>" or "sell this one".` };
492
+ }
493
+ const sym = tgt.kind === 'symbol' ? tgt.symbol : '__selected__';
494
+ return { kind: 'open-ticket', side: trade.side, target: sym,
495
+ say: `opening a ${trade.side} ticket for ${sym === '__selected__' ? 'this token' : sym} — review and press enter to confirm` };
496
+ }
497
+ // --- Analysis hints or a trailing question mark -> the analyst. ---
498
+ if (has(text, ANALYSIS_HINTS) || utterance.trim().endsWith('?'))
499
+ return { kind: 'analyst', question: utterance.trim() };
500
+ // --- Default: treat it as a question for the analyst rather than guessing an action. ---
501
+ return { kind: 'analyst', question: utterance.trim() };
502
+ }
@@ -0,0 +1,103 @@
1
+ // The Swift source for the Apple Speech helper, kept as a string so the package ships no build step.
2
+ // AppleStt writes it to a cache dir and compiles it with `swiftc` on first use (SPEC: an Apple Speech
3
+ // helper compiled on the fly with swiftc if present). It uses on-device recognition (no key, no
4
+ // network) and streams NDJSON to stdout: {"type":"partial","text":...} … {"type":"final","text":...}.
5
+ //
6
+ // Protocol: it listens until it reads a line on stdin (the app writes "stop\n") or gets SIGINT, then
7
+ // prints the final line and exits 0. On authorization/denial it prints {"type":"error",...} and exits 1.
8
+ export const APPLE_HELPER_SWIFT = String.raw `
9
+ import Foundation
10
+ import AVFoundation
11
+ import Speech
12
+
13
+ // Line-buffered stdout so Node sees partials immediately.
14
+ setvbuf(stdout, nil, _IOLBF, 0)
15
+
16
+ func emit(_ obj: [String: Any]) {
17
+ if let data = try? JSONSerialization.data(withJSONObject: obj),
18
+ let s = String(data: data, encoding: .utf8) {
19
+ print(s)
20
+ }
21
+ }
22
+
23
+ let recognizer = SFSpeechRecognizer(locale: Locale(identifier: "en-US"))
24
+ guard let recognizer = recognizer, recognizer.isAvailable else {
25
+ emit(["type": "error", "message": "no speech recognizer available"]); exit(1)
26
+ }
27
+
28
+ let sem = DispatchSemaphore(value: 0)
29
+ var authorized = false
30
+ SFSpeechRecognizer.requestAuthorization { status in
31
+ authorized = (status == .authorized)
32
+ sem.signal()
33
+ }
34
+ sem.wait()
35
+ guard authorized else { emit(["type": "error", "message": "speech recognition not authorized"]); exit(1) }
36
+
37
+ let audioEngine = AVAudioEngine()
38
+ let request = SFSpeechAudioBufferRecognitionRequest()
39
+ request.shouldReportPartialResults = true
40
+ if #available(macOS 10.15, *) { request.requiresOnDeviceRecognition = true }
41
+
42
+ var finalText = ""
43
+ var finalConfidence = 1.0
44
+ var lastError = ""
45
+ let doneSem = DispatchSemaphore(value: 0)
46
+ var gotFinal = false
47
+ let task = recognizer.recognitionTask(with: request) { result, error in
48
+ if let result = result {
49
+ let text = result.bestTranscription.formattedString
50
+ finalText = text
51
+ let segs = result.bestTranscription.segments
52
+ if !segs.isEmpty { finalConfidence = Double(segs.map { $0.confidence }.reduce(0, +)) / Double(segs.count) }
53
+ emit(["type": "partial", "text": text])
54
+ if result.isFinal && !gotFinal { gotFinal = true; doneSem.signal() }
55
+ }
56
+ if let e = error {
57
+ // "No speech detected" (1110) and "cancelled" (216/301) are an empty result, not a failure.
58
+ let ns = e as NSError
59
+ if ![1110, 216, 301].contains(ns.code) { lastError = "\(e)" }
60
+ if !gotFinal { gotFinal = true; doneSem.signal() }
61
+ }
62
+ }
63
+
64
+ let input = audioEngine.inputNode
65
+ let format = input.outputFormat(forBus: 0)
66
+ input.installTap(onBus: 0, bufferSize: 1024, format: format) { buffer, _ in request.append(buffer) }
67
+ audioEngine.prepare()
68
+ do { try audioEngine.start() } catch {
69
+ emit(["type": "error", "message": "could not start audio engine: \(error)"]); exit(1)
70
+ }
71
+ emit(["type": "ready"])
72
+
73
+ var finishing = false
74
+ let finishLock = NSLock()
75
+ func finish() {
76
+ finishLock.lock()
77
+ if finishing { finishLock.unlock(); return }
78
+ finishing = true
79
+ finishLock.unlock()
80
+ audioEngine.stop()
81
+ input.removeTap(onBus: 0)
82
+ request.endAudio()
83
+ // The last words of an utterance arrive with the FINAL result, not the last partial: wait for it
84
+ // (bounded) instead of a fixed sleep, so a short phrase is not lost.
85
+ _ = doneSem.wait(timeout: .now() + 2.0)
86
+ task.finish()
87
+ if finalText.isEmpty && !lastError.isEmpty { emit(["type": "error", "message": lastError]) }
88
+ emit(["type": "final", "text": finalText, "confidence": finalConfidence])
89
+ exit(0)
90
+ }
91
+
92
+ signal(SIGINT) { _ in finish() }
93
+
94
+ // Stop when the parent writes a line to our stdin.
95
+ DispatchQueue.global().async {
96
+ while let line = readLine(strippingNewline: true) {
97
+ if line == "stop" { finish() }
98
+ }
99
+ finish() // stdin closed -> stop
100
+ }
101
+
102
+ RunLoop.main.run()
103
+ `;