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,375 @@
|
|
|
1
|
+
// dexterm doctor (task-1305): a pre-flight the user runs before anything else. It answers one
|
|
2
|
+
// question — "will dexterm work on this machine, and if not, exactly what do I fix?" — as a plain
|
|
3
|
+
// pass/warn/fail list with a copy-pasteable fix on every line that is not a pass.
|
|
4
|
+
//
|
|
5
|
+
// Everything that touches the outside world (the claude binary, the network) is injected so the
|
|
6
|
+
// checks stay deterministic in tests and never hang: each probe carries its own timeout and any
|
|
7
|
+
// failure is turned into a `fail`/`warn` line, never an exception. The doctor NEVER prints the
|
|
8
|
+
// Helius key or any URL that could carry it — it reports the provider name only.
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { join, dirname } from 'node:path';
|
|
11
|
+
import { mkdtempSync, writeFileSync, rmSync, mkdirSync, existsSync } from 'node:fs';
|
|
12
|
+
import { spawnSync } from 'node:child_process';
|
|
13
|
+
import { shellBinaryPath } from '../../popup/src/index.js';
|
|
14
|
+
import { detectColorDepth, protocolFromEnv } from '../../tui/src/index.js';
|
|
15
|
+
import { resolveRpcUrl, defaultDbPath } from '../../data/src/index.js';
|
|
16
|
+
import { XTokenStore } from '../../data/src/index.js';
|
|
17
|
+
import { SayTts, voiceDoctor, resolveVoice, WHISPER_MODEL, resolvePiperBin, pickPlayer, PIPER_MODEL } from '../../voice/src/index.js';
|
|
18
|
+
import { loadConfig, dextermHome } from '../../custom/src/index.js';
|
|
19
|
+
import { kbStore, skillStore } from "./kb-skill-cmd.js";
|
|
20
|
+
/** The minimum Node that runs dexterm (type-stripping of the .ts entry needs 22.18). */
|
|
21
|
+
export const MIN_NODE = [22, 18, 0];
|
|
22
|
+
/** Cheap PATH lookup (no spawn) for the voice probe. */
|
|
23
|
+
function onPath(bin) {
|
|
24
|
+
for (const p of (process.env.PATH ?? '').split(':'))
|
|
25
|
+
if (p && existsSync(join(p, bin)))
|
|
26
|
+
return true;
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Default voice probe. Deliberately cheap and non-blocking: it never compiles the Swift helper (that
|
|
31
|
+
* is left to the first real `v` press), so resolvedStt is inferred from swiftc / whisper presence.
|
|
32
|
+
*/
|
|
33
|
+
export async function defaultVoiceProbe(platform = process.platform, configuredVoice) {
|
|
34
|
+
const say = new SayTts();
|
|
35
|
+
const sayAvailable = platform === 'darwin' ? await say.available() : false;
|
|
36
|
+
const voices = sayAvailable ? await say.listVoicesDetailed() : [];
|
|
37
|
+
const swiftcPresent = platform === 'darwin' && onPath('swiftc');
|
|
38
|
+
const whisperBinPresent = onPath('whisper-cli');
|
|
39
|
+
const whisperModelPresent = existsSync(WHISPER_MODEL);
|
|
40
|
+
const micToolPresent = onPath('sox') || onPath('rec') || onPath('ffmpeg') || onPath('arecord');
|
|
41
|
+
// Apple Speech is macOS-only; everywhere else whisper.cpp is the default voice-in (task-1393 Part 2).
|
|
42
|
+
const resolvedStt = swiftcPresent ? 'apple' : (whisperBinPresent && whisperModelPresent && micToolPresent) ? 'whisper' : null;
|
|
43
|
+
// espeak-ng is the baseline portable speech-out engine off macOS.
|
|
44
|
+
const espeakPresent = platform !== 'darwin' && (onPath('espeak-ng') || onPath('espeak'));
|
|
45
|
+
// Piper is the preferred neural speech-out engine off macOS (task-1408): binary + voice model + a player.
|
|
46
|
+
const piperPresent = platform !== 'darwin' && resolvePiperBin() !== null;
|
|
47
|
+
const piperVoicePresent = platform !== 'darwin' && existsSync(PIPER_MODEL);
|
|
48
|
+
const playerName = platform !== 'darwin' ? pickPlayer() : null;
|
|
49
|
+
// task-1393: show which voice will actually speak, and why — never a silent system/novelty voice.
|
|
50
|
+
const r = sayAvailable ? resolveVoice({ configured: configuredVoice, installed: voices }) : { voice: null, reason: '' };
|
|
51
|
+
return {
|
|
52
|
+
platform, sayAvailable, voiceCount: voices.length, swiftcPresent, whisperBinPresent, whisperModelPresent, micToolPresent, resolvedStt,
|
|
53
|
+
espeakPresent, piperPresent, piperVoicePresent, playerName,
|
|
54
|
+
resolvedVoice: sayAvailable ? r.voice : undefined, resolvedVoiceReason: r.reason,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** The keyless data sources the live app depends on, with a cheap reachability URL for each. */
|
|
58
|
+
export const DATA_SOURCES = [
|
|
59
|
+
{ name: 'DexScreener', url: 'https://api.dexscreener.com/latest/dex/search?q=SOL' },
|
|
60
|
+
{ name: 'Jupiter', url: 'https://lite-api.jup.ag/price/v3?ids=So11111111111111111111111111111111111111112' },
|
|
61
|
+
{ name: 'GeckoTerminal', url: 'https://api.geckoterminal.com/api/v2/networks' },
|
|
62
|
+
{ name: 'RugCheck', url: 'https://api.rugcheck.xyz/v1/stats/new_tokens' },
|
|
63
|
+
{ name: 'GoPlus', url: 'https://api.gopluslabs.io/api/v1/supported_chains' },
|
|
64
|
+
{ name: 'Solana RPC', url: 'https://api.mainnet-beta.solana.com' },
|
|
65
|
+
// PumpPortal is a websocket; a plain GET to the host proves the network path (new pairs is a soft dep).
|
|
66
|
+
{ name: 'PumpPortal', url: 'https://pumpportal.fun/', soft: true },
|
|
67
|
+
];
|
|
68
|
+
function parseVersion(v) {
|
|
69
|
+
const m = /(\d+)\.(\d+)\.(\d+)/.exec(v);
|
|
70
|
+
if (!m)
|
|
71
|
+
return [0, 0, 0];
|
|
72
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
73
|
+
}
|
|
74
|
+
/** Compare two semver triples: -1 / 0 / 1. */
|
|
75
|
+
export function cmpVersion(a, b) {
|
|
76
|
+
for (let i = 0; i < 3; i++) {
|
|
77
|
+
const d = (a[i] ?? 0) - (b[i] ?? 0);
|
|
78
|
+
if (d)
|
|
79
|
+
return d < 0 ? -1 : 1;
|
|
80
|
+
}
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
/** Default live probe: a GET with a hard timeout. Any HTTP answer = reachable; only transport errors fail. */
|
|
84
|
+
export async function livingProbe(url, timeoutMs = 4000) {
|
|
85
|
+
const ctl = new AbortController();
|
|
86
|
+
const timer = setTimeout(() => ctl.abort(), timeoutMs);
|
|
87
|
+
const t0 = Date.now();
|
|
88
|
+
try {
|
|
89
|
+
const res = await fetch(url, { method: 'GET', signal: ctl.signal, headers: { 'user-agent': 'dexterm-doctor' } });
|
|
90
|
+
// Drain a little so sockets close cleanly; we do not care about the body.
|
|
91
|
+
try {
|
|
92
|
+
await res.arrayBuffer();
|
|
93
|
+
}
|
|
94
|
+
catch { /* ignore */ }
|
|
95
|
+
return { reachable: true, status: res.status, ms: Date.now() - t0 };
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
const err = e.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : e.message;
|
|
99
|
+
return { reachable: false, error: err, ms: Date.now() - t0 };
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Default claude probe: spawn `claude --version` with a timeout. Presence only (login is not asserted here). */
|
|
106
|
+
export async function claudeProbe(timeoutMs = 4000) {
|
|
107
|
+
const { execFile } = await import('node:child_process');
|
|
108
|
+
return new Promise((resolve) => {
|
|
109
|
+
let done = false;
|
|
110
|
+
const finish = (r) => { if (!done) {
|
|
111
|
+
done = true;
|
|
112
|
+
resolve(r);
|
|
113
|
+
} };
|
|
114
|
+
try {
|
|
115
|
+
const child = execFile('claude', ['--version'], { timeout: timeoutMs }, (err) => {
|
|
116
|
+
if (err && err.code === 'ENOENT')
|
|
117
|
+
finish('missing');
|
|
118
|
+
else if (err)
|
|
119
|
+
finish('present'); // on PATH but the probe errored/timed out: do not block on it
|
|
120
|
+
else
|
|
121
|
+
finish('present');
|
|
122
|
+
});
|
|
123
|
+
child.on('error', () => finish('missing'));
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
finish('missing');
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/** Default writable check: create the directory, write and remove a temp file. Returns an error message or null. */
|
|
131
|
+
export function writableProbe(dir) {
|
|
132
|
+
try {
|
|
133
|
+
mkdirSync(dir, { recursive: true });
|
|
134
|
+
const tmp = mkdtempSync(join(dir, '.dexterm-doctor-'));
|
|
135
|
+
const f = join(tmp, 'probe');
|
|
136
|
+
writeFileSync(f, 'ok');
|
|
137
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
return e.message;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export function defaultDeps(env = process.env, isTTY = process.stdout.isTTY === true) {
|
|
145
|
+
return {
|
|
146
|
+
env,
|
|
147
|
+
isTTY,
|
|
148
|
+
nodeVersion: process.versions.node,
|
|
149
|
+
platform: process.platform,
|
|
150
|
+
claude: () => claudeProbe(),
|
|
151
|
+
probe: (url) => livingProbe(url),
|
|
152
|
+
checkWritable: writableProbe,
|
|
153
|
+
offline: env.DEXTERM_OFFLINE === '1' || env.DEXTERM_OFFLINE === 'true',
|
|
154
|
+
popupShell: () => {
|
|
155
|
+
const path = shellBinaryPath(env);
|
|
156
|
+
const cargo = spawnSync('cargo', ['--version'], { stdio: 'ignore', timeout: 3000 }).status === 0;
|
|
157
|
+
return { path, built: existsSync(path), cargo };
|
|
158
|
+
},
|
|
159
|
+
voiceProbe: () => defaultVoiceProbe(process.platform, loadConfig(dextermHome(env)).config.voice?.name ?? null),
|
|
160
|
+
content: () => ({ kb: kbStore(env).get(), skills: skillStore(env).get() }),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/** Run every check. Never throws; network and claude probes are the injected ones. */
|
|
164
|
+
export async function runDoctor(deps) {
|
|
165
|
+
const checks = [];
|
|
166
|
+
// 1. Node version
|
|
167
|
+
const node = parseVersion(deps.nodeVersion);
|
|
168
|
+
if (cmpVersion(node, MIN_NODE) >= 0) {
|
|
169
|
+
checks.push({ name: 'Node', status: 'pass', detail: `v${deps.nodeVersion} (need ${MIN_NODE.join('.')}+)` });
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
checks.push({
|
|
173
|
+
name: 'Node', status: 'fail',
|
|
174
|
+
detail: `v${deps.nodeVersion} is too old (need ${MIN_NODE.join('.')}+ for the TypeScript entry)`,
|
|
175
|
+
fix: 'Install Node 22.18+ : `nvm install 22` (nvm) or `brew install node` (Homebrew), then re-run.',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
// 2. Terminal colour
|
|
179
|
+
const depth = detectColorDepth(deps.env, deps.isTTY);
|
|
180
|
+
const term = deps.env.TERM_PROGRAM || deps.env.TERM || 'unknown';
|
|
181
|
+
if (!deps.isTTY) {
|
|
182
|
+
checks.push({
|
|
183
|
+
name: 'Terminal', status: 'warn',
|
|
184
|
+
detail: 'not a TTY (output is being piped); the interactive UI needs a real terminal',
|
|
185
|
+
fix: 'Run dexterm directly in a terminal window, not through a pipe or a non-interactive shell.',
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
else if (depth === 'truecolor') {
|
|
189
|
+
checks.push({ name: 'Terminal colour', status: 'pass', detail: `24-bit truecolor (${term})` });
|
|
190
|
+
}
|
|
191
|
+
else if (depth === '256') {
|
|
192
|
+
checks.push({
|
|
193
|
+
name: 'Terminal colour', status: 'warn',
|
|
194
|
+
detail: `256-colour only (${term}); dexterm runs but the neon look is flattened`,
|
|
195
|
+
fix: 'Use a truecolor terminal (iTerm2, WezTerm, Ghostty, Kitty, Alacritty, Windows Terminal) or set COLORTERM=truecolor.',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
checks.push({
|
|
200
|
+
name: 'Terminal colour', status: 'warn',
|
|
201
|
+
detail: `no colour (${term}${deps.env.NO_COLOR ? ', NO_COLOR set' : ''}); dexterm falls back to monochrome`,
|
|
202
|
+
fix: 'Unset NO_COLOR and use a colour terminal for the full look; dexterm still works without it.',
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
// 2b. Coin images (task-1326): the protocol this terminal gets, judged from the environment alone (the
|
|
206
|
+
// interactive run also asks the tty). Apple Terminal.app is the one known no-images terminal → warn + fix.
|
|
207
|
+
{
|
|
208
|
+
const p = protocolFromEnv(deps.env);
|
|
209
|
+
const label = p.protocol === 'kitty' ? 'real pixels (kitty graphics)' : p.protocol === 'iterm' ? 'real pixels (iTerm2 inline images)' : p.protocol === 'sixel' ? 'real pixels (sixel)' : 'half-block art';
|
|
210
|
+
if (p.protocol === 'blocks' && /apple terminal/i.test(p.reason)) {
|
|
211
|
+
checks.push({ name: 'Coin images', status: 'warn', detail: `${label}: ${p.reason}`, fix: 'For real coin pictures use Ghostty (`brew install --cask ghostty`) or iTerm2 (`brew install --cask iterm2`); the block-art fallback still shows every coin here.' });
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
checks.push({ name: 'Coin images', status: 'pass', detail: `${label} (${p.reason})${p.protocol === 'blocks' ? '; --no-images turns them off' : ''}` });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// 3. Claude CLI (the analyst). Not required to browse; required for the `a` analyst.
|
|
218
|
+
const claude = await deps.claude();
|
|
219
|
+
if (claude === 'missing') {
|
|
220
|
+
checks.push({
|
|
221
|
+
name: 'Claude CLI', status: 'warn',
|
|
222
|
+
detail: 'not found on PATH; the analyst (a) is disabled, the rest of dexterm works',
|
|
223
|
+
fix: 'Install Claude Code (https://claude.com/claude-code) and run `claude` once to log in.',
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
checks.push({
|
|
228
|
+
name: 'Claude CLI', status: 'pass',
|
|
229
|
+
detail: 'found on PATH (the analyst runs your local `claude`; log in once with `claude` if the analyst errors)',
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
// 4b. Login with X (optional, task-1357): the user's own X app + login unlock X posts in the SOCIAL panel / TRENDING.
|
|
233
|
+
{
|
|
234
|
+
const xs = new XTokenStore({ env: deps.env });
|
|
235
|
+
const meta = xs.meta();
|
|
236
|
+
const modes = xs.modes();
|
|
237
|
+
const clientId = xs.clientId(deps.env);
|
|
238
|
+
if (meta.loggedIn) {
|
|
239
|
+
const loose = (modes.tokens !== null && modes.tokens !== 0o600) || (modes.dir !== null && modes.dir !== 0o700);
|
|
240
|
+
checks.push({ name: 'Login with X', status: loose ? 'warn' : 'pass', detail: `logged in${meta.user ? ` as @${meta.user.username}` : ''} · scopes ${meta.scopes.join(' ') || '?'} · refresh ${meta.hasRefresh ? 'yes' : 'no'}${loose ? ' · token file permissions are looser than 0600' : ''}`, ...(loose ? { fix: `chmod 700 ${xs.dir} && chmod 600 ${xs.tokensPath}` } : {}) });
|
|
241
|
+
}
|
|
242
|
+
else if (clientId) {
|
|
243
|
+
checks.push({ name: 'Login with X', status: 'warn', detail: 'client id stored, not logged in — X posts stay off (keyless venues, news and on-chain rows still work)', fix: 'dexterm x login' });
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
checks.push({ name: 'Login with X', status: 'pass', detail: 'not set up (optional) — X posts need your own X app: free Native App + `dexterm x set-client-id` + `dexterm x login`; see docs › Login with X' });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// 4. Helius key (optional; the app is live and keyless without it, just with fewer holders/safety fields).
|
|
250
|
+
const rpc = resolveRpcUrl({ env: deps.env, home: deps.env.HOME || undefined });
|
|
251
|
+
if (rpc.provider === 'helius') {
|
|
252
|
+
checks.push({ name: 'Helius RPC', status: 'pass', detail: `key configured (${rpc.basis}) — holders & safety enriched` });
|
|
253
|
+
}
|
|
254
|
+
else if (rpc.provider === 'custom') {
|
|
255
|
+
checks.push({ name: 'RPC', status: 'pass', detail: 'custom endpoint via DEXTERM_RPC_URL' });
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
checks.push({
|
|
259
|
+
name: 'Helius RPC', status: 'warn',
|
|
260
|
+
detail: 'no key — dexterm uses the public RPC (holders & some safety fields will show as "needs key")',
|
|
261
|
+
fix: 'Free tier: make a key at https://dashboard.helius.dev, then `mkdir -p ~/.dexterm && printf %s "YOUR_KEY" > ~/.dexterm/helius.key && chmod 600 ~/.dexterm/helius.key`',
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
// 5. Network + each data source
|
|
265
|
+
if (deps.offline) {
|
|
266
|
+
checks.push({
|
|
267
|
+
name: 'Network', status: 'warn',
|
|
268
|
+
detail: 'DEXTERM_OFFLINE is set; skipped the live source probes (use --fixtures / --replay for offline data)',
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
const results = await Promise.all(DATA_SOURCES.map(async (s) => ({ s, r: await deps.probe(s.url) })));
|
|
273
|
+
const down = results.filter((x) => !x.r.reachable);
|
|
274
|
+
const hardDown = down.filter((x) => !x.s.soft);
|
|
275
|
+
if (down.length === 0) {
|
|
276
|
+
const slow = results.filter((x) => (x.r.ms ?? 0) > 2500).map((x) => x.s.name);
|
|
277
|
+
checks.push({
|
|
278
|
+
name: 'Data sources', status: 'pass',
|
|
279
|
+
detail: `all ${results.length} reachable${slow.length ? ` (slow: ${slow.join(', ')})` : ''}`,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
checks.push({
|
|
284
|
+
name: 'Data sources', status: hardDown.length ? 'fail' : 'warn',
|
|
285
|
+
detail: `unreachable: ${down.map((x) => `${x.s.name} (${x.r.error ?? 'error'})`).join(', ')}`,
|
|
286
|
+
fix: hardDown.length
|
|
287
|
+
? 'Check your internet connection / VPN / firewall. If only some hosts fail, they may be regionally blocked — try again or use a different network.'
|
|
288
|
+
: 'New pairs (PumpPortal) is unreachable; the rest of dexterm works. Check back later or your network may block its host.',
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// 5b. task-1325: the native chart window (docked beside the terminal) vs the browser fallback
|
|
293
|
+
if (deps.popupShell) {
|
|
294
|
+
const sh = deps.popupShell();
|
|
295
|
+
if (sh.built)
|
|
296
|
+
checks.push({ name: 'Chart window', status: 'pass', detail: `native window ready (${sh.path}); C on a token opens it docked beside this terminal` });
|
|
297
|
+
else
|
|
298
|
+
checks.push({
|
|
299
|
+
name: 'Chart window', status: 'warn',
|
|
300
|
+
detail: `native window not built — C opens the chart in your browser instead`,
|
|
301
|
+
fix: sh.cargo ? 'Run `npm run build:shell` in packages/popup (about a minute the first time).' : 'Install Rust (https://rustup.rs), then run `npm run build:shell` in packages/popup.',
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
// 6. Writable DB path
|
|
305
|
+
const dbPath = defaultDbPath(deps.env);
|
|
306
|
+
const dbDir = dirname(dbPath);
|
|
307
|
+
const werr = deps.checkWritable(dbDir);
|
|
308
|
+
if (!werr) {
|
|
309
|
+
checks.push({ name: 'Data directory', status: 'pass', detail: `${dbDir} is writable (cache, watchlist, credit meter)` });
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
checks.push({
|
|
313
|
+
name: 'Data directory', status: 'fail',
|
|
314
|
+
detail: `cannot write ${dbDir}: ${werr}`,
|
|
315
|
+
fix: `Fix the permissions (\`mkdir -p ${dbDir} && chmod u+rwx ${dbDir}\`) or run with \`--db :memory:\` for a throwaway session.`,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
// 6b. task-1377: the user's KB / skill overlay. Warn-only: a bad user file is skipped and the bundled one stays in effect.
|
|
319
|
+
if (deps.content) {
|
|
320
|
+
try {
|
|
321
|
+
const { kb, skills } = deps.content();
|
|
322
|
+
const part = (label, m) => `${label} ${m.bundled} bundled · ${m.user} user (${m.overridden} overridden, ${m.added} new)${m.skipped.length ? ` · ${m.skipped.length} skipped` : ''}`;
|
|
323
|
+
const detail = `${part('KB', kb)} · ${part('skills', skills)}`;
|
|
324
|
+
const skipped = [...kb.skipped.map((s) => `kb ${s.id}: ${s.reason}`), ...skills.skipped.map((s) => `skill ${s.id}: ${s.reason}`)];
|
|
325
|
+
if (skipped.length) {
|
|
326
|
+
checks.push({ name: 'KB + skills', status: 'warn', detail: `${detail}; skipped ${skipped.slice(0, 3).join('; ')}`, fix: `Run \`dexterm kb check\` / \`dexterm skill check\` and fix the file, or \`dexterm kb|skill reset <id>\` to go back to the bundled one.` });
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
checks.push({ name: 'KB + skills', status: 'pass', detail: `${detail} (yours: ${kb.dir}, ${skills.dir}; \`dexterm kb|skill list\`)` });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
catch { /* an overlay problem must not break the doctor */ }
|
|
333
|
+
}
|
|
334
|
+
// 7. Voice mode (task-1282). Warn-only — voice always degrades to text, so it never fails the doctor.
|
|
335
|
+
if (deps.voiceProbe) {
|
|
336
|
+
try {
|
|
337
|
+
const vc = voiceDoctor(await deps.voiceProbe());
|
|
338
|
+
for (const c of vc)
|
|
339
|
+
checks.push(c);
|
|
340
|
+
}
|
|
341
|
+
catch { /* a voice probe failure must not break the doctor */ }
|
|
342
|
+
}
|
|
343
|
+
const ok = !checks.some((c) => c.status === 'fail');
|
|
344
|
+
return { checks, ok };
|
|
345
|
+
}
|
|
346
|
+
const GLYPH = { pass: '✓', warn: '!', fail: '✗' };
|
|
347
|
+
const ANSI = { pass: '\x1b[32m', warn: '\x1b[33m', fail: '\x1b[31m' };
|
|
348
|
+
const RESET = '\x1b[0m';
|
|
349
|
+
const DIM = '\x1b[2m';
|
|
350
|
+
const BOLD = '\x1b[1m';
|
|
351
|
+
/** Render the result as a plain pass/fail list. `color` adds ANSI; off for pipes / NO_COLOR. */
|
|
352
|
+
export function renderDoctor(result, opts = {}) {
|
|
353
|
+
const color = opts.color ?? false;
|
|
354
|
+
const paint = (s, code) => (color ? code + s + RESET : s);
|
|
355
|
+
const lines = [];
|
|
356
|
+
lines.push(paint('dexterm doctor', color ? BOLD : ''));
|
|
357
|
+
lines.push('');
|
|
358
|
+
const nameW = Math.max(...result.checks.map((c) => c.name.length));
|
|
359
|
+
for (const c of result.checks) {
|
|
360
|
+
const g = paint(GLYPH[c.status], color ? ANSI[c.status] : '');
|
|
361
|
+
lines.push(` ${g} ${c.name.padEnd(nameW)} ${c.detail}`);
|
|
362
|
+
if (c.fix)
|
|
363
|
+
lines.push(` ${' '.repeat(nameW)} ${paint('→ ' + c.fix, color ? DIM : '')}`);
|
|
364
|
+
}
|
|
365
|
+
lines.push('');
|
|
366
|
+
const fails = result.checks.filter((c) => c.status === 'fail').length;
|
|
367
|
+
const warns = result.checks.filter((c) => c.status === 'warn').length;
|
|
368
|
+
if (result.ok && warns === 0)
|
|
369
|
+
lines.push(paint('All checks passed — run `dexterm` to start.', color ? ANSI.pass : ''));
|
|
370
|
+
else if (result.ok)
|
|
371
|
+
lines.push(paint(`Ready to run (${warns} warning${warns === 1 ? '' : 's'} above are optional).`, color ? ANSI.warn : ''));
|
|
372
|
+
else
|
|
373
|
+
lines.push(paint(`${fails} check${fails === 1 ? '' : 's'} failed — fix the ✗ lines above, then re-run \`dexterm doctor\`.`, color ? ANSI.fail : ''));
|
|
374
|
+
return lines.join('\n');
|
|
375
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// dexterm — EXPORT / SHARE (task-1401). One key (`E`) renders the current view to a PNG plus a plain-text report in
|
|
2
|
+
// ~/.dexterm/exports (DEXTERM_HOME overrides). Nothing is uploaded, posted or copied anywhere else.
|
|
3
|
+
//
|
|
4
|
+
// The renderer is reusable on purpose (task-1407 PNL cards build on it): give `renderExportPng` / `renderExportText`
|
|
5
|
+
// / `writeExport` an ExportView — any rendered Canvas plus its title, the data label (live / sample / replay) and the
|
|
6
|
+
// data ages — and it frames the view with a header band (title, the label, the export time) and a footer band (every
|
|
7
|
+
// data age, "nothing was uploaded"). The label is never optional: a sample-data frame always says SAMPLE DATA.
|
|
8
|
+
//
|
|
9
|
+
// The frame is rendered CALM (no rain, glitch or scanlines) with no toast, help card or prompt on top, so the PNG
|
|
10
|
+
// is the data, not an effect. PNGs are pure TypeScript (@dexterm/tui rasterize + @dexterm/data encodePng).
|
|
11
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { Canvas, rasterizeCanvas, fmtUsd, fmtPrice, fmtPct } from '../../tui/src/index.js';
|
|
14
|
+
import { encodePng } from '../../data/src/index.js';
|
|
15
|
+
import { dextermHome } from "./boot-state.js";
|
|
16
|
+
import { cleanText } from "./screener/rows.js";
|
|
17
|
+
import { rugP, fmtRugP, RUG_MODEL_LABEL } from "./screener/rug.js";
|
|
18
|
+
import { renderFrame } from "./state.js";
|
|
19
|
+
export const LABEL_TEXT = {
|
|
20
|
+
live: '● LIVE DATA',
|
|
21
|
+
sample: '◌ SAMPLE DATA · not real prices',
|
|
22
|
+
replay: '◌ REPLAY · recorded data',
|
|
23
|
+
};
|
|
24
|
+
/** `4s` / `3m` / `2h` / `5d`; `—` when unknown. */
|
|
25
|
+
export function fmtAge(sec) {
|
|
26
|
+
if (sec === null || !Number.isFinite(sec))
|
|
27
|
+
return '—';
|
|
28
|
+
const s = Math.max(0, Math.round(sec));
|
|
29
|
+
return s < 60 ? `${s}s` : s < 3600 ? `${Math.floor(s / 60)}m` : s < 86_400 ? `${Math.floor(s / 3600)}h` : `${Math.floor(s / 86_400)}d`;
|
|
30
|
+
}
|
|
31
|
+
const ageText = (a) => (a.ageSec !== null ? `${a.name} ${fmtAge(a.ageSec)}` : `${a.name} ${a.state || 'n/a'}`);
|
|
32
|
+
/** Local timestamp `2026-10-16 01:23:45` (the machine's own clock; the report also carries the ISO time). */
|
|
33
|
+
function localStamp(now) {
|
|
34
|
+
const d = new Date(now);
|
|
35
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
36
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
37
|
+
}
|
|
38
|
+
/** Wrap `parts` joined by ` · ` into lines of at most `w` columns. */
|
|
39
|
+
function wrapParts(prefix, parts, w) {
|
|
40
|
+
const out = [];
|
|
41
|
+
let line = prefix;
|
|
42
|
+
for (const p of parts) {
|
|
43
|
+
const add = line === prefix ? p : ` · ${p}`;
|
|
44
|
+
if (line.length + add.length > w && line !== prefix) {
|
|
45
|
+
out.push(line);
|
|
46
|
+
line = ' '.repeat(prefix.length) + p;
|
|
47
|
+
}
|
|
48
|
+
else
|
|
49
|
+
line += add;
|
|
50
|
+
}
|
|
51
|
+
out.push(line);
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Frame a rendered view: a header band (title, label, export time), the view itself, and a footer band (every data
|
|
56
|
+
* age, local-only note). Returns a new canvas; the view canvas is not changed.
|
|
57
|
+
*/
|
|
58
|
+
export function composeExportCanvas(v, now) {
|
|
59
|
+
const src = v.canvas;
|
|
60
|
+
const w = Math.max(src.w, 60);
|
|
61
|
+
const t = src.theme;
|
|
62
|
+
const footer = [
|
|
63
|
+
...(v.mint ? [` contract ${v.mint}`] : []),
|
|
64
|
+
...wrapParts(v.label === 'replay' ? ' data ages (replay clock): ' : ' data ages: ', v.ages.length ? v.ages.map(ageText) : ['none reported for this view'], w - 1),
|
|
65
|
+
` exported locally by dexterm ${localStamp(now)} · nothing was uploaded · not financial advice`,
|
|
66
|
+
];
|
|
67
|
+
const c = new Canvas(w, src.h + 2 + footer.length, t);
|
|
68
|
+
c.fill(0, 0, w, c.h, ' ', { bg: t.bg });
|
|
69
|
+
// header band
|
|
70
|
+
c.fill(0, 0, w, 1, ' ', { bg: t.headBg });
|
|
71
|
+
const label = ` ${LABEL_TEXT[v.label]} `;
|
|
72
|
+
const labelFg = v.label === 'live' ? t.up : t.warn;
|
|
73
|
+
c.text(1, 0, `dexterm export · ${v.title}`, { fg: t.text, bg: t.headBg, b: true }, Math.max(0, w - label.length - 3));
|
|
74
|
+
c.textR(0, 0, w - 1, label, { fg: labelFg, bg: t.headBg, b: true });
|
|
75
|
+
// the view
|
|
76
|
+
for (let y = 0; y < src.h; y++)
|
|
77
|
+
for (let x = 0; x < src.w; x++) {
|
|
78
|
+
const i = src.idx(x, y), j = c.idx(x, y + 1);
|
|
79
|
+
c.ch[j] = src.ch[i];
|
|
80
|
+
c.fg[j] = src.fg[i];
|
|
81
|
+
c.bg[j] = src.bg[i];
|
|
82
|
+
c.at[j] = src.at[i];
|
|
83
|
+
c.hard[j] = src.hard[i];
|
|
84
|
+
}
|
|
85
|
+
for (const s of src.images)
|
|
86
|
+
c.images.push({ ...s, y: s.y + 1 });
|
|
87
|
+
c.imageMode = src.imageMode;
|
|
88
|
+
// footer band
|
|
89
|
+
const fy = src.h + 1;
|
|
90
|
+
c.fill(0, fy, w, c.h - fy, ' ', { bg: t.headBg });
|
|
91
|
+
footer.forEach((line, k) => c.text(0, fy + 1 + k, line, { fg: k === footer.length - 1 ? t.muted : t.text, bg: t.headBg }, w));
|
|
92
|
+
c.text(0, fy, ` ${LABEL_TEXT[v.label]}`, { fg: labelFg, bg: t.headBg, b: true }, w);
|
|
93
|
+
return c;
|
|
94
|
+
}
|
|
95
|
+
/** The PNG bytes of a framed view. */
|
|
96
|
+
export function renderExportPng(v, now, opts = {}) {
|
|
97
|
+
const px = rasterizeCanvas(composeExportCanvas(v, now), opts);
|
|
98
|
+
return encodePng({ w: px.w, h: px.h, rgba: px.rgba });
|
|
99
|
+
}
|
|
100
|
+
/** The plain-text report: header, label, ages, the view's own lines, the local-only note. */
|
|
101
|
+
export function renderExportText(v, now) {
|
|
102
|
+
const L = [];
|
|
103
|
+
L.push(`dexterm export · ${v.title}`);
|
|
104
|
+
if (v.mint)
|
|
105
|
+
L.push(`contract ${v.mint}`);
|
|
106
|
+
L.push(`data ${LABEL_TEXT[v.label]}`);
|
|
107
|
+
L.push(`exported ${localStamp(now)} (local time) · ${new Date(now).toISOString()}`);
|
|
108
|
+
L.push(`data ages${v.label === 'replay' ? ' (on the replay clock)' : ''}`);
|
|
109
|
+
if (!v.ages.length)
|
|
110
|
+
L.push(' none reported for this view');
|
|
111
|
+
for (const a of v.ages)
|
|
112
|
+
L.push(` ${a.name.padEnd(12)} ${(a.ageSec !== null ? fmtAge(a.ageSec) : a.state || 'n/a').padEnd(14)} ${a.source ?? ''}`.trimEnd());
|
|
113
|
+
L.push('');
|
|
114
|
+
L.push(...v.report);
|
|
115
|
+
L.push('');
|
|
116
|
+
L.push('Exported locally by dexterm. Nothing was uploaded. Model outputs are labelled as such. Not financial advice.');
|
|
117
|
+
return L.join('\n') + '\n';
|
|
118
|
+
}
|
|
119
|
+
/** ~/.dexterm/exports (DEXTERM_HOME overrides). */
|
|
120
|
+
export function exportsDir(env = process.env) {
|
|
121
|
+
return join(dextermHome(env), 'exports');
|
|
122
|
+
}
|
|
123
|
+
const slug = (s) => s.replace(/[^A-Za-z0-9_-]+/g, '').slice(0, 16) || 'view';
|
|
124
|
+
/** Write `<dir>/dexterm-<kind>-<subject>-<stamp>.png` and `.txt` (dir 0700, files 0600). */
|
|
125
|
+
export function writeExport(v, opts = {}) {
|
|
126
|
+
const now = opts.now ?? Date.now();
|
|
127
|
+
const dir = opts.dir ?? exportsDir(opts.env);
|
|
128
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
129
|
+
const stamp = localStamp(now).replace(/[-: ]/g, '').replace(/^(\d{8})(\d{6})$/, '$1-$2');
|
|
130
|
+
const base = join(dir, `dexterm-${slug(v.kind)}-${slug(opts.subject ?? v.mint?.slice(0, 6) ?? '')}-${stamp}`);
|
|
131
|
+
const png = renderExportPng(v, now, opts.raster);
|
|
132
|
+
writeFileSync(`${base}.png`, png, { mode: 0o600 });
|
|
133
|
+
writeFileSync(`${base}.txt`, renderExportText(v, now), { mode: 0o600 });
|
|
134
|
+
return { png: `${base}.png`, txt: `${base}.txt`, bytes: png.length };
|
|
135
|
+
}
|
|
136
|
+
// ------------------------------------------------------------------------------------------ view builders
|
|
137
|
+
const PANEL_NAMES = { market: 'market', candles: 'candles', authorities: 'authorities', holders: 'holders', rugcheck: 'rugcheck', goplus: 'goplus', trades: 'trades' };
|
|
138
|
+
/** Data ages of a live token view, one per panel, from each panel's own fetch time. */
|
|
139
|
+
export function tokenAges(v, now) {
|
|
140
|
+
return Object.keys(PANEL_NAMES).map((p) => {
|
|
141
|
+
const s = v.panels[p];
|
|
142
|
+
const ok = s.fetchedAt !== null && (s.state === 'ok' || s.stale);
|
|
143
|
+
return {
|
|
144
|
+
name: PANEL_NAMES[p], source: s.via,
|
|
145
|
+
ageSec: ok ? (now - s.fetchedAt) / 1000 : null,
|
|
146
|
+
state: ok ? (s.stale ? 'stale' : null) : s.state === 'needs-key' ? 'needs key' : s.state === 'error' ? `unavailable${s.reason ? ` (${s.reason})` : ''}` : s.state,
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
const yesNo = (b) => (b === null ? 'unknown' : b ? 'ENABLED' : 'revoked');
|
|
151
|
+
/** The text report body for a live token view: the numbers on screen, each from its panel. */
|
|
152
|
+
export function tokenReport(state, v, now) {
|
|
153
|
+
const L = [];
|
|
154
|
+
L.push(`coin $${cleanText(v.symbol, 16)}${v.name ? ` · ${cleanText(v.name, 40)}` : ''} · ${v.launchpad}`);
|
|
155
|
+
L.push(`price ${fmtPrice(v.price)} 5m ${fmtPct(v.chg.m5)} 1h ${fmtPct(v.chg.h1)} 6h ${fmtPct(v.chg.h6)} 24h ${fmtPct(v.chg.h24)}`);
|
|
156
|
+
L.push(`market cap ${fmtUsd(v.mcap)}${v.mcapFlagged ? ' (flagged)' : ''} liquidity ${fmtUsd(v.liq)} vol 24h ${fmtUsd(v.vol24)}`);
|
|
157
|
+
L.push(`holders ${v.holders ?? '—'} top-10 ${v.top10 === null ? '—' : `${v.top10.toFixed(1)}%`} dev ${v.dev === null ? '—' : `${v.dev.toFixed(1)}%`}${v.holdersSample ? ` (${v.holdersSample})` : ''}`);
|
|
158
|
+
L.push(`age ${v.ageMin === null ? '—' : fmtAge(v.ageMin * 60)}`);
|
|
159
|
+
L.push(`authorities mint ${yesNo(v.mintAuth)} · freeze ${yesNo(v.freezeAuth)}${v.riskyExtensions.length ? ` · risky extensions: ${v.riskyExtensions.join(', ')}` : ''}`);
|
|
160
|
+
if (v.rug)
|
|
161
|
+
L.push(`rugcheck score ${v.rug.score ?? '—'} · ${v.rug.risks.length} risk${v.rug.risks.length === 1 ? '' : 's'}${v.rug.risks.length ? `: ${v.rug.risks.slice(0, 4).map((r) => cleanText(r.name, 40)).join('; ')}` : ''}${v.rug.lpLockedPct !== null ? ` · LP locked ${v.rug.lpLockedPct.toFixed(0)}%` : ''}`);
|
|
162
|
+
if (v.flags.length)
|
|
163
|
+
L.push(`flags ${v.flags.join(', ')}`);
|
|
164
|
+
const row = state.screener.rows.find((r) => r.mint === v.mint);
|
|
165
|
+
const p = row ? rugP(row) : null;
|
|
166
|
+
if (p !== null)
|
|
167
|
+
L.push(`p(rug) ${fmtRugP(p)} · model output: ${RUG_MODEL_LABEL} (a ranking, not a probability; see dexterm backtest)`);
|
|
168
|
+
const larp = state.larpByMint?.get(v.mint);
|
|
169
|
+
if (larp)
|
|
170
|
+
L.push(`LARP check ${larp.verdict}${larp.confidence !== null ? ` · confidence ${larp.confidence.toFixed(2)}` : ''} · model output, ${fmtAge((now - larp.at) / 1000)} old · a risk flag, not a certificate`);
|
|
171
|
+
const links = [v.socialLinks.web, v.socialLinks.x, v.socialLinks.tg].filter((x) => !!x).map((u) => cleanText(u, 120));
|
|
172
|
+
if (links.length)
|
|
173
|
+
L.push(`links ${links.join(' ')} (advertised by the coin; unverified)`);
|
|
174
|
+
return L;
|
|
175
|
+
}
|
|
176
|
+
/** Plain text of every row of a canvas (the generic report body for screens without a dedicated builder). */
|
|
177
|
+
export function canvasText(c) {
|
|
178
|
+
const out = [];
|
|
179
|
+
for (let y = 0; y < c.h; y++)
|
|
180
|
+
out.push(c.ch.slice(y * c.w, (y + 1) * c.w).join('').trimEnd());
|
|
181
|
+
while (out.length && out[out.length - 1] === '')
|
|
182
|
+
out.pop();
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Render the current screen for export: CALM (no rain / glitch / scanlines), and without the toast, help card or
|
|
187
|
+
* GO-TO prompt on top. The state is restored afterwards, so the live loop is unaffected.
|
|
188
|
+
*/
|
|
189
|
+
export function renderCalmFrame(state, w, h) {
|
|
190
|
+
const v = state.vfx;
|
|
191
|
+
const saved = { intensity: v.intensity, toast: v.toast, overlay: state.overlay, goto: state.goto };
|
|
192
|
+
try {
|
|
193
|
+
v.intensity = 'off';
|
|
194
|
+
v.toast = null;
|
|
195
|
+
state.overlay = null;
|
|
196
|
+
state.goto = null;
|
|
197
|
+
return renderFrame(state, w, h);
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
v.intensity = saved.intensity;
|
|
201
|
+
v.toast = saved.toast;
|
|
202
|
+
state.overlay = saved.overlay;
|
|
203
|
+
state.goto = saved.goto;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/** The data label of what is on screen: seeded fixtures are SAMPLE, a `--replay` file is REPLAY, else LIVE. */
|
|
207
|
+
export function dataLabelOf(state) {
|
|
208
|
+
return state.screener.source === 'fixtures' ? 'sample' : state.screener.source === 'replay' ? 'replay' : 'live';
|
|
209
|
+
}
|
|
210
|
+
/** Build the ExportView for whatever screen is showing (the token screen gets the full per-panel report). */
|
|
211
|
+
export function exportViewFor(state, w, h, now) {
|
|
212
|
+
const canvas = renderCalmFrame(state, w, h);
|
|
213
|
+
const label = dataLabelOf(state);
|
|
214
|
+
const tv = state.screen === 'token' && label !== 'sample' ? state.live?.token?.view ?? null : null;
|
|
215
|
+
if (tv) {
|
|
216
|
+
return {
|
|
217
|
+
kind: 'token', title: `TOKEN · $${cleanText(tv.symbol, 16)}${tv.name ? ` · ${cleanText(tv.name, 32)}` : ''}`, mint: tv.mint,
|
|
218
|
+
label, canvas, ages: tokenAges(tv, now), report: tokenReport(state, tv, now),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const sample = label === 'sample' ? [{ name: 'all panels', source: 'seeded fixtures', ageSec: null, state: 'sample data' }] : [];
|
|
222
|
+
if (state.screen === 'token') {
|
|
223
|
+
const tk = state.token || state.fx.tokens[state.selected || 0];
|
|
224
|
+
return { kind: 'token', title: `TOKEN · $${cleanText(tk?.symbol ?? '?', 16)}`, mint: tk?.mint ?? null, label, canvas, ages: sample, report: canvasText(canvas) };
|
|
225
|
+
}
|
|
226
|
+
const ages = [...sample];
|
|
227
|
+
if (label !== 'sample' && state.screener.rows.length) {
|
|
228
|
+
const ts = state.screener.rows.map((r) => r.fetchedAt).filter((t) => typeof t === 'number' && Number.isFinite(t));
|
|
229
|
+
if (ts.length) {
|
|
230
|
+
ages.push({ name: 'rows newest', source: 'screener feed', ageSec: (now - Math.max(...ts)) / 1000 });
|
|
231
|
+
ages.push({ name: 'rows oldest', source: 'screener feed', ageSec: (now - Math.min(...ts)) / 1000 });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return { kind: state.screen, title: state.screen.toUpperCase(), mint: null, label, canvas, ages, report: canvasText(canvas) };
|
|
235
|
+
}
|