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,133 @@
|
|
|
1
|
+
// @dexterm/data — analyst UI-action channel (task-1313 part C).
|
|
2
|
+
//
|
|
3
|
+
// The analyst (a local `claude -p` run) can drive non-financial app navigation through a small,
|
|
4
|
+
// strictly-validated action set. The MCP server (the only thing claude can call) validates an action
|
|
5
|
+
// against the screen context it was given and, on success, appends one line to a local, token-
|
|
6
|
+
// authenticated channel file. The running TUI tails that file and RE-validates every action against
|
|
7
|
+
// its *current* state before applying it, then shows "analyst did X".
|
|
8
|
+
//
|
|
9
|
+
// Trust model: all token-derived text (symbols, names, descriptions) is untrusted. An injected
|
|
10
|
+
// instruction that convinces the model to call `open_token EVILMINT` cannot act, because a mint is
|
|
11
|
+
// only accepted when it is already in the caller-supplied allow-list (what is on screen / watched).
|
|
12
|
+
// Nothing here can sign, send, size, or confirm a trade: `open_ticket` only *opens* the PAPER ticket.
|
|
13
|
+
//
|
|
14
|
+
// This module is pure and zero-dependency so both the MCP server and the CLI import the same
|
|
15
|
+
// validator and the same channel format.
|
|
16
|
+
export const BASE58_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
17
|
+
/** Env var the MCP server reads for the channel file path; set by the CLI when it spawns claude. */
|
|
18
|
+
export const ACTION_FILE_ENV = 'DEXTERM_ACTION_FILE';
|
|
19
|
+
/** Env var carrying the per-run random token that authenticates channel lines. */
|
|
20
|
+
export const ACTION_TOKEN_ENV = 'DEXTERM_ACTION_TOKEN';
|
|
21
|
+
/** The five non-financial UI actions the analyst may request. */
|
|
22
|
+
export const ANALYST_ACTION_TYPES = ['open_token', 'show_in_screener', 'add_to_watchlist', 'switch_feed', 'open_ticket'];
|
|
23
|
+
/** Max actions the consumer should apply per rolling window, and the window length. Advisory shared constants. */
|
|
24
|
+
export const ACTION_RATE = { max: 6, windowMs: 10_000 };
|
|
25
|
+
const FILTER_MAX = 120;
|
|
26
|
+
// A conservative charset for a screener filter string; the consumer re-parses it with the real grammar.
|
|
27
|
+
// `!` is included so tag negation (`!flag:mint`, `!launchpad:pump`) — a documented part of the grammar —
|
|
28
|
+
// survives the boundary instead of being rejected as malformed (task-1313 part E).
|
|
29
|
+
const FILTER_SAFE_RE = /^[\w\s<>=:.%$,+!\-|()]*$/;
|
|
30
|
+
function isObj(x) {
|
|
31
|
+
return typeof x === 'object' && x !== null && !Array.isArray(x);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Validate one requested action against `allow`. Strict: unknown types/fields, out-of-allow-list
|
|
35
|
+
* mints/feeds/sorts, and malformed values are all rejected. Pure — no I/O, no state.
|
|
36
|
+
*/
|
|
37
|
+
export function validateAnalystAction(raw, allow) {
|
|
38
|
+
if (!isObj(raw))
|
|
39
|
+
return { ok: false, error: 'action must be an object' };
|
|
40
|
+
const type = raw.type;
|
|
41
|
+
if (typeof type !== 'string' || !ANALYST_ACTION_TYPES.includes(type)) {
|
|
42
|
+
return { ok: false, error: `unknown action type ${JSON.stringify(type)}` };
|
|
43
|
+
}
|
|
44
|
+
if (allow.types && !allow.types.includes(type)) {
|
|
45
|
+
return { ok: false, error: `${type}: not allowed in this session (allowed: ${allow.types.join(', ') || 'none — observe-only'})` };
|
|
46
|
+
}
|
|
47
|
+
const mintOk = (m) => typeof m === 'string' && BASE58_RE.test(m) && allow.mints.includes(m);
|
|
48
|
+
const short = (m) => m.slice(0, 4) + '…' + m.slice(-4);
|
|
49
|
+
switch (type) {
|
|
50
|
+
case 'open_token': {
|
|
51
|
+
if (!mintOk(raw.mint))
|
|
52
|
+
return { ok: false, error: 'open_token: mint not among the tokens currently on screen' };
|
|
53
|
+
return { ok: true, action: { type, mint: raw.mint }, label: `opened ${short(raw.mint)}` };
|
|
54
|
+
}
|
|
55
|
+
case 'add_to_watchlist': {
|
|
56
|
+
if (!mintOk(raw.mint))
|
|
57
|
+
return { ok: false, error: 'add_to_watchlist: mint not among the tokens currently on screen' };
|
|
58
|
+
return { ok: true, action: { type, mint: raw.mint }, label: `watchlisted ${short(raw.mint)}` };
|
|
59
|
+
}
|
|
60
|
+
case 'open_ticket': {
|
|
61
|
+
if (!mintOk(raw.mint))
|
|
62
|
+
return { ok: false, error: 'open_ticket: mint not among the tokens currently on screen' };
|
|
63
|
+
if (raw.side !== 'buy' && raw.side !== 'sell')
|
|
64
|
+
return { ok: false, error: 'open_ticket: side must be "buy" or "sell"' };
|
|
65
|
+
return { ok: true, action: { type, mint: raw.mint, side: raw.side }, label: `opened a PAPER ${raw.side} ticket for ${short(raw.mint)}` };
|
|
66
|
+
}
|
|
67
|
+
case 'switch_feed': {
|
|
68
|
+
if (typeof raw.feed !== 'string' || !allow.feeds.includes(raw.feed)) {
|
|
69
|
+
return { ok: false, error: `switch_feed: feed must be one of ${allow.feeds.join(', ') || '(none)'}` };
|
|
70
|
+
}
|
|
71
|
+
return { ok: true, action: { type, feed: raw.feed }, label: `switched the screener to "${raw.feed}"` };
|
|
72
|
+
}
|
|
73
|
+
case 'show_in_screener': {
|
|
74
|
+
const out = { type };
|
|
75
|
+
const bits = [];
|
|
76
|
+
if (raw.filter !== undefined && raw.filter !== null && raw.filter !== '') {
|
|
77
|
+
if (typeof raw.filter !== 'string' || raw.filter.length > FILTER_MAX || !FILTER_SAFE_RE.test(raw.filter)) {
|
|
78
|
+
return { ok: false, error: 'show_in_screener: filter is malformed or too long' };
|
|
79
|
+
}
|
|
80
|
+
out.filter = raw.filter.trim();
|
|
81
|
+
bits.push(`filter ${out.filter}`);
|
|
82
|
+
}
|
|
83
|
+
if (raw.sort !== undefined && raw.sort !== null) {
|
|
84
|
+
if (!isObj(raw.sort) || typeof raw.sort.key !== 'string' || !allow.sortKeys.includes(raw.sort.key)) {
|
|
85
|
+
return { ok: false, error: `show_in_screener: sort.key must be one of ${allow.sortKeys.join(', ')}` };
|
|
86
|
+
}
|
|
87
|
+
const dir = raw.sort.dir === 'asc' ? 'asc' : 'desc';
|
|
88
|
+
out.sort = { key: raw.sort.key, dir };
|
|
89
|
+
bits.push(`sort ${out.sort.key} ${dir}`);
|
|
90
|
+
}
|
|
91
|
+
if (raw.highlight !== undefined && raw.highlight !== null && raw.highlight !== '') {
|
|
92
|
+
if (!mintOk(raw.highlight))
|
|
93
|
+
return { ok: false, error: 'show_in_screener: highlight mint not among the tokens currently on screen' };
|
|
94
|
+
out.highlight = raw.highlight;
|
|
95
|
+
bits.push(`highlight ${short(raw.highlight)}`);
|
|
96
|
+
}
|
|
97
|
+
if (!bits.length)
|
|
98
|
+
return { ok: false, error: 'show_in_screener: nothing to do (give filter, sort, or highlight)' };
|
|
99
|
+
return { ok: true, action: out, label: `showed the screener (${bits.join(', ')})` };
|
|
100
|
+
}
|
|
101
|
+
default:
|
|
102
|
+
return { ok: false, error: `unknown action type ${type}` };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Serialize a channel line (newline-terminated). */
|
|
106
|
+
export function serializeActionLine(token, seq, action) {
|
|
107
|
+
const line = { v: 1, token, seq, ts: Date.now(), action };
|
|
108
|
+
return JSON.stringify(line) + '\n';
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Parse and authenticate one channel line. Returns the (still unvalidated) requested action only when
|
|
112
|
+
* the line is well-formed and its token matches `expectedToken`; otherwise null. Validation against an
|
|
113
|
+
* allow-list is a separate step (`validateAnalystAction`) the consumer must still run.
|
|
114
|
+
*/
|
|
115
|
+
export function parseActionLine(line, expectedToken) {
|
|
116
|
+
const s = line.trim();
|
|
117
|
+
if (!s)
|
|
118
|
+
return null;
|
|
119
|
+
let msg;
|
|
120
|
+
try {
|
|
121
|
+
msg = JSON.parse(s);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
if (!isObj(msg) || msg.v !== 1)
|
|
127
|
+
return null;
|
|
128
|
+
if (typeof msg.token !== 'string' || msg.token !== expectedToken)
|
|
129
|
+
return null;
|
|
130
|
+
if (typeof msg.seq !== 'number' || !isObj(msg.action))
|
|
131
|
+
return null;
|
|
132
|
+
return { seq: msg.seq, action: msg.action };
|
|
133
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Per-source request budget meter (task-1309). Every free data source gives each user their own
|
|
2
|
+
// per-IP quota (that is why a local client scales: there is no shared server quota). This meter
|
|
3
|
+
// counts the requests dexterm sends to each source — this session and today (persisted in the SQLite
|
|
4
|
+
// cache, so restarts keep counting) — and exposes a snapshot + header label so the budget is visible.
|
|
5
|
+
// Optional soft daily caps let a cautious user bound a source; passing no cap means "unlimited, just
|
|
6
|
+
// count" (the limiter already paces to the documented rpm). The meter never sees or stores any key.
|
|
7
|
+
import { systemClock } from "./clock.js";
|
|
8
|
+
const NS = 'meter';
|
|
9
|
+
const DAY_TTL_MS = 2 * 86_400_000;
|
|
10
|
+
export const BUDGET_WARN_PCT = 0.8;
|
|
11
|
+
/** Counts requests per source for a visible budget meter; soft daily caps are advisory, not enforced here. */
|
|
12
|
+
export class BudgetMeter {
|
|
13
|
+
clock;
|
|
14
|
+
cache;
|
|
15
|
+
dayCaps;
|
|
16
|
+
warnPct;
|
|
17
|
+
session = new Map();
|
|
18
|
+
dayTotals = new Map();
|
|
19
|
+
dayKey = '';
|
|
20
|
+
constructor(opts = {}) {
|
|
21
|
+
this.clock = opts.clock ?? systemClock;
|
|
22
|
+
this.cache = opts.cache;
|
|
23
|
+
this.dayCaps = { ...opts.dayCaps };
|
|
24
|
+
this.warnPct = opts.warnPct ?? BUDGET_WARN_PCT;
|
|
25
|
+
this.roll();
|
|
26
|
+
}
|
|
27
|
+
static dayKeyFor(ms) {
|
|
28
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
29
|
+
}
|
|
30
|
+
roll() {
|
|
31
|
+
const key = BudgetMeter.dayKeyFor(this.clock.now());
|
|
32
|
+
if (key === this.dayKey)
|
|
33
|
+
return;
|
|
34
|
+
this.dayKey = key;
|
|
35
|
+
this.dayTotals.clear();
|
|
36
|
+
}
|
|
37
|
+
dayTotal(source) {
|
|
38
|
+
const cached = this.dayTotals.get(source);
|
|
39
|
+
if (cached !== undefined)
|
|
40
|
+
return cached;
|
|
41
|
+
const hit = this.cache?.get(NS, `req:${this.dayKey}:${source}`);
|
|
42
|
+
const n = hit && typeof hit.value === 'number' ? hit.value : 0;
|
|
43
|
+
this.dayTotals.set(source, n);
|
|
44
|
+
return n;
|
|
45
|
+
}
|
|
46
|
+
/** Record one request to `source`. */
|
|
47
|
+
charge(source) {
|
|
48
|
+
this.roll();
|
|
49
|
+
this.session.set(source, (this.session.get(source) ?? 0) + 1);
|
|
50
|
+
const day = this.dayTotal(source) + 1;
|
|
51
|
+
this.dayTotals.set(source, day);
|
|
52
|
+
this.cache?.set(NS, `req:${this.dayKey}:${source}`, day, { ttlMs: DAY_TTL_MS, source });
|
|
53
|
+
}
|
|
54
|
+
/** True while another request to `source` is within its soft cap (always true when uncapped). */
|
|
55
|
+
allow(source) {
|
|
56
|
+
this.roll();
|
|
57
|
+
const cap = this.dayCaps[source];
|
|
58
|
+
if (cap === undefined)
|
|
59
|
+
return true;
|
|
60
|
+
return this.dayTotal(source) < cap;
|
|
61
|
+
}
|
|
62
|
+
status(source) {
|
|
63
|
+
this.roll();
|
|
64
|
+
const cap = this.dayCaps[source] ?? null;
|
|
65
|
+
const day = this.dayTotal(source);
|
|
66
|
+
return {
|
|
67
|
+
source,
|
|
68
|
+
session: this.session.get(source) ?? 0,
|
|
69
|
+
day,
|
|
70
|
+
dayCap: cap,
|
|
71
|
+
dayPct: cap && cap > 0 ? day / cap : null,
|
|
72
|
+
over: cap !== null && day >= cap,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Every source seen this run (plus any with a cap), busiest first. */
|
|
76
|
+
snapshot() {
|
|
77
|
+
this.roll();
|
|
78
|
+
const sources = new Set([...this.session.keys(), ...Object.keys(this.dayCaps)]);
|
|
79
|
+
return [...sources].map((s) => this.status(s)).sort((a, b) => b.session - a.session);
|
|
80
|
+
}
|
|
81
|
+
/** Header-sized summary of the busiest sources, e.g. `req: dexscreener 42 · jupiter 11`. */
|
|
82
|
+
label(max = 3) {
|
|
83
|
+
const top = this.snapshot().filter((s) => s.session > 0).slice(0, max);
|
|
84
|
+
if (top.length === 0)
|
|
85
|
+
return 'req: 0';
|
|
86
|
+
return `req: ${top.map((s) => `${s.source} ${s.session}${s.dayCap ? `/${s.dayCap}` : ''}`).join(' · ')}`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// SQLite cache (SPEC §3.4) on Node's built-in `node:sqlite`.
|
|
2
|
+
//
|
|
3
|
+
// Why node:sqlite over better-sqlite3: it ships with Node >= 22.5 (verified on
|
|
4
|
+
// 24.20), so there is no native addon to compile per platform/ABI, which keeps
|
|
5
|
+
// `npx dexterm` and the Homebrew formula dependency-free. The API is synchronous
|
|
6
|
+
// like better-sqlite3, which suits a cache. Trade-offs: it is still flagged
|
|
7
|
+
// experimental in Node 24 and `bun build --compile` binaries would need a
|
|
8
|
+
// bun:sqlite shim behind this same class (D-22).
|
|
9
|
+
//
|
|
10
|
+
// One table, namespaced: (ns, key) -> json, fetched_at, ttl_ms, source.
|
|
11
|
+
// fetch() layers in-flight coalescing and stale-while-revalidate on top.
|
|
12
|
+
import { mkdirSync } from 'node:fs';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
16
|
+
import { systemClock } from "./clock.js";
|
|
17
|
+
/** TTLs from SPEC §3.4 (ms). `lists` (boosts/trending/recent feeds) is ours: they refresh at 15-30 s. */
|
|
18
|
+
export const TTL = {
|
|
19
|
+
pairs: 10_000,
|
|
20
|
+
enrichment: 60_000,
|
|
21
|
+
price: 30_000,
|
|
22
|
+
candlesOpen: 30_000,
|
|
23
|
+
candlesClosed: 3_600_000,
|
|
24
|
+
holders: 60_000,
|
|
25
|
+
audit: 600_000,
|
|
26
|
+
rugcheck: 600_000,
|
|
27
|
+
socials: 3_600_000,
|
|
28
|
+
devHistory: 86_400_000,
|
|
29
|
+
lists: 15_000,
|
|
30
|
+
};
|
|
31
|
+
/** XDG data path for the cache DB (SPEC §2.2). Pure: does not create anything. */
|
|
32
|
+
export function defaultDbPath(env = process.env) {
|
|
33
|
+
if (process.platform === 'win32' && env.APPDATA)
|
|
34
|
+
return join(env.APPDATA, 'dexterm', 'dexterm.db');
|
|
35
|
+
const base = env.XDG_DATA_HOME || join(homedir(), '.local', 'share');
|
|
36
|
+
return join(base, 'dexterm', 'dexterm.db');
|
|
37
|
+
}
|
|
38
|
+
export class Cache {
|
|
39
|
+
db;
|
|
40
|
+
clock;
|
|
41
|
+
inflight = new Map();
|
|
42
|
+
constructor(opts = {}) {
|
|
43
|
+
const path = opts.path ?? ':memory:';
|
|
44
|
+
if (path !== ':memory:')
|
|
45
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
46
|
+
this.db = new DatabaseSync(path);
|
|
47
|
+
this.clock = opts.clock ?? systemClock;
|
|
48
|
+
this.db.exec(`
|
|
49
|
+
PRAGMA journal_mode = WAL;
|
|
50
|
+
PRAGMA busy_timeout = 3000;
|
|
51
|
+
CREATE TABLE IF NOT EXISTS cache (
|
|
52
|
+
ns TEXT NOT NULL,
|
|
53
|
+
key TEXT NOT NULL,
|
|
54
|
+
json TEXT NOT NULL,
|
|
55
|
+
fetched_at INTEGER NOT NULL,
|
|
56
|
+
ttl_ms INTEGER NOT NULL,
|
|
57
|
+
source TEXT,
|
|
58
|
+
PRIMARY KEY (ns, key)
|
|
59
|
+
) WITHOUT ROWID;
|
|
60
|
+
`);
|
|
61
|
+
}
|
|
62
|
+
get(ns, key) {
|
|
63
|
+
const row = this.db
|
|
64
|
+
.prepare('SELECT json, fetched_at, ttl_ms, source FROM cache WHERE ns = ? AND key = ?')
|
|
65
|
+
.get(ns, key);
|
|
66
|
+
if (!row)
|
|
67
|
+
return undefined;
|
|
68
|
+
const ageMs = this.clock.now() - row.fetched_at;
|
|
69
|
+
return {
|
|
70
|
+
value: JSON.parse(row.json),
|
|
71
|
+
fetchedAt: row.fetched_at,
|
|
72
|
+
ttlMs: row.ttl_ms,
|
|
73
|
+
source: row.source,
|
|
74
|
+
ageMs,
|
|
75
|
+
stale: ageMs >= row.ttl_ms,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
set(ns, key, value, opts) {
|
|
79
|
+
this.db
|
|
80
|
+
.prepare('INSERT OR REPLACE INTO cache (ns, key, json, fetched_at, ttl_ms, source) VALUES (?, ?, ?, ?, ?, ?)')
|
|
81
|
+
.run(ns, key, JSON.stringify(value), opts.fetchedAt ?? this.clock.now(), opts.ttlMs, opts.source ?? null);
|
|
82
|
+
}
|
|
83
|
+
delete(ns, key) {
|
|
84
|
+
this.db.prepare('DELETE FROM cache WHERE ns = ? AND key = ?').run(ns, key);
|
|
85
|
+
}
|
|
86
|
+
/** Drop entries older than ttl + `graceMs` (stale data is still useful for degraded mode). */
|
|
87
|
+
prune(graceMs = 86_400_000) {
|
|
88
|
+
const res = this.db.prepare('DELETE FROM cache WHERE fetched_at + ttl_ms + ? < ?').run(graceMs, this.clock.now());
|
|
89
|
+
return Number(res.changes);
|
|
90
|
+
}
|
|
91
|
+
count(ns) {
|
|
92
|
+
const row = (ns === undefined
|
|
93
|
+
? this.db.prepare('SELECT COUNT(*) AS n FROM cache').get()
|
|
94
|
+
: this.db.prepare('SELECT COUNT(*) AS n FROM cache WHERE ns = ?').get(ns));
|
|
95
|
+
return Number(row.n);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Cached load with coalescing (identical concurrent loads share one promise)
|
|
99
|
+
* and stale-while-revalidate. On loader failure a stale value is served with
|
|
100
|
+
* `error` set; with nothing cached the error propagates.
|
|
101
|
+
*/
|
|
102
|
+
async fetch(ns, key, loader, opts) {
|
|
103
|
+
const hit = this.get(ns, key);
|
|
104
|
+
if (hit && !hit.stale) {
|
|
105
|
+
return { value: hit.value, fetchedAt: hit.fetchedAt, source: hit.source, fromCache: true, stale: false };
|
|
106
|
+
}
|
|
107
|
+
if (hit && (opts.swr ?? true)) {
|
|
108
|
+
const refreshing = this.load(ns, key, loader, opts).then(() => undefined, () => undefined);
|
|
109
|
+
return { value: hit.value, fetchedAt: hit.fetchedAt, source: hit.source, fromCache: true, stale: true, refreshing };
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const value = await this.load(ns, key, loader, opts);
|
|
113
|
+
return { value, fetchedAt: this.clock.now(), source: opts.source ?? null, fromCache: false, stale: false };
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
if (!hit)
|
|
117
|
+
throw err;
|
|
118
|
+
return { value: hit.value, fetchedAt: hit.fetchedAt, source: hit.source, fromCache: true, stale: true, error: err };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
close() {
|
|
122
|
+
this.db.close();
|
|
123
|
+
}
|
|
124
|
+
load(ns, key, loader, opts) {
|
|
125
|
+
const id = `${ns}\u0000${key}`;
|
|
126
|
+
const existing = this.inflight.get(id);
|
|
127
|
+
if (existing)
|
|
128
|
+
return existing;
|
|
129
|
+
const p = (async () => {
|
|
130
|
+
try {
|
|
131
|
+
const value = await loader();
|
|
132
|
+
this.set(ns, key, value, { ttlMs: opts.ttlMs, source: opts.source });
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
this.inflight.delete(id);
|
|
137
|
+
}
|
|
138
|
+
})();
|
|
139
|
+
this.inflight.set(id, p);
|
|
140
|
+
return p;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Candle store (SPEC §3.4): GeckoTerminal OHLCV with closed-bar caching.
|
|
2
|
+
//
|
|
3
|
+
// Two cache rows per (pool, interval, token):
|
|
4
|
+
// candles:closed bars whose period has ended. They do not change, so they are
|
|
5
|
+
// kept for TTL.candlesClosed (1 h) and only re-verified by a full
|
|
6
|
+
// fetch after that.
|
|
7
|
+
// candles:view the last served series (closed + the open bar), TTL.candlesOpen
|
|
8
|
+
// (30 s). Within that window a request costs nothing.
|
|
9
|
+
// When the view expires, a refresh asks GeckoTerminal only for the bars missing
|
|
10
|
+
// since the last closed bar plus the open one (usually limit=2), merges them into
|
|
11
|
+
// the closed set and re-splits on the clock. Every request goes through the
|
|
12
|
+
// HttpClient, so the geckoterminal bucket (10 rpm x 0.7) is never exceeded and
|
|
13
|
+
// identical concurrent loads share one request.
|
|
14
|
+
import { TTL } from "./cache.js";
|
|
15
|
+
import { CANDLE_TIMEFRAMES, GT_MAX_LIMIT } from "./sources/geckoterminal.js";
|
|
16
|
+
import { currentPriority, withPriority } from "./http.js";
|
|
17
|
+
import { PriorityRef } from "./limiter.js";
|
|
18
|
+
const NS_CLOSED = 'candles:closed';
|
|
19
|
+
const NS_VIEW = 'candles:view';
|
|
20
|
+
/** Closed bars kept per series (GeckoTerminal's max page). */
|
|
21
|
+
export const MAX_KEPT_BARS = GT_MAX_LIMIT;
|
|
22
|
+
/** Default bars per series: enough for the widest chart (SPEC §5.2 caps the AI tool at 300). */
|
|
23
|
+
export const DEFAULT_CANDLE_LIMIT = 300;
|
|
24
|
+
/**
|
|
25
|
+
* task-1314: the coarser timeframes derive exactly from a finer base, so two network fetches (1m, 1h)
|
|
26
|
+
* cover all six tabs and a timeframe switch paints instantly from local aggregation. Each target period
|
|
27
|
+
* is an integer multiple of its base and every dexterm period divides the epoch evenly (minute/hour/day
|
|
28
|
+
* all start at a UTC boundary = floor(t/period)*period), so the derived buckets line up with the bars
|
|
29
|
+
* GeckoTerminal would return natively. 1m and 1h are the base intervals (not derivable).
|
|
30
|
+
*/
|
|
31
|
+
export const CANDLE_BASE = {
|
|
32
|
+
'5m': '1m',
|
|
33
|
+
'15m': '1m',
|
|
34
|
+
'4h': '1h',
|
|
35
|
+
'1d': '1h',
|
|
36
|
+
};
|
|
37
|
+
/** Base intervals that must be fetched from the network; every other tab derives from one of these. */
|
|
38
|
+
export const CANDLE_BASE_INTERVALS = ['1m', '1h'];
|
|
39
|
+
/**
|
|
40
|
+
* Aggregate oldest-first base candles into a coarser interval by exact period multiple. Returns `[]`
|
|
41
|
+
* when `toPeriodMs` is not a positive integer multiple of `fromPeriodMs` (the caller then fetches
|
|
42
|
+
* natively). Buckets align to `floor(t/toPeriodMs)*toPeriodMs`; gaps (GeckoTerminal omits empty bars)
|
|
43
|
+
* stay gaps. open = first sub-bar's open, close = last sub-bar's close, high/low = extremes, v = sum.
|
|
44
|
+
*/
|
|
45
|
+
export function aggregateCandles(base, fromPeriodMs, toPeriodMs) {
|
|
46
|
+
if (fromPeriodMs <= 0 || toPeriodMs <= fromPeriodMs || toPeriodMs % fromPeriodMs !== 0)
|
|
47
|
+
return [];
|
|
48
|
+
if (base.length === 0)
|
|
49
|
+
return [];
|
|
50
|
+
const sorted = base.every((b, i) => i === 0 || b.t >= base[i - 1].t) ? base : [...base].sort((a, b) => a.t - b.t);
|
|
51
|
+
const buckets = new Map();
|
|
52
|
+
for (const b of sorted) {
|
|
53
|
+
const t = Math.floor(b.t / toPeriodMs) * toPeriodMs;
|
|
54
|
+
const cur = buckets.get(t);
|
|
55
|
+
if (!cur) {
|
|
56
|
+
buckets.set(t, { t, o: b.o, h: b.h, l: b.l, c: b.c, v: b.v });
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
cur.h = Math.max(cur.h, b.h);
|
|
60
|
+
cur.l = Math.min(cur.l, b.l);
|
|
61
|
+
cur.c = b.c; // sorted oldest-first: the last write is the latest close
|
|
62
|
+
cur.v += b.v;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return [...buckets.values()].sort((a, b) => a.t - b.t);
|
|
66
|
+
}
|
|
67
|
+
export class CandleStore {
|
|
68
|
+
cache;
|
|
69
|
+
gecko;
|
|
70
|
+
clock;
|
|
71
|
+
inflight = new Map();
|
|
72
|
+
constructor(deps) {
|
|
73
|
+
this.cache = deps.cache;
|
|
74
|
+
this.gecko = deps.gecko;
|
|
75
|
+
this.clock = deps.clock;
|
|
76
|
+
}
|
|
77
|
+
async get(pool, interval, req = {}) {
|
|
78
|
+
const limit = Math.max(1, Math.min(MAX_KEPT_BARS, Math.floor(req.limit ?? DEFAULT_CANDLE_LIMIT)));
|
|
79
|
+
const key = `${pool}:${interval}:${req.token ?? ''}`;
|
|
80
|
+
const hit = this.cache.get(NS_VIEW, key);
|
|
81
|
+
const usable = hit && hit.value.limit >= limit ? hit : undefined;
|
|
82
|
+
const series = (v, fetchedAt, extra) => ({
|
|
83
|
+
pool,
|
|
84
|
+
interval,
|
|
85
|
+
candles: v.candles.slice(-limit),
|
|
86
|
+
openBarT: v.openBarT,
|
|
87
|
+
source: 'geckoterminal',
|
|
88
|
+
fetchedAt,
|
|
89
|
+
fromCache: false,
|
|
90
|
+
stale: false,
|
|
91
|
+
...extra,
|
|
92
|
+
});
|
|
93
|
+
if (usable && !usable.stale)
|
|
94
|
+
return series(usable.value, usable.fetchedAt, { fromCache: true });
|
|
95
|
+
if (usable && (req.swr ?? true)) {
|
|
96
|
+
// The caller already has something to paint: the revalidation is background work (task-1323),
|
|
97
|
+
// so it never takes the bucket's foreground reserve away from a chart that is actually empty.
|
|
98
|
+
const refreshing = withPriority('low', () => this.load(key, pool, interval, limit, req.token)).then(() => undefined, () => undefined);
|
|
99
|
+
return series(usable.value, usable.fetchedAt, { fromCache: true, stale: true, refreshing });
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const v = await this.load(key, pool, interval, limit, req.token);
|
|
103
|
+
return series(v, this.clock.now(), {});
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
if (!usable)
|
|
107
|
+
throw err;
|
|
108
|
+
return series(usable.value, usable.fetchedAt, { fromCache: true, stale: true, error: err });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Coalesced refresh: identical concurrent loads share one request. Each load runs under its own
|
|
113
|
+
* priority ref (task-1323); a more urgent caller joining a queued background load raises it, so a
|
|
114
|
+
* prefetch already waiting in the GeckoTerminal bucket cannot hold a foreground chart back.
|
|
115
|
+
*/
|
|
116
|
+
load(key, pool, interval, limit, token) {
|
|
117
|
+
const caller = currentPriority();
|
|
118
|
+
const existing = this.inflight.get(key);
|
|
119
|
+
if (existing) {
|
|
120
|
+
if (caller)
|
|
121
|
+
existing.ref.raise(caller.level);
|
|
122
|
+
return existing.p;
|
|
123
|
+
}
|
|
124
|
+
const ref = new PriorityRef(caller?.level ?? 'normal');
|
|
125
|
+
const p = withPriority(ref, () => this.refresh(key, pool, interval, limit, token)).finally(() => this.inflight.delete(key));
|
|
126
|
+
this.inflight.set(key, { p, ref });
|
|
127
|
+
return p;
|
|
128
|
+
}
|
|
129
|
+
/** True when a usable series for (pool, interval, token) is cached (fresh or stale) at >= `limit` bars. */
|
|
130
|
+
has(pool, interval, req = {}) {
|
|
131
|
+
const limit = Math.max(1, Math.min(MAX_KEPT_BARS, Math.floor(req.limit ?? DEFAULT_CANDLE_LIMIT)));
|
|
132
|
+
const hit = this.cache.get(NS_VIEW, `${pool}:${interval}:${req.token ?? ''}`);
|
|
133
|
+
return !!hit && hit.value.limit >= limit && hit.value.candles.length > 0;
|
|
134
|
+
}
|
|
135
|
+
async refresh(key, pool, interval, limit, token) {
|
|
136
|
+
const { periodMs } = CANDLE_TIMEFRAMES[interval];
|
|
137
|
+
const closed = this.cache.get(NS_CLOSED, key);
|
|
138
|
+
const reuse = closed && !closed.stale && closed.value.length > 0 ? closed : undefined;
|
|
139
|
+
let fetchLimit = limit;
|
|
140
|
+
if (reuse) {
|
|
141
|
+
const last = reuse.value[reuse.value.length - 1].t;
|
|
142
|
+
// Bars that can have appeared since the last closed one, plus the open bar.
|
|
143
|
+
const missing = Math.ceil((this.clock.now() - last) / periodMs) + 1;
|
|
144
|
+
const have = reuse.value.length;
|
|
145
|
+
// Incremental only if the kept history already covers the requested depth.
|
|
146
|
+
if (have + missing >= limit || have >= MAX_KEPT_BARS)
|
|
147
|
+
fetchLimit = Math.min(limit, Math.max(2, missing));
|
|
148
|
+
}
|
|
149
|
+
const res = await this.gecko.ohlcv(pool, interval, { limit: fetchLimit, token });
|
|
150
|
+
const now = this.clock.now();
|
|
151
|
+
const merged = new Map();
|
|
152
|
+
if (reuse && fetchLimit < limit)
|
|
153
|
+
for (const b of reuse.value)
|
|
154
|
+
merged.set(b.t, b);
|
|
155
|
+
for (const b of res.candles)
|
|
156
|
+
merged.set(b.t, b);
|
|
157
|
+
const bars = [...merged.values()].sort((a, b) => a.t - b.t);
|
|
158
|
+
const closedBars = bars.filter((b) => b.t + periodMs <= now).slice(-MAX_KEPT_BARS);
|
|
159
|
+
const open = bars.find((b) => b.t + periodMs > now) ?? null;
|
|
160
|
+
// An incremental merge keeps the original fetchedAt, so closed bars are still
|
|
161
|
+
// fully re-verified once an hour (GeckoTerminal occasionally revises late trades).
|
|
162
|
+
const closedFetchedAt = reuse && fetchLimit < limit ? reuse.fetchedAt : now;
|
|
163
|
+
this.cache.set(NS_CLOSED, key, closedBars, { ttlMs: TTL.candlesClosed, source: 'geckoterminal', fetchedAt: closedFetchedAt });
|
|
164
|
+
const view = { candles: open ? [...closedBars, open] : closedBars, openBarT: open?.t ?? null, limit };
|
|
165
|
+
this.cache.set(NS_VIEW, key, view, { ttlMs: TTL.candlesOpen, source: 'geckoterminal' });
|
|
166
|
+
return view;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Injectable clock so the limiter, health registry and cache can be tested
|
|
2
|
+
// deterministically with a fake clock (no real timers in tests).
|
|
3
|
+
export const systemClock = {
|
|
4
|
+
now: () => Date.now(),
|
|
5
|
+
sleep: (ms, signal) => {
|
|
6
|
+
if (signal?.aborted)
|
|
7
|
+
return Promise.resolve();
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
const t = setTimeout(done, Math.max(0, ms));
|
|
10
|
+
function done() { signal?.removeEventListener('abort', done); clearTimeout(t); resolve(); }
|
|
11
|
+
signal?.addEventListener('abort', done, { once: true });
|
|
12
|
+
});
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
/** Manual clock: time only moves when the test calls advance()/runUntil(). */
|
|
16
|
+
export class FakeClock {
|
|
17
|
+
t;
|
|
18
|
+
seq = 0;
|
|
19
|
+
timers = [];
|
|
20
|
+
constructor(start = 1_700_000_000_000) {
|
|
21
|
+
this.t = start;
|
|
22
|
+
}
|
|
23
|
+
now() {
|
|
24
|
+
return this.t;
|
|
25
|
+
}
|
|
26
|
+
sleep(ms, signal) {
|
|
27
|
+
if (signal?.aborted)
|
|
28
|
+
return Promise.resolve();
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
const timer = { at: this.t + Math.max(0, ms), seq: this.seq++, resolve };
|
|
31
|
+
this.timers.push(timer);
|
|
32
|
+
// Cancellation resolves the wait at once and drops the pending timer, matching systemClock.
|
|
33
|
+
signal?.addEventListener('abort', () => {
|
|
34
|
+
const i = this.timers.indexOf(timer);
|
|
35
|
+
if (i >= 0)
|
|
36
|
+
this.timers.splice(i, 1);
|
|
37
|
+
resolve();
|
|
38
|
+
}, { once: true });
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
get pendingTimers() {
|
|
42
|
+
return this.timers.length;
|
|
43
|
+
}
|
|
44
|
+
/** Advance time by `ms`, firing due timers in order and letting their continuations run. */
|
|
45
|
+
async advance(ms) {
|
|
46
|
+
const target = this.t + ms;
|
|
47
|
+
for (;;) {
|
|
48
|
+
await flushMicrotasks();
|
|
49
|
+
this.timers.sort((a, b) => a.at - b.at || a.seq - b.seq);
|
|
50
|
+
const next = this.timers[0];
|
|
51
|
+
if (!next || next.at > target)
|
|
52
|
+
break;
|
|
53
|
+
this.timers.shift();
|
|
54
|
+
this.t = Math.max(this.t, next.at);
|
|
55
|
+
next.resolve();
|
|
56
|
+
}
|
|
57
|
+
this.t = target;
|
|
58
|
+
await flushMicrotasks();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Let every queued microtask (and their follow-ups) run before returning. */
|
|
62
|
+
export function flushMicrotasks() {
|
|
63
|
+
return new Promise((resolve) => setImmediate(resolve));
|
|
64
|
+
}
|