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.
- package/LICENSE +661 -0
- package/dist/packages/agents/src/audit.js +122 -0
- package/dist/packages/agents/src/bus.js +394 -0
- package/dist/packages/agents/src/executor.js +202 -0
- package/dist/packages/agents/src/gate.js +179 -0
- package/dist/packages/agents/src/identity.js +218 -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 +119 -0
- package/dist/packages/agents/src/tools.js +72 -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 +75 -0
- package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +74 -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 +336 -0
- package/dist/packages/cli/src/agent-roster.js +26 -0
- package/dist/packages/cli/src/alerts/adapters/desktop.js +40 -0
- package/dist/packages/cli/src/alerts/adapters/discord.js +36 -0
- package/dist/packages/cli/src/alerts/adapters/telegram.js +43 -0
- package/dist/packages/cli/src/alerts/adapters/toast.js +16 -0
- package/dist/packages/cli/src/alerts/creds.js +83 -0
- package/dist/packages/cli/src/alerts/format.js +41 -0
- package/dist/packages/cli/src/alerts/index.js +10 -0
- package/dist/packages/cli/src/alerts/notifier.js +53 -0
- package/dist/packages/cli/src/alerts/route.js +19 -0
- package/dist/packages/cli/src/alerts/types.js +1 -0
- package/dist/packages/cli/src/alerts-cmd.js +212 -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 +208 -0
- package/dist/packages/cli/src/boot-state.js +62 -0
- package/dist/packages/cli/src/coin-target.js +2 -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 +375 -0
- package/dist/packages/cli/src/export.js +235 -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 +511 -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 +5785 -0
- package/dist/packages/cli/src/mcp-cmd.js +326 -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 +358 -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/skill-eval-build.js +109 -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/update.js +294 -0
- package/dist/packages/cli/src/version.js +3 -0
- package/dist/packages/cli/src/voice-cmd.js +119 -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/watch-cmd.js +190 -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 +538 -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/coin-target.js +82 -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 +211 -0
- package/dist/packages/data/src/intel/helius-history.js +70 -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/link-baseline.js +196 -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 +184 -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 +252 -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 +936 -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 +20 -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/skill-backtest.js +300 -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 +18 -0
- package/dist/packages/tui/src/keymap.js +357 -0
- package/dist/packages/tui/src/layout.js +11 -0
- package/dist/packages/tui/src/raster-font.js +195 -0
- package/dist/packages/tui/src/raster.js +213 -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 +407 -0
- package/dist/packages/voice/src/doctor.js +131 -0
- package/dist/packages/voice/src/download.js +184 -0
- package/dist/packages/voice/src/engines.js +84 -0
- package/dist/packages/voice/src/index.js +20 -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 +112 -0
- package/dist/packages/voice/src/summary.js +44 -0
- package/dist/packages/voice/src/tts/espeak.js +131 -0
- package/dist/packages/voice/src/tts/fake.js +30 -0
- package/dist/packages/voice/src/tts/piper.js +206 -0
- package/dist/packages/voice/src/tts/say.js +132 -0
- package/dist/packages/voice/src/tts/voices.js +206 -0
- package/dist/packages/voice/src/types.js +3 -0
- package/package.json +38 -4
- package/README.md +0 -3
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Fixture recorder / replayer. The recorder wraps a live transport and captures
|
|
2
|
+
// responses into a JSON file; the replayer serves only from that file and fails
|
|
3
|
+
// loudly on a miss, so tests can never fall through to the network.
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
5
|
+
import { dirname } from 'node:path';
|
|
6
|
+
import { containsSecret, redactSecrets } from "./rpc-config.js";
|
|
7
|
+
/**
|
|
8
|
+
* Match key: method + normalized URL, plus the canonical JSON body for POSTs. Secret query
|
|
9
|
+
* values (`api-key=`) are redacted in the key, so a recording made with a Helius key replays
|
|
10
|
+
* with any key or none, and the key itself never reaches a fixture file.
|
|
11
|
+
*/
|
|
12
|
+
export function requestKey(req) {
|
|
13
|
+
const base = `${req.method} ${redactSecrets(new URL(req.url).toString())}`;
|
|
14
|
+
return req.body === undefined ? base : `${base} ${canonicalBody(req.body)}`;
|
|
15
|
+
}
|
|
16
|
+
function canonicalBody(body) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.stringify(JSON.parse(body));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return body;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function entryKey(e) {
|
|
25
|
+
return requestKey({ method: e.method, url: e.url, body: e.requestBody === undefined ? undefined : JSON.stringify(e.requestBody) });
|
|
26
|
+
}
|
|
27
|
+
export class FixtureStore {
|
|
28
|
+
entries = new Map();
|
|
29
|
+
note;
|
|
30
|
+
recordedAt;
|
|
31
|
+
/** Recorded WebSocket sessions (see StreamRecording). */
|
|
32
|
+
streams = [];
|
|
33
|
+
static load(path) {
|
|
34
|
+
const file = JSON.parse(readFileSync(path, 'utf8'));
|
|
35
|
+
if (file.version !== 1)
|
|
36
|
+
throw new Error(`unsupported fixture version in ${path}`);
|
|
37
|
+
const store = new FixtureStore();
|
|
38
|
+
store.note = file.note;
|
|
39
|
+
store.recordedAt = file.recordedAt;
|
|
40
|
+
store.streams = file.streams ?? [];
|
|
41
|
+
for (const e of file.entries)
|
|
42
|
+
store.add(e);
|
|
43
|
+
return store;
|
|
44
|
+
}
|
|
45
|
+
static fromEntries(entries) {
|
|
46
|
+
const store = new FixtureStore();
|
|
47
|
+
for (const e of entries)
|
|
48
|
+
store.add(e);
|
|
49
|
+
return store;
|
|
50
|
+
}
|
|
51
|
+
add(entry) {
|
|
52
|
+
this.entries.set(entryKey(entry), entry);
|
|
53
|
+
}
|
|
54
|
+
find(req) {
|
|
55
|
+
return this.entries.get(requestKey(req));
|
|
56
|
+
}
|
|
57
|
+
list() {
|
|
58
|
+
return [...this.entries.values()];
|
|
59
|
+
}
|
|
60
|
+
get size() {
|
|
61
|
+
return this.entries.size;
|
|
62
|
+
}
|
|
63
|
+
save(path, note) {
|
|
64
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
65
|
+
const file = {
|
|
66
|
+
version: 1,
|
|
67
|
+
recordedAt: new Date().toISOString(),
|
|
68
|
+
note: note ?? this.note,
|
|
69
|
+
entries: this.list(),
|
|
70
|
+
};
|
|
71
|
+
if (this.streams.length)
|
|
72
|
+
file.streams = this.streams;
|
|
73
|
+
const text = JSON.stringify(file, null, 1) + '\n';
|
|
74
|
+
// Belt and braces: the key is redacted at capture time; refuse to write a file that still carries one.
|
|
75
|
+
if (containsSecret(text))
|
|
76
|
+
throw new Error('refusing to save a recording that contains an api-key value');
|
|
77
|
+
writeFileSync(path, text);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export function toEntry(req, res, recordedAt = new Date().toISOString()) {
|
|
81
|
+
let body = res.body;
|
|
82
|
+
let bodyIsJson = false;
|
|
83
|
+
try {
|
|
84
|
+
body = JSON.parse(res.body);
|
|
85
|
+
bodyIsJson = true;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// keep raw text
|
|
89
|
+
}
|
|
90
|
+
const entry = { method: req.method, url: redactSecrets(new URL(req.url).toString()), status: res.status, headers: res.headers, body, bodyIsJson, latencyMs: res.latencyMs, recordedAt };
|
|
91
|
+
if (req.body !== undefined) {
|
|
92
|
+
try {
|
|
93
|
+
entry.requestBody = JSON.parse(req.body);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
entry.requestBody = req.body;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return entry;
|
|
100
|
+
}
|
|
101
|
+
/** Live transport + capture. Throws once `maxRequests` would be exceeded. */
|
|
102
|
+
export function recordingTransport(live, store, opts) {
|
|
103
|
+
let n = 0;
|
|
104
|
+
const t = (async (req) => {
|
|
105
|
+
if (n >= opts.maxRequests)
|
|
106
|
+
throw new Error(`recorder request budget exhausted (${opts.maxRequests})`);
|
|
107
|
+
const seq = ++n;
|
|
108
|
+
const res = await live(req);
|
|
109
|
+
store.add(toEntry(req, res));
|
|
110
|
+
opts.onRequest?.(seq, req, res);
|
|
111
|
+
return res;
|
|
112
|
+
});
|
|
113
|
+
t.count = () => n;
|
|
114
|
+
return t;
|
|
115
|
+
}
|
|
116
|
+
export class FixtureMissError extends Error {
|
|
117
|
+
constructor(key) {
|
|
118
|
+
super(`no recorded fixture for ${key} (tests never hit the network; re-record with scripts/record-fixtures.ts)`);
|
|
119
|
+
this.name = 'FixtureMissError';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** Serve recorded responses only. Misses throw FixtureMissError. */
|
|
123
|
+
export function replayTransport(store) {
|
|
124
|
+
return async (req) => {
|
|
125
|
+
const e = store.find(req);
|
|
126
|
+
if (!e)
|
|
127
|
+
throw new FixtureMissError(requestKey(req));
|
|
128
|
+
return {
|
|
129
|
+
status: e.status,
|
|
130
|
+
headers: { ...e.headers },
|
|
131
|
+
body: e.bodyIsJson ? JSON.stringify(e.body) : String(e.body),
|
|
132
|
+
latencyMs: e.latencyMs,
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// Per-source health registry (SPEC §3.5, learning CL-08).
|
|
2
|
+
//
|
|
3
|
+
// Rules: three consecutive failures, or two 429s inside a minute, flip a source
|
|
4
|
+
// to `degraded` for a backoff window (30 s, doubling per repeat up to 10 min).
|
|
5
|
+
// When the window ends the source is `probing` (half-open): requests are allowed
|
|
6
|
+
// again, one success returns it to `ok` and resets the backoff, one failure
|
|
7
|
+
// re-degrades it with the doubled window. Keyed sources without a key are
|
|
8
|
+
// `needs-key`; feature-flagged sources can be `disabled`.
|
|
9
|
+
//
|
|
10
|
+
// The registry gates which columns, features and AI tools are offered.
|
|
11
|
+
import { systemClock } from "./clock.js";
|
|
12
|
+
export const DEFAULT_HEALTH_POLICY = {
|
|
13
|
+
failureThreshold: 3,
|
|
14
|
+
rateLimitThreshold: 2,
|
|
15
|
+
rateLimitWindowMs: 60_000,
|
|
16
|
+
baseBackoffMs: 30_000,
|
|
17
|
+
maxBackoffMs: 600_000,
|
|
18
|
+
};
|
|
19
|
+
/** Sources that cannot work without a user-supplied key (SPEC §3.2 Tier 1). */
|
|
20
|
+
export const KEYED_SOURCES = new Set(['helius', 'birdeye']);
|
|
21
|
+
export class HealthRegistry {
|
|
22
|
+
entries = new Map();
|
|
23
|
+
clock;
|
|
24
|
+
policy;
|
|
25
|
+
keys;
|
|
26
|
+
disabled;
|
|
27
|
+
usage;
|
|
28
|
+
listeners = new Set();
|
|
29
|
+
constructor(opts = {}) {
|
|
30
|
+
this.clock = opts.clock ?? systemClock;
|
|
31
|
+
this.policy = { ...DEFAULT_HEALTH_POLICY, ...opts.policy };
|
|
32
|
+
this.keys = { ...opts.keys };
|
|
33
|
+
this.disabled = new Set(opts.disabled ?? []);
|
|
34
|
+
this.usage = opts.usage;
|
|
35
|
+
}
|
|
36
|
+
onChange(fn) {
|
|
37
|
+
this.listeners.add(fn);
|
|
38
|
+
return () => this.listeners.delete(fn);
|
|
39
|
+
}
|
|
40
|
+
setKey(source, present) {
|
|
41
|
+
const before = this.state(source);
|
|
42
|
+
this.keys[source] = present;
|
|
43
|
+
this.rebase(source);
|
|
44
|
+
this.emitIfChanged(source, before);
|
|
45
|
+
}
|
|
46
|
+
setDisabled(source, disabled) {
|
|
47
|
+
const before = this.state(source);
|
|
48
|
+
if (disabled)
|
|
49
|
+
this.disabled.add(source);
|
|
50
|
+
else
|
|
51
|
+
this.disabled.delete(source);
|
|
52
|
+
this.rebase(source);
|
|
53
|
+
this.emitIfChanged(source, before);
|
|
54
|
+
}
|
|
55
|
+
/** Re-derive key/flag state; a degraded source stays degraded unless it is now gated off. */
|
|
56
|
+
rebase(source) {
|
|
57
|
+
const e = this.entry(source);
|
|
58
|
+
const base = this.initialBase(source);
|
|
59
|
+
if (e.base !== 'degraded' || base !== 'ok') {
|
|
60
|
+
if (e.base !== base)
|
|
61
|
+
e.since = this.clock.now();
|
|
62
|
+
e.base = base;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Record the outcome of one call to `source`. */
|
|
66
|
+
record(source, outcome) {
|
|
67
|
+
const now = this.clock.now();
|
|
68
|
+
const before = this.state(source);
|
|
69
|
+
const e = this.entry(source);
|
|
70
|
+
if (outcome.latencyMs !== undefined)
|
|
71
|
+
e.latencyMs = outcome.latencyMs;
|
|
72
|
+
if (outcome.ok) {
|
|
73
|
+
e.okCount++;
|
|
74
|
+
e.consecutiveFailures = 0;
|
|
75
|
+
if (e.base === 'degraded') {
|
|
76
|
+
e.base = 'ok';
|
|
77
|
+
e.since = now;
|
|
78
|
+
e.degradedUntil = 0;
|
|
79
|
+
e.backoffMs = 0;
|
|
80
|
+
e.nextBackoffMs = this.policy.baseBackoffMs;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
e.errorCount++;
|
|
85
|
+
e.consecutiveFailures++;
|
|
86
|
+
e.lastError = outcome.error ?? (outcome.status ? `HTTP ${outcome.status}` : 'error');
|
|
87
|
+
if (outcome.status === 429)
|
|
88
|
+
e.recent429.push(now);
|
|
89
|
+
e.recent429 = e.recent429.filter((t) => t > now - this.policy.rateLimitWindowMs);
|
|
90
|
+
const probing = before === 'probing';
|
|
91
|
+
const tripped = e.consecutiveFailures >= this.policy.failureThreshold || e.recent429.length >= this.policy.rateLimitThreshold;
|
|
92
|
+
if ((probing || tripped) && (e.base === 'ok' || e.base === 'degraded') && before !== 'degraded') {
|
|
93
|
+
this.degrade(e, now);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
this.emitIfChanged(source, before);
|
|
97
|
+
}
|
|
98
|
+
state(source) {
|
|
99
|
+
const e = this.entry(source);
|
|
100
|
+
if (e.base === 'degraded')
|
|
101
|
+
return this.clock.now() >= e.degradedUntil ? 'probing' : 'degraded';
|
|
102
|
+
return e.base;
|
|
103
|
+
}
|
|
104
|
+
/** True when requests to `source` should be attempted (ok or probing). */
|
|
105
|
+
isAvailable(source) {
|
|
106
|
+
const s = this.state(source);
|
|
107
|
+
return s === 'ok' || s === 'probing';
|
|
108
|
+
}
|
|
109
|
+
get(source) {
|
|
110
|
+
const e = this.entry(source);
|
|
111
|
+
return {
|
|
112
|
+
source,
|
|
113
|
+
state: this.state(source),
|
|
114
|
+
since: e.since,
|
|
115
|
+
consecutiveFailures: e.consecutiveFailures,
|
|
116
|
+
degradedUntil: e.degradedUntil,
|
|
117
|
+
backoffMs: e.backoffMs,
|
|
118
|
+
latencyMs: e.latencyMs,
|
|
119
|
+
lastError: e.lastError,
|
|
120
|
+
okCount: e.okCount,
|
|
121
|
+
errorCount: e.errorCount,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** SPEC §3.3 `Health` records for every source seen (or the ones asked for). */
|
|
125
|
+
snapshot(sources) {
|
|
126
|
+
const ids = sources ?? [...this.entries.keys()];
|
|
127
|
+
return ids.map((source) => {
|
|
128
|
+
const h = this.get(source);
|
|
129
|
+
const u = this.usage?.(source);
|
|
130
|
+
return {
|
|
131
|
+
source,
|
|
132
|
+
ok: h.state === 'ok' || h.state === 'probing',
|
|
133
|
+
state: h.state === 'probing' ? 'degraded' : h.state,
|
|
134
|
+
latencyMs: h.latencyMs ?? null,
|
|
135
|
+
rpmUsed: u?.rpmUsed ?? 0,
|
|
136
|
+
rpmLimit: u?.rpmLimit ?? null,
|
|
137
|
+
lastError: h.lastError ?? null,
|
|
138
|
+
since: h.since,
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
degrade(e, now) {
|
|
143
|
+
e.base = 'degraded';
|
|
144
|
+
e.since = now;
|
|
145
|
+
e.backoffMs = e.nextBackoffMs;
|
|
146
|
+
e.degradedUntil = now + e.backoffMs;
|
|
147
|
+
e.nextBackoffMs = Math.min(e.nextBackoffMs * 2, this.policy.maxBackoffMs);
|
|
148
|
+
e.consecutiveFailures = 0;
|
|
149
|
+
e.recent429 = [];
|
|
150
|
+
}
|
|
151
|
+
initialBase(source) {
|
|
152
|
+
if (this.disabled.has(source))
|
|
153
|
+
return 'disabled';
|
|
154
|
+
if (KEYED_SOURCES.has(source) && !this.keys[source])
|
|
155
|
+
return 'needs-key';
|
|
156
|
+
return 'ok';
|
|
157
|
+
}
|
|
158
|
+
entry(source) {
|
|
159
|
+
let e = this.entries.get(source);
|
|
160
|
+
if (!e) {
|
|
161
|
+
e = {
|
|
162
|
+
source,
|
|
163
|
+
base: this.initialBase(source),
|
|
164
|
+
since: this.clock.now(),
|
|
165
|
+
consecutiveFailures: 0,
|
|
166
|
+
recent429: [],
|
|
167
|
+
degradedUntil: 0,
|
|
168
|
+
backoffMs: 0,
|
|
169
|
+
nextBackoffMs: this.policy.baseBackoffMs,
|
|
170
|
+
okCount: 0,
|
|
171
|
+
errorCount: 0,
|
|
172
|
+
};
|
|
173
|
+
this.entries.set(source, e);
|
|
174
|
+
}
|
|
175
|
+
return e;
|
|
176
|
+
}
|
|
177
|
+
emitIfChanged(source, before) {
|
|
178
|
+
const after = this.state(source);
|
|
179
|
+
if (after !== before)
|
|
180
|
+
for (const fn of this.listeners)
|
|
181
|
+
fn(source, after);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** Screener columns (SPEC §4.2) and the sources that can fill them. */
|
|
185
|
+
export const COLUMN_SOURCES = {
|
|
186
|
+
// Jupiter Tokens v2 carries price/stats too, so these survive a DexScreener outage.
|
|
187
|
+
price: { anyOf: ['dexscreener', 'jupiter-tokens', 'jupiter-price'] },
|
|
188
|
+
'5m': { anyOf: ['dexscreener', 'jupiter-tokens'] },
|
|
189
|
+
'1h': { anyOf: ['dexscreener', 'jupiter-tokens'] },
|
|
190
|
+
'6h': { anyOf: ['dexscreener', 'jupiter-tokens'] },
|
|
191
|
+
'24h': { anyOf: ['dexscreener', 'jupiter-tokens'] },
|
|
192
|
+
vol: { anyOf: ['dexscreener', 'jupiter-tokens'] },
|
|
193
|
+
liq: { anyOf: ['dexscreener', 'jupiter-tokens'] },
|
|
194
|
+
mcap: { anyOf: ['dexscreener', 'jupiter-tokens'] },
|
|
195
|
+
age: { anyOf: ['dexscreener', 'jupiter-tokens'] },
|
|
196
|
+
bs: { anyOf: ['dexscreener', 'jupiter-tokens'], label: 'buy/sell txns' },
|
|
197
|
+
sparkline: { anyOf: ['dexscreener', 'jupiter-tokens'], label: 'local history of polled prices' },
|
|
198
|
+
holders: { anyOf: ['jupiter-tokens'] },
|
|
199
|
+
org: { anyOf: ['jupiter-tokens'], label: 'organic score' },
|
|
200
|
+
flags: { anyOf: ['jupiter-tokens', 'solana-rpc', 'helius', 'goplus'], label: 'mint/freeze authority' },
|
|
201
|
+
launchpad: { anyOf: ['jupiter-tokens'] },
|
|
202
|
+
bonding: { anyOf: ['pumpfun', 'pumpportal'] },
|
|
203
|
+
dev: { anyOf: ['jupiter-tokens', 'helius'] },
|
|
204
|
+
snipers: { anyOf: ['helius'] },
|
|
205
|
+
bundled: { anyOf: ['helius'] },
|
|
206
|
+
};
|
|
207
|
+
/** Screens, panels and AI tools that depend on specific sources (SPEC §3.2, §5.2). */
|
|
208
|
+
export const FEATURE_SOURCES = {
|
|
209
|
+
screener: { anyOf: ['dexscreener', 'jupiter-tokens'] },
|
|
210
|
+
'sol-price': { anyOf: ['jupiter-price', 'dexscreener'] },
|
|
211
|
+
candles: { anyOf: ['geckoterminal', 'birdeye'] },
|
|
212
|
+
'top-holders': { anyOf: ['helius', 'solana-rpc'] },
|
|
213
|
+
'new-pairs-live': { anyOf: ['pumpportal', 'jupiter-tokens'] },
|
|
214
|
+
rugcheck: { anyOf: ['rugcheck'] },
|
|
215
|
+
security: { anyOf: ['rugcheck', 'goplus'], label: 'risk opinions' },
|
|
216
|
+
authorities: { anyOf: ['helius', 'solana-rpc', 'goplus', 'jupiter-tokens'] },
|
|
217
|
+
forensics: { anyOf: ['helius'] },
|
|
218
|
+
'dev-history': { anyOf: ['helius', 'jupiter-tokens'] },
|
|
219
|
+
'top-traders': { anyOf: ['birdeye', 'helius'] },
|
|
220
|
+
'live-tape': { anyOf: ['geckoterminal', 'helius'] },
|
|
221
|
+
'new-pairs-poll': { anyOf: ['jupiter-tokens', 'rugcheck'] },
|
|
222
|
+
// Paper trading (task-1310): the ticket prices fills from the keyless swap quote only.
|
|
223
|
+
'trade-quote': { anyOf: ['jupiter-swap'], label: 'paper trade quote' },
|
|
224
|
+
};
|
|
225
|
+
const GATE_RANK = { ok: 0, degraded: 1, 'needs-key': 2, disabled: 3 };
|
|
226
|
+
export function gate(registry, id, spec) {
|
|
227
|
+
const via = spec.anyOf.filter((s) => registry.isAvailable(s));
|
|
228
|
+
if (via.length > 0)
|
|
229
|
+
return { id, state: 'ok', available: true, via };
|
|
230
|
+
// Report the most actionable reason: degraded beats needs-key beats disabled.
|
|
231
|
+
let best = 'disabled';
|
|
232
|
+
for (const s of spec.anyOf) {
|
|
233
|
+
const st = registry.state(s);
|
|
234
|
+
const g = st === 'degraded' ? 'degraded' : st === 'needs-key' ? 'needs-key' : st === 'disabled' ? 'disabled' : 'ok';
|
|
235
|
+
if (GATE_RANK[g] < GATE_RANK[best])
|
|
236
|
+
best = g;
|
|
237
|
+
}
|
|
238
|
+
return { id, state: best, available: false, via };
|
|
239
|
+
}
|
|
240
|
+
export function gateAll(registry, specs) {
|
|
241
|
+
const out = {};
|
|
242
|
+
for (const [id, spec] of Object.entries(specs))
|
|
243
|
+
out[id] = gate(registry, id, spec);
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// Keyless, read-only HTTP client. Every call goes through the health gate and
|
|
2
|
+
// the rate limiter, is timed, and reports its outcome to the health registry.
|
|
3
|
+
// GET for REST sources; POST only for JSON-RPC reads (the Solana RPC adapter
|
|
4
|
+
// allow-lists read-only methods), never with auth headers.
|
|
5
|
+
// The transport is pluggable: live fetch, recorder (live + capture) or replayer
|
|
6
|
+
// (fixtures only, never touches the network).
|
|
7
|
+
import { systemClock } from "./clock.js";
|
|
8
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
9
|
+
import { PriorityRef } from "./limiter.js";
|
|
10
|
+
import { redactSecrets } from "./rpc-config.js";
|
|
11
|
+
/** Response headers worth keeping (rate-limit hints); everything else is dropped. */
|
|
12
|
+
const KEEP_HEADERS = ['content-type', 'retry-after', 'x-ratelimit-limit', 'x-ratelimit-remaining', 'x-ratelimit-reset'];
|
|
13
|
+
/** Real network via global fetch. GET only, no auth headers, no cookies. */
|
|
14
|
+
export function liveTransport(opts = {}) {
|
|
15
|
+
const timeoutMs = opts.timeoutMs ?? 10_000;
|
|
16
|
+
const userAgent = opts.userAgent ?? 'dexterm/0.0.1 (+local terminal screener; keyless read-only)';
|
|
17
|
+
const shutdownSignal = opts.shutdownSignal;
|
|
18
|
+
return async (req) => {
|
|
19
|
+
const t0 = performance.now();
|
|
20
|
+
const headers = { accept: req.accept ?? 'application/json', 'user-agent': userAgent };
|
|
21
|
+
if (req.body !== undefined)
|
|
22
|
+
headers['content-type'] = 'application/json';
|
|
23
|
+
// Own the timeout + shutdown abort via one controller instead of AbortSignal.timeout(): the latter
|
|
24
|
+
// schedules an internal *ref'd* timer that is never cancelled when the fetch settles (or when a
|
|
25
|
+
// companion signal fires), so each live request left a ref'd ~10 s timer keeping the event loop
|
|
26
|
+
// alive — the 10-33 s quit hang (task-1319, QA M1). This timer is unref'd (never holds the loop on
|
|
27
|
+
// its own) and cleared the instant the request settles, and the shutdown signal aborts it at once.
|
|
28
|
+
// The caller may also pass its own abort (task-1310: bounded confirm quote); whichever fires first wins.
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
const onTimeout = () => controller.abort(new DOMException(`timed out after ${timeoutMs}ms`, 'TimeoutError'));
|
|
31
|
+
const onShutdown = () => controller.abort(new DOMException('shutting down', 'AbortError'));
|
|
32
|
+
const onCaller = () => controller.abort(req.signal?.reason ?? new DOMException('aborted by caller', 'AbortError'));
|
|
33
|
+
const timer = setTimeout(onTimeout, timeoutMs);
|
|
34
|
+
timer.unref?.();
|
|
35
|
+
if (shutdownSignal) {
|
|
36
|
+
if (shutdownSignal.aborted)
|
|
37
|
+
onShutdown();
|
|
38
|
+
else
|
|
39
|
+
shutdownSignal.addEventListener('abort', onShutdown, { once: true });
|
|
40
|
+
}
|
|
41
|
+
if (req.signal) {
|
|
42
|
+
if (req.signal.aborted)
|
|
43
|
+
onCaller();
|
|
44
|
+
else
|
|
45
|
+
req.signal.addEventListener('abort', onCaller, { once: true });
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const res = await globalThis.fetch(req.url, {
|
|
49
|
+
method: req.method,
|
|
50
|
+
headers,
|
|
51
|
+
body: req.body,
|
|
52
|
+
signal: controller.signal,
|
|
53
|
+
});
|
|
54
|
+
const body = await res.text();
|
|
55
|
+
const kept = {};
|
|
56
|
+
for (const h of KEEP_HEADERS) {
|
|
57
|
+
const v = res.headers.get(h);
|
|
58
|
+
if (v !== null)
|
|
59
|
+
kept[h] = v;
|
|
60
|
+
}
|
|
61
|
+
return { status: res.status, headers: kept, body, latencyMs: Math.round(performance.now() - t0) };
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
shutdownSignal?.removeEventListener('abort', onShutdown);
|
|
66
|
+
req.signal?.removeEventListener('abort', onCaller);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Parse a `Retry-After` header (seconds) to ms; undefined when absent or not a positive number. task-1368:
|
|
72
|
+
* Jupiter's keyless gateway sends NO Retry-After on a 429 (recorded 2026-10-12, fixtures/trade/jupiter-429-*.json)
|
|
73
|
+
* but does send `x-ratelimit-reset` (epoch seconds), so when Retry-After is absent the wait is derived from the
|
|
74
|
+
* reset time instead — the honest number for "retry in Ns" and for the retry policy.
|
|
75
|
+
*/
|
|
76
|
+
function retryAfterMs(res, now) {
|
|
77
|
+
const v = Number(res.headers['retry-after']);
|
|
78
|
+
if (Number.isFinite(v) && v > 0)
|
|
79
|
+
return v * 1000;
|
|
80
|
+
if (now !== undefined) {
|
|
81
|
+
const reset = Number(res.headers['x-ratelimit-reset']);
|
|
82
|
+
if (Number.isFinite(reset) && reset > 0) {
|
|
83
|
+
const until = reset * 1000 - now;
|
|
84
|
+
if (until > 0)
|
|
85
|
+
return Math.min(until, MAX_HEADER_PAUSE_MS);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
const MAX_HEADER_PAUSE_MS = 60_000;
|
|
91
|
+
/** Cap for the doubling pause after consecutive 429s on one bucket (matches the health backoff cap). */
|
|
92
|
+
const MAX_429_PAUSE_MS = 600_000;
|
|
93
|
+
export class HttpError extends Error {
|
|
94
|
+
status;
|
|
95
|
+
url;
|
|
96
|
+
/** Retry-After in ms when the server sent one (429/503); used by the RPC pool to pause an endpoint. */
|
|
97
|
+
retryAfterMs;
|
|
98
|
+
constructor(status, url, message, retryAfterMs) {
|
|
99
|
+
// URLs may carry an RPC api-key: never let one into a message that can reach a log or the screen.
|
|
100
|
+
super(redactSecrets(message ?? `HTTP ${status} for ${url}`));
|
|
101
|
+
this.name = 'HttpError';
|
|
102
|
+
this.status = status;
|
|
103
|
+
this.url = redactSecrets(url);
|
|
104
|
+
if (retryAfterMs !== undefined)
|
|
105
|
+
this.retryAfterMs = retryAfterMs;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export class SourceUnavailableError extends Error {
|
|
109
|
+
source;
|
|
110
|
+
constructor(source, state) {
|
|
111
|
+
super(`source ${source} is ${state}`);
|
|
112
|
+
this.name = 'SourceUnavailableError';
|
|
113
|
+
this.source = source;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Request priority context (task-1323). Code that knows how urgent its requests are wraps the call:
|
|
118
|
+
* `withPriority('high', () => dl.detail.candles(...))`; every request issued inside (through any number
|
|
119
|
+
* of awaits, caches and adapters) is queued at that priority in its rate bucket. Without a context a
|
|
120
|
+
* request is 'normal' — the pre-1323 behaviour. Passing a PriorityRef shares it, so a later raise()
|
|
121
|
+
* (a foreground caller coalescing onto a background load) re-prioritises requests already queued.
|
|
122
|
+
*/
|
|
123
|
+
const priorityContext = new AsyncLocalStorage();
|
|
124
|
+
export function withPriority(priority, fn) {
|
|
125
|
+
return priorityContext.run(typeof priority === 'string' ? new PriorityRef(priority) : priority, fn);
|
|
126
|
+
}
|
|
127
|
+
/** The caller's priority context, if any (undefined = normal). */
|
|
128
|
+
export function currentPriority() {
|
|
129
|
+
return priorityContext.getStore();
|
|
130
|
+
}
|
|
131
|
+
export class HttpClient {
|
|
132
|
+
opts;
|
|
133
|
+
/** The clock every wait in this client runs on (a FakeClock in tests); adapters pace their own retries by it. */
|
|
134
|
+
clock;
|
|
135
|
+
inflight = new Map();
|
|
136
|
+
/** Consecutive 429s per bucket, for the doubling pause. Reset by any non-429 response. */
|
|
137
|
+
strikes = new Map();
|
|
138
|
+
constructor(opts) {
|
|
139
|
+
this.opts = opts;
|
|
140
|
+
this.clock = opts.clock ?? systemClock;
|
|
141
|
+
}
|
|
142
|
+
/** GET JSON. Identical in-flight URLs share one request (SPEC §3.4 coalescing). */
|
|
143
|
+
getJson(url, meta, check, signal) {
|
|
144
|
+
return this.coalesce(`GET ${url}`, { method: 'GET', url, signal }, meta, check);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* POST a JSON body (JSON-RPC reads only). Identical in-flight url+body pairs share one request.
|
|
148
|
+
* `signal` (task-1368) bounds the wait like getJson's: a caller deadline cuts the limiter queue and the
|
|
149
|
+
* transport, and an abortable POST is never shared.
|
|
150
|
+
*/
|
|
151
|
+
postJson(url, body, meta, check, signal) {
|
|
152
|
+
const text = JSON.stringify(body);
|
|
153
|
+
return this.coalesce(`POST ${url} ${text}`, { method: 'POST', url, body: text, signal }, meta, check);
|
|
154
|
+
}
|
|
155
|
+
/** Ms until a DEGRADED source is probed again (0 when it is available or unknown). The health gate's own cool-down. */
|
|
156
|
+
unavailableFor(source) {
|
|
157
|
+
try {
|
|
158
|
+
const h = this.opts.health.get(source);
|
|
159
|
+
return h.state === 'degraded' ? Math.max(0, h.degradedUntil - this.clock.now()) : 0;
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** Remaining pause on a rate bucket in ms (0 when it is not paused or unknown). For honest "retry in Ns" messages. */
|
|
166
|
+
pausedFor(bucket) {
|
|
167
|
+
try {
|
|
168
|
+
const until = this.opts.limiter.bucket(bucket).stats().pausedUntil;
|
|
169
|
+
return until > 0 ? Math.max(0, until - this.clock.now()) : 0;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* GET a text document (task-1330: RSS/XML news feeds). Same gate, limiter, health accounting,
|
|
177
|
+
* coalescing and rate-limit header handling as getJson; the body is returned unparsed.
|
|
178
|
+
*/
|
|
179
|
+
getText(url, meta, accept = 'application/rss+xml, application/xml, text/xml;q=0.9, */*;q=0.5') {
|
|
180
|
+
return this.coalesce(`GET ${url}`, { method: 'GET', url, accept }, meta, undefined, true);
|
|
181
|
+
}
|
|
182
|
+
coalesce(key, req, meta, check, raw = false) {
|
|
183
|
+
const caller = currentPriority();
|
|
184
|
+
// An abortable request is never shared (task-1310): a caller that cancels (e.g. a bounded confirm
|
|
185
|
+
// quote) must not cancel an unrelated caller, and must not be ignored by joining a non-abortable one.
|
|
186
|
+
if (req.signal)
|
|
187
|
+
return this.doRequest(req, meta, check, new PriorityRef(caller?.level ?? 'normal'));
|
|
188
|
+
const existing = this.inflight.get(key);
|
|
189
|
+
if (existing) {
|
|
190
|
+
// A more urgent caller joining a queued request lifts it (task-1323): never inherit a lower priority.
|
|
191
|
+
if (caller)
|
|
192
|
+
existing.ref.raise(caller.level);
|
|
193
|
+
return existing.p;
|
|
194
|
+
}
|
|
195
|
+
// Each request gets its own ref seeded from the caller's level and following the caller's raises,
|
|
196
|
+
// so raising one coalesced request does not re-prioritise unrelated requests of the same caller.
|
|
197
|
+
const ref = new PriorityRef(caller?.level ?? 'normal');
|
|
198
|
+
const off = caller ? caller.on(() => ref.raise(caller.level)) : null;
|
|
199
|
+
const p = this.doRequest(req, meta, check, ref, raw).finally(() => { this.inflight.delete(key); off?.(); });
|
|
200
|
+
this.inflight.set(key, { p: p, ref });
|
|
201
|
+
return p;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* 429 -> pause (Retry-After, or the default doubling per consecutive 429 on the bucket);
|
|
205
|
+
* `x-ratelimit-remaining: 0` -> pause until reset (capped).
|
|
206
|
+
*/
|
|
207
|
+
respectRateLimitHeaders(res, meta, rateLimited = res.status === 429) {
|
|
208
|
+
const { limiter } = this.opts;
|
|
209
|
+
if (rateLimited) {
|
|
210
|
+
const n = (this.strikes.get(meta.bucket) ?? 0) + 1;
|
|
211
|
+
this.strikes.set(meta.bucket, n);
|
|
212
|
+
const base = this.opts.rateLimitPauseMs ?? 30_000;
|
|
213
|
+
const backoff = Math.min(base * 2 ** (n - 1), MAX_429_PAUSE_MS);
|
|
214
|
+
// The server's own wait (Retry-After, else its x-ratelimit-reset — task-1368) governs the first strike;
|
|
215
|
+
// a repeated 429 on the same bucket never pauses less than the doubling backoff.
|
|
216
|
+
const stated = retryAfterMs(res, this.clock.now());
|
|
217
|
+
limiter.pause(meta.bucket, stated !== undefined ? Math.max(stated, n > 1 ? backoff : 0) : backoff);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
this.strikes.delete(meta.bucket);
|
|
221
|
+
}
|
|
222
|
+
if (res.headers['x-ratelimit-remaining'] === '0') {
|
|
223
|
+
const reset = Number(res.headers['x-ratelimit-reset']); // epoch seconds (Jupiter)
|
|
224
|
+
const until = Number.isFinite(reset) && reset > 0 ? reset * 1000 - this.clock.now() : 10_000;
|
|
225
|
+
limiter.pause(meta.bucket, Math.min(Math.max(until, 1_000), MAX_HEADER_PAUSE_MS));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async doRequest(req, meta, check, priority, raw = false) {
|
|
229
|
+
const { health, limiter, transport } = this.opts;
|
|
230
|
+
const url = req.url;
|
|
231
|
+
if (!health.isAvailable(meta.source))
|
|
232
|
+
throw new SourceUnavailableError(meta.source, health.state(meta.source));
|
|
233
|
+
// Pass the caller's abort to the limiter so a bounded wait is cut even while queued behind a
|
|
234
|
+
// 429 pause (task-1310): the confirm-time quote can never hang past its deadline in the bucket.
|
|
235
|
+
await limiter.acquire(meta.bucket, req.signal, priority);
|
|
236
|
+
let res;
|
|
237
|
+
try {
|
|
238
|
+
res = await transport(req);
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
const msg = redactSecrets(err instanceof Error ? err.message : String(err));
|
|
242
|
+
health.record(meta.source, { ok: false, error: msg });
|
|
243
|
+
this.opts.onRequest?.(req, meta, { status: null, latencyMs: 0 });
|
|
244
|
+
throw err;
|
|
245
|
+
}
|
|
246
|
+
this.opts.onRequest?.(req, meta, { status: res.status, latencyMs: res.latencyMs });
|
|
247
|
+
if (res.status < 200 || res.status >= 300) {
|
|
248
|
+
this.respectRateLimitHeaders(res, meta);
|
|
249
|
+
health.record(meta.source, { ok: false, status: res.status, latencyMs: res.latencyMs });
|
|
250
|
+
throw new HttpError(res.status, url, undefined, retryAfterMs(res, this.clock.now()));
|
|
251
|
+
}
|
|
252
|
+
if (raw) {
|
|
253
|
+
// Text document (RSS/XML): no JSON parse, no body check; headers still drive the limiter.
|
|
254
|
+
this.respectRateLimitHeaders(res, meta);
|
|
255
|
+
health.record(meta.source, { ok: true, status: res.status, latencyMs: res.latencyMs });
|
|
256
|
+
return { data: res.body, status: res.status, latencyMs: res.latencyMs, fetchedAt: this.clock.now() };
|
|
257
|
+
}
|
|
258
|
+
let data;
|
|
259
|
+
try {
|
|
260
|
+
data = JSON.parse(res.body);
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
this.respectRateLimitHeaders(res, meta);
|
|
264
|
+
health.record(meta.source, { ok: false, status: res.status, latencyMs: res.latencyMs, error: 'invalid JSON' });
|
|
265
|
+
throw new HttpError(res.status, url, `invalid JSON from ${url}`);
|
|
266
|
+
}
|
|
267
|
+
const bad = check?.(data);
|
|
268
|
+
this.respectRateLimitHeaders(res, meta, bad?.status === 429);
|
|
269
|
+
if (bad) {
|
|
270
|
+
const status = bad.status ?? res.status;
|
|
271
|
+
health.record(meta.source, { ok: false, status, latencyMs: res.latencyMs, error: bad.error });
|
|
272
|
+
throw new HttpError(status, url, `${bad.error} (${url})`, retryAfterMs(res, this.clock.now()));
|
|
273
|
+
}
|
|
274
|
+
health.record(meta.source, { ok: true, status: res.status, latencyMs: res.latencyMs });
|
|
275
|
+
return { data, status: res.status, latencyMs: res.latencyMs, fetchedAt: this.clock.now() };
|
|
276
|
+
}
|
|
277
|
+
}
|