dexterm-cli 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/dist/packages/agents/src/audit.js +122 -0
- package/dist/packages/agents/src/bus.js +379 -0
- package/dist/packages/agents/src/executor.js +177 -0
- package/dist/packages/agents/src/gate.js +176 -0
- package/dist/packages/agents/src/identity.js +179 -0
- package/dist/packages/agents/src/index.js +15 -0
- package/dist/packages/agents/src/proposals.js +153 -0
- package/dist/packages/agents/src/schema.js +87 -0
- package/dist/packages/agents/src/sdk.js +140 -0
- package/dist/packages/agents/src/tiers.js +114 -0
- package/dist/packages/agents/src/tools.js +69 -0
- package/dist/packages/agents/src/ws.js +158 -0
- package/dist/packages/cli/assets/claude/analyst-system.md +76 -0
- package/dist/packages/cli/assets/claude/plugin/.claude-plugin/plugin.json +5 -0
- package/dist/packages/cli/assets/claude/plugin/skills/REGISTRY.json +16 -0
- package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +67 -0
- package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +64 -0
- package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +63 -0
- package/dist/packages/cli/assets/claude/plugin/skills/trench-scan/SKILL.md +44 -0
- package/dist/packages/cli/assets/claude/schemas/larp.schema.json +117 -0
- package/dist/packages/cli/assets/claude/schemas/trench-scan.schema.json +71 -0
- package/dist/packages/cli/assets/claude/schemas/verdict.schema.json +115 -0
- package/dist/packages/cli/bin/dexterm.mjs +11 -0
- package/dist/packages/cli/src/agent-cmd.js +303 -0
- package/dist/packages/cli/src/agent-roster.js +26 -0
- package/dist/packages/cli/src/analyst/backend.js +526 -0
- package/dist/packages/cli/src/analyst/results.js +38 -0
- package/dist/packages/cli/src/analyst/session.js +319 -0
- package/dist/packages/cli/src/audit.js +51 -0
- package/dist/packages/cli/src/backtest-cmd.js +149 -0
- package/dist/packages/cli/src/boot-state.js +62 -0
- package/dist/packages/cli/src/custom.js +261 -0
- package/dist/packages/cli/src/display.js +191 -0
- package/dist/packages/cli/src/doctor.js +361 -0
- package/dist/packages/cli/src/feed.js +634 -0
- package/dist/packages/cli/src/fixtures.js +275 -0
- package/dist/packages/cli/src/follow-cmd.js +221 -0
- package/dist/packages/cli/src/images-config.js +59 -0
- package/dist/packages/cli/src/index.js +31 -0
- package/dist/packages/cli/src/kb-skill-cmd.js +240 -0
- package/dist/packages/cli/src/larp-record.js +21 -0
- package/dist/packages/cli/src/live/copy.js +80 -0
- package/dist/packages/cli/src/live/flow.js +233 -0
- package/dist/packages/cli/src/live/index.js +509 -0
- package/dist/packages/cli/src/live/intel.js +227 -0
- package/dist/packages/cli/src/live/newpairs.js +334 -0
- package/dist/packages/cli/src/live/screens/newpairs-live.js +219 -0
- package/dist/packages/cli/src/live/screens/token-live.js +459 -0
- package/dist/packages/cli/src/live/screens/watchlist-live.js +149 -0
- package/dist/packages/cli/src/live/token.js +585 -0
- package/dist/packages/cli/src/live/trade.js +929 -0
- package/dist/packages/cli/src/live/watchlist.js +178 -0
- package/dist/packages/cli/src/main.js +5583 -0
- package/dist/packages/cli/src/narrative-cmd.js +76 -0
- package/dist/packages/cli/src/pack-cmd.js +323 -0
- package/dist/packages/cli/src/plan-cmd.js +278 -0
- package/dist/packages/cli/src/popup.js +810 -0
- package/dist/packages/cli/src/rules-cmd.js +131 -0
- package/dist/packages/cli/src/rules.js +339 -0
- package/dist/packages/cli/src/scan/command.js +57 -0
- package/dist/packages/cli/src/scan/engine.js +419 -0
- package/dist/packages/cli/src/scan/observe.js +64 -0
- package/dist/packages/cli/src/scan/ui.js +98 -0
- package/dist/packages/cli/src/screener/filter.js +241 -0
- package/dist/packages/cli/src/screener/presets.js +80 -0
- package/dist/packages/cli/src/screener/rows.js +146 -0
- package/dist/packages/cli/src/screener/rug.js +50 -0
- package/dist/packages/cli/src/screener/sort.js +52 -0
- package/dist/packages/cli/src/screener/view.js +183 -0
- package/dist/packages/cli/src/screens/analyst-session.js +343 -0
- package/dist/packages/cli/src/screens/analyst.js +326 -0
- package/dist/packages/cli/src/screens/boot.js +74 -0
- package/dist/packages/cli/src/screens/bubbles.js +255 -0
- package/dist/packages/cli/src/screens/coin.js +33 -0
- package/dist/packages/cli/src/screens/follows.js +201 -0
- package/dist/packages/cli/src/screens/kol.js +90 -0
- package/dist/packages/cli/src/screens/narrative.js +90 -0
- package/dist/packages/cli/src/screens/newpairs.js +150 -0
- package/dist/packages/cli/src/screens/overlay.js +257 -0
- package/dist/packages/cli/src/screens/plans.js +126 -0
- package/dist/packages/cli/src/screens/positions.js +176 -0
- package/dist/packages/cli/src/screens/rules.js +170 -0
- package/dist/packages/cli/src/screens/screener.js +459 -0
- package/dist/packages/cli/src/screens/social-panel.js +107 -0
- package/dist/packages/cli/src/screens/ticket.js +342 -0
- package/dist/packages/cli/src/screens/token.js +312 -0
- package/dist/packages/cli/src/screens/trending.js +181 -0
- package/dist/packages/cli/src/screens/watchlist.js +104 -0
- package/dist/packages/cli/src/social/demo.js +96 -0
- package/dist/packages/cli/src/social/view.js +250 -0
- package/dist/packages/cli/src/state.js +463 -0
- package/dist/packages/cli/src/theme-cmd.js +126 -0
- package/dist/packages/cli/src/trade.js +319 -0
- package/dist/packages/cli/src/voice-hud.js +42 -0
- package/dist/packages/cli/src/wallet-cmd.js +440 -0
- package/dist/packages/cli/src/wallet-command.js +31 -0
- package/dist/packages/cli/src/widgets.js +208 -0
- package/dist/packages/cli/src/workspace-cmd.js +68 -0
- package/dist/packages/cli/src/x-cmd.js +154 -0
- package/dist/packages/custom/src/config.js +213 -0
- package/dist/packages/custom/src/index.js +10 -0
- package/dist/packages/custom/src/keymap.js +107 -0
- package/dist/packages/custom/src/layout.js +235 -0
- package/dist/packages/custom/src/packs.js +633 -0
- package/dist/packages/custom/src/rules.js +704 -0
- package/dist/packages/custom/src/schema.js +515 -0
- package/dist/packages/custom/src/themes.js +176 -0
- package/dist/packages/custom/src/widget-html.js +222 -0
- package/dist/packages/custom/src/workspace.js +141 -0
- package/dist/packages/data/src/analyst-actions.js +133 -0
- package/dist/packages/data/src/budget.js +88 -0
- package/dist/packages/data/src/cache.js +142 -0
- package/dist/packages/data/src/candles.js +168 -0
- package/dist/packages/data/src/clock.js +64 -0
- package/dist/packages/data/src/credits.js +137 -0
- package/dist/packages/data/src/detail.js +97 -0
- package/dist/packages/data/src/fast-lane.js +321 -0
- package/dist/packages/data/src/fixtures.js +135 -0
- package/dist/packages/data/src/health.js +245 -0
- package/dist/packages/data/src/http.js +277 -0
- package/dist/packages/data/src/images/cache.js +141 -0
- package/dist/packages/data/src/images/decode-worker.js +28 -0
- package/dist/packages/data/src/images/decode.js +81 -0
- package/dist/packages/data/src/images/fetch.js +162 -0
- package/dist/packages/data/src/images/gif.js +214 -0
- package/dist/packages/data/src/images/index.js +10 -0
- package/dist/packages/data/src/images/jpeg.js +544 -0
- package/dist/packages/data/src/images/png.js +310 -0
- package/dist/packages/data/src/images/service.js +334 -0
- package/dist/packages/data/src/images/types.js +27 -0
- package/dist/packages/data/src/index.js +208 -0
- package/dist/packages/data/src/intel/helius-history.js +59 -0
- package/dist/packages/data/src/intel/holder-class.js +125 -0
- package/dist/packages/data/src/intel/rules.js +421 -0
- package/dist/packages/data/src/intel/scan.js +170 -0
- package/dist/packages/data/src/larp/evidence.js +288 -0
- package/dist/packages/data/src/larp/links.js +137 -0
- package/dist/packages/data/src/larp/robots.js +90 -0
- package/dist/packages/data/src/larp/safe-fetch.js +389 -0
- package/dist/packages/data/src/limiter.js +341 -0
- package/dist/packages/data/src/model.js +12 -0
- package/dist/packages/data/src/paper.js +331 -0
- package/dist/packages/data/src/rpc-config.js +126 -0
- package/dist/packages/data/src/rpc-pool.js +166 -0
- package/dist/packages/data/src/social/chain.js +83 -0
- package/dist/packages/data/src/social/dedupe.js +205 -0
- package/dist/packages/data/src/social/feed.js +462 -0
- package/dist/packages/data/src/social/firewall.js +110 -0
- package/dist/packages/data/src/social/index.js +11 -0
- package/dist/packages/data/src/social/kol.js +269 -0
- package/dist/packages/data/src/social/model.js +11 -0
- package/dist/packages/data/src/social/rank.js +184 -0
- package/dist/packages/data/src/social/rss.js +113 -0
- package/dist/packages/data/src/social/summary.js +104 -0
- package/dist/packages/data/src/social/venues.js +186 -0
- package/dist/packages/data/src/sources/dexscreener.js +170 -0
- package/dist/packages/data/src/sources/geckoterminal.js +133 -0
- package/dist/packages/data/src/sources/goplus.js +161 -0
- package/dist/packages/data/src/sources/jupiter-swap.js +236 -0
- package/dist/packages/data/src/sources/jupiter.js +198 -0
- package/dist/packages/data/src/sources/pda.js +113 -0
- package/dist/packages/data/src/sources/pump-curve.js +58 -0
- package/dist/packages/data/src/sources/pumpportal.js +247 -0
- package/dist/packages/data/src/sources/rugcheck.js +71 -0
- package/dist/packages/data/src/sources/solana-rpc.js +299 -0
- package/dist/packages/data/src/sources/solana-ws.js +287 -0
- package/dist/packages/data/src/store.js +289 -0
- package/dist/packages/data/src/trade/bridge.js +464 -0
- package/dist/packages/data/src/trade/caps.js +323 -0
- package/dist/packages/data/src/trade/copy/auto.js +385 -0
- package/dist/packages/data/src/trade/copy/follows.js +103 -0
- package/dist/packages/data/src/trade/copy/index.js +9 -0
- package/dist/packages/data/src/trade/copy/mirror.js +91 -0
- package/dist/packages/data/src/trade/copy/stats.js +120 -0
- package/dist/packages/data/src/trade/copy/tracker.js +158 -0
- package/dist/packages/data/src/trade/copy/trades.js +87 -0
- package/dist/packages/data/src/trade/copy/types.js +6 -0
- package/dist/packages/data/src/trade/executor.js +933 -0
- package/dist/packages/data/src/trade/kill-switch.js +47 -0
- package/dist/packages/data/src/trade/link-store.js +52 -0
- package/dist/packages/data/src/trade/plans/confirm.js +157 -0
- package/dist/packages/data/src/trade/plans/engine.js +784 -0
- package/dist/packages/data/src/trade/plans/executor-live.js +220 -0
- package/dist/packages/data/src/trade/plans/executor-paper.js +44 -0
- package/dist/packages/data/src/trade/plans/executor.js +11 -0
- package/dist/packages/data/src/trade/plans/index.js +12 -0
- package/dist/packages/data/src/trade/plans/journal.js +77 -0
- package/dist/packages/data/src/trade/plans/mandate.js +129 -0
- package/dist/packages/data/src/trade/plans/plan.js +179 -0
- package/dist/packages/data/src/trade/plans/service.js +168 -0
- package/dist/packages/data/src/trade/plans/types.js +6 -0
- package/dist/packages/data/src/trade/rpc-exec.js +229 -0
- package/dist/packages/data/src/trade/signer/client.js +197 -0
- package/dist/packages/data/src/trade/signer/fast-wallet.js +154 -0
- package/dist/packages/data/src/trade/signer/host.js +97 -0
- package/dist/packages/data/src/trade/signer/main.js +121 -0
- package/dist/packages/data/src/trade/signer/probe-builder.js +85 -0
- package/dist/packages/data/src/trade/signer/protocol.js +92 -0
- package/dist/packages/data/src/trade/signer/service.js +400 -0
- package/dist/packages/data/src/trade/signer/sol-usd-feed.js +98 -0
- package/dist/packages/data/src/trade/sol-usd.js +86 -0
- package/dist/packages/data/src/trade/swap-build.js +108 -0
- package/dist/packages/data/src/trade/token-delta.js +201 -0
- package/dist/packages/data/src/trade/tx-decode.js +632 -0
- package/dist/packages/data/src/util.js +24 -0
- package/dist/packages/data/src/watchlist.js +176 -0
- package/dist/packages/data/src/x/api.js +256 -0
- package/dist/packages/data/src/x/budget.js +66 -0
- package/dist/packages/data/src/x/config.js +49 -0
- package/dist/packages/data/src/x/index.js +10 -0
- package/dist/packages/data/src/x/mock-server.js +217 -0
- package/dist/packages/data/src/x/oauth.js +242 -0
- package/dist/packages/data/src/x/pkce.js +21 -0
- package/dist/packages/data/src/x/source.js +285 -0
- package/dist/packages/data/src/x/token-store.js +134 -0
- package/dist/packages/data/src/x/transport.js +71 -0
- package/dist/packages/data/vendor/solana-web3/LICENSE +20 -0
- package/dist/packages/data/vendor/solana-web3/NOTICE.md +13 -0
- package/dist/packages/data/vendor/solana-web3/index.iife.min.js +20 -0
- package/dist/packages/mcp/assets/cases.json +1194 -0
- package/dist/packages/mcp/assets/kb/entries/cabal-and-coordinated-launches.md +28 -0
- package/dist/packages/mcp/assets/kb/entries/common-rug-patterns.md +40 -0
- package/dist/packages/mcp/assets/kb/entries/dbc-mechanics.md +30 -0
- package/dist/packages/mcp/assets/kb/entries/dev-wallet-behaviour.md +33 -0
- package/dist/packages/mcp/assets/kb/entries/glossary.md +35 -0
- package/dist/packages/mcp/assets/kb/entries/holder-concentration.md +30 -0
- package/dist/packages/mcp/assets/kb/entries/jito-bundles.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/launchlab-curve.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/lp-burn-vs-lock.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/metadata-and-copycats.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/mint-freeze-authority.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/narrative-lifecycle.md +32 -0
- package/dist/packages/mcp/assets/kb/entries/pump-bonding-curve.md +33 -0
- package/dist/packages/mcp/assets/kb/entries/pumpswap-migration.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/sniping-and-snipers.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/token2022-extensions-risk.md +32 -0
- package/dist/packages/mcp/assets/kb/entries/wash-trading-fake-volume.md +33 -0
- package/dist/packages/mcp/assets/skills/rug-check.md +52 -0
- package/dist/packages/mcp/assets/skills/token-dossier.md +57 -0
- package/dist/packages/mcp/assets/skills/trench-scan.md +40 -0
- package/dist/packages/mcp/bin/dexterm-mcp.mjs +16 -0
- package/dist/packages/mcp/src/dossier.js +78 -0
- package/dist/packages/mcp/src/firewall.js +21 -0
- package/dist/packages/mcp/src/index.js +22 -0
- package/dist/packages/mcp/src/server.js +849 -0
- package/dist/packages/mcp/src/user-content.js +434 -0
- package/dist/packages/packs/src/backtest.js +183 -0
- package/dist/packages/packs/src/digest.js +95 -0
- package/dist/packages/packs/src/firewall.js +57 -0
- package/dist/packages/packs/src/index.js +19 -0
- package/dist/packages/packs/src/larp-eval.js +100 -0
- package/dist/packages/packs/src/narrative.js +309 -0
- package/dist/packages/packs/src/reason.js +135 -0
- package/dist/packages/packs/src/rug-model.js +137 -0
- package/dist/packages/packs/src/schema.js +54 -0
- package/dist/packages/packs/src/token-dossier.js +174 -0
- package/dist/packages/packs/src/verdicts.js +17 -0
- package/dist/packages/packs/src/wallet-dossier.js +105 -0
- package/dist/packages/popup/src/config.js +71 -0
- package/dist/packages/popup/src/dock.js +271 -0
- package/dist/packages/popup/src/host.js +210 -0
- package/dist/packages/popup/src/index.js +12 -0
- package/dist/packages/popup/src/loopback.js +53 -0
- package/dist/packages/popup/src/open.js +183 -0
- package/dist/packages/popup/src/pages/bubbles-lib.js +160 -0
- package/dist/packages/popup/src/pages/bubbles.css +23 -0
- package/dist/packages/popup/src/pages/bubbles.html +36 -0
- package/dist/packages/popup/src/pages/bubbles.js +214 -0
- package/dist/packages/popup/src/pages/chart-lib.js +262 -0
- package/dist/packages/popup/src/pages/chart.css +50 -0
- package/dist/packages/popup/src/pages/chart.html +56 -0
- package/dist/packages/popup/src/pages/chart.js +554 -0
- package/dist/packages/popup/src/pages/common.css +34 -0
- package/dist/packages/popup/src/pages/common.js +187 -0
- package/dist/packages/popup/src/pages/feeds.html +20 -0
- package/dist/packages/popup/src/pages/feeds.js +77 -0
- package/dist/packages/popup/src/pages/narrative.html +20 -0
- package/dist/packages/popup/src/pages/narrative.js +61 -0
- package/dist/packages/popup/src/pages/plans.css +30 -0
- package/dist/packages/popup/src/pages/plans.html +26 -0
- package/dist/packages/popup/src/pages/plans.js +82 -0
- package/dist/packages/popup/src/pages/roster.css +41 -0
- package/dist/packages/popup/src/pages/roster.html +25 -0
- package/dist/packages/popup/src/pages/roster.js +75 -0
- package/dist/packages/popup/src/pages/token.html +20 -0
- package/dist/packages/popup/src/pages/token.js +112 -0
- package/dist/packages/popup/src/pages/widget-sdk.js +224 -0
- package/dist/packages/popup/src/pages/widget.css +5 -0
- package/dist/packages/popup/src/pages/widget.html +21 -0
- package/dist/packages/popup/src/pages/widget.js +85 -0
- package/dist/packages/popup/src/protocol.js +31 -0
- package/dist/packages/popup/src/selftest.js +156 -0
- package/dist/packages/popup/src/server.js +582 -0
- package/dist/packages/popup/src/sinkproxy.js +167 -0
- package/dist/packages/popup/src/widget-html.js +231 -0
- package/dist/packages/popup/vendor/lightweight-charts/LICENSE +201 -0
- package/dist/packages/popup/vendor/lightweight-charts/NOTICE +2 -0
- package/dist/packages/popup/vendor/lightweight-charts/README.md +30 -0
- package/dist/packages/popup/vendor/lightweight-charts/VERSION +1 -0
- package/dist/packages/popup/vendor/lightweight-charts/lightweight-charts.standalone.production.js +7 -0
- package/dist/packages/recorder/bin/dexterm-recorder.mjs +11 -0
- package/dist/packages/recorder/src/cli.js +113 -0
- package/dist/packages/recorder/src/export.js +71 -0
- package/dist/packages/recorder/src/index.js +20 -0
- package/dist/packages/recorder/src/labels.js +151 -0
- package/dist/packages/recorder/src/open.js +29 -0
- package/dist/packages/recorder/src/recorder.js +553 -0
- package/dist/packages/recorder/src/schedule.js +43 -0
- package/dist/packages/recorder/src/schema.js +135 -0
- package/dist/packages/recorder/src/snapshot.js +191 -0
- package/dist/packages/recorder/src/sweep.js +52 -0
- package/dist/packages/recorder/src/verdicts.js +52 -0
- package/dist/packages/tui/src/bubbles.js +415 -0
- package/dist/packages/tui/src/canvas.js +370 -0
- package/dist/packages/tui/src/chrome.js +85 -0
- package/dist/packages/tui/src/color.js +133 -0
- package/dist/packages/tui/src/flush.js +62 -0
- package/dist/packages/tui/src/fmt.js +188 -0
- package/dist/packages/tui/src/fx-trade.js +278 -0
- package/dist/packages/tui/src/fx.js +527 -0
- package/dist/packages/tui/src/images/blocks.js +141 -0
- package/dist/packages/tui/src/images/coin.js +22 -0
- package/dist/packages/tui/src/images/detect.js +140 -0
- package/dist/packages/tui/src/images/index.js +7 -0
- package/dist/packages/tui/src/images/layer.js +146 -0
- package/dist/packages/tui/src/images/pixels.js +157 -0
- package/dist/packages/tui/src/images/protocols.js +87 -0
- package/dist/packages/tui/src/index.js +15 -0
- package/dist/packages/tui/src/keymap.js +355 -0
- package/dist/packages/tui/src/layout.js +11 -0
- package/dist/packages/tui/src/sound.js +177 -0
- package/dist/packages/tui/src/theme.js +182 -0
- package/dist/packages/tui/src/widgets.js +598 -0
- package/dist/packages/voice/src/controller.js +395 -0
- package/dist/packages/voice/src/doctor.js +63 -0
- package/dist/packages/voice/src/engines.js +44 -0
- package/dist/packages/voice/src/index.js +16 -0
- package/dist/packages/voice/src/router.js +502 -0
- package/dist/packages/voice/src/stt/apple-helper.swift.js +103 -0
- package/dist/packages/voice/src/stt/apple.js +179 -0
- package/dist/packages/voice/src/stt/fake.js +34 -0
- package/dist/packages/voice/src/stt/whisper.js +102 -0
- package/dist/packages/voice/src/summary.js +44 -0
- package/dist/packages/voice/src/tts/fake.js +30 -0
- package/dist/packages/voice/src/tts/say.js +96 -0
- package/dist/packages/voice/src/types.js +3 -0
- package/package.json +42 -4
- 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
|
+
`;
|