dexterm-cli 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/dist/packages/agents/src/audit.js +122 -0
- package/dist/packages/agents/src/bus.js +379 -0
- package/dist/packages/agents/src/executor.js +177 -0
- package/dist/packages/agents/src/gate.js +176 -0
- package/dist/packages/agents/src/identity.js +179 -0
- package/dist/packages/agents/src/index.js +15 -0
- package/dist/packages/agents/src/proposals.js +153 -0
- package/dist/packages/agents/src/schema.js +87 -0
- package/dist/packages/agents/src/sdk.js +140 -0
- package/dist/packages/agents/src/tiers.js +114 -0
- package/dist/packages/agents/src/tools.js +69 -0
- package/dist/packages/agents/src/ws.js +158 -0
- package/dist/packages/cli/assets/claude/analyst-system.md +76 -0
- package/dist/packages/cli/assets/claude/plugin/.claude-plugin/plugin.json +5 -0
- package/dist/packages/cli/assets/claude/plugin/skills/REGISTRY.json +16 -0
- package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +67 -0
- package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +64 -0
- package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +63 -0
- package/dist/packages/cli/assets/claude/plugin/skills/trench-scan/SKILL.md +44 -0
- package/dist/packages/cli/assets/claude/schemas/larp.schema.json +117 -0
- package/dist/packages/cli/assets/claude/schemas/trench-scan.schema.json +71 -0
- package/dist/packages/cli/assets/claude/schemas/verdict.schema.json +115 -0
- package/dist/packages/cli/bin/dexterm.mjs +11 -0
- package/dist/packages/cli/src/agent-cmd.js +303 -0
- package/dist/packages/cli/src/agent-roster.js +26 -0
- package/dist/packages/cli/src/analyst/backend.js +526 -0
- package/dist/packages/cli/src/analyst/results.js +38 -0
- package/dist/packages/cli/src/analyst/session.js +319 -0
- package/dist/packages/cli/src/audit.js +51 -0
- package/dist/packages/cli/src/backtest-cmd.js +149 -0
- package/dist/packages/cli/src/boot-state.js +62 -0
- package/dist/packages/cli/src/custom.js +261 -0
- package/dist/packages/cli/src/display.js +191 -0
- package/dist/packages/cli/src/doctor.js +361 -0
- package/dist/packages/cli/src/feed.js +634 -0
- package/dist/packages/cli/src/fixtures.js +275 -0
- package/dist/packages/cli/src/follow-cmd.js +221 -0
- package/dist/packages/cli/src/images-config.js +59 -0
- package/dist/packages/cli/src/index.js +31 -0
- package/dist/packages/cli/src/kb-skill-cmd.js +240 -0
- package/dist/packages/cli/src/larp-record.js +21 -0
- package/dist/packages/cli/src/live/copy.js +80 -0
- package/dist/packages/cli/src/live/flow.js +233 -0
- package/dist/packages/cli/src/live/index.js +509 -0
- package/dist/packages/cli/src/live/intel.js +227 -0
- package/dist/packages/cli/src/live/newpairs.js +334 -0
- package/dist/packages/cli/src/live/screens/newpairs-live.js +219 -0
- package/dist/packages/cli/src/live/screens/token-live.js +459 -0
- package/dist/packages/cli/src/live/screens/watchlist-live.js +149 -0
- package/dist/packages/cli/src/live/token.js +585 -0
- package/dist/packages/cli/src/live/trade.js +929 -0
- package/dist/packages/cli/src/live/watchlist.js +178 -0
- package/dist/packages/cli/src/main.js +5583 -0
- package/dist/packages/cli/src/narrative-cmd.js +76 -0
- package/dist/packages/cli/src/pack-cmd.js +323 -0
- package/dist/packages/cli/src/plan-cmd.js +278 -0
- package/dist/packages/cli/src/popup.js +810 -0
- package/dist/packages/cli/src/rules-cmd.js +131 -0
- package/dist/packages/cli/src/rules.js +339 -0
- package/dist/packages/cli/src/scan/command.js +57 -0
- package/dist/packages/cli/src/scan/engine.js +419 -0
- package/dist/packages/cli/src/scan/observe.js +64 -0
- package/dist/packages/cli/src/scan/ui.js +98 -0
- package/dist/packages/cli/src/screener/filter.js +241 -0
- package/dist/packages/cli/src/screener/presets.js +80 -0
- package/dist/packages/cli/src/screener/rows.js +146 -0
- package/dist/packages/cli/src/screener/rug.js +50 -0
- package/dist/packages/cli/src/screener/sort.js +52 -0
- package/dist/packages/cli/src/screener/view.js +183 -0
- package/dist/packages/cli/src/screens/analyst-session.js +343 -0
- package/dist/packages/cli/src/screens/analyst.js +326 -0
- package/dist/packages/cli/src/screens/boot.js +74 -0
- package/dist/packages/cli/src/screens/bubbles.js +255 -0
- package/dist/packages/cli/src/screens/coin.js +33 -0
- package/dist/packages/cli/src/screens/follows.js +201 -0
- package/dist/packages/cli/src/screens/kol.js +90 -0
- package/dist/packages/cli/src/screens/narrative.js +90 -0
- package/dist/packages/cli/src/screens/newpairs.js +150 -0
- package/dist/packages/cli/src/screens/overlay.js +257 -0
- package/dist/packages/cli/src/screens/plans.js +126 -0
- package/dist/packages/cli/src/screens/positions.js +176 -0
- package/dist/packages/cli/src/screens/rules.js +170 -0
- package/dist/packages/cli/src/screens/screener.js +459 -0
- package/dist/packages/cli/src/screens/social-panel.js +107 -0
- package/dist/packages/cli/src/screens/ticket.js +342 -0
- package/dist/packages/cli/src/screens/token.js +312 -0
- package/dist/packages/cli/src/screens/trending.js +181 -0
- package/dist/packages/cli/src/screens/watchlist.js +104 -0
- package/dist/packages/cli/src/social/demo.js +96 -0
- package/dist/packages/cli/src/social/view.js +250 -0
- package/dist/packages/cli/src/state.js +463 -0
- package/dist/packages/cli/src/theme-cmd.js +126 -0
- package/dist/packages/cli/src/trade.js +319 -0
- package/dist/packages/cli/src/voice-hud.js +42 -0
- package/dist/packages/cli/src/wallet-cmd.js +440 -0
- package/dist/packages/cli/src/wallet-command.js +31 -0
- package/dist/packages/cli/src/widgets.js +208 -0
- package/dist/packages/cli/src/workspace-cmd.js +68 -0
- package/dist/packages/cli/src/x-cmd.js +154 -0
- package/dist/packages/custom/src/config.js +213 -0
- package/dist/packages/custom/src/index.js +10 -0
- package/dist/packages/custom/src/keymap.js +107 -0
- package/dist/packages/custom/src/layout.js +235 -0
- package/dist/packages/custom/src/packs.js +633 -0
- package/dist/packages/custom/src/rules.js +704 -0
- package/dist/packages/custom/src/schema.js +515 -0
- package/dist/packages/custom/src/themes.js +176 -0
- package/dist/packages/custom/src/widget-html.js +222 -0
- package/dist/packages/custom/src/workspace.js +141 -0
- package/dist/packages/data/src/analyst-actions.js +133 -0
- package/dist/packages/data/src/budget.js +88 -0
- package/dist/packages/data/src/cache.js +142 -0
- package/dist/packages/data/src/candles.js +168 -0
- package/dist/packages/data/src/clock.js +64 -0
- package/dist/packages/data/src/credits.js +137 -0
- package/dist/packages/data/src/detail.js +97 -0
- package/dist/packages/data/src/fast-lane.js +321 -0
- package/dist/packages/data/src/fixtures.js +135 -0
- package/dist/packages/data/src/health.js +245 -0
- package/dist/packages/data/src/http.js +277 -0
- package/dist/packages/data/src/images/cache.js +141 -0
- package/dist/packages/data/src/images/decode-worker.js +28 -0
- package/dist/packages/data/src/images/decode.js +81 -0
- package/dist/packages/data/src/images/fetch.js +162 -0
- package/dist/packages/data/src/images/gif.js +214 -0
- package/dist/packages/data/src/images/index.js +10 -0
- package/dist/packages/data/src/images/jpeg.js +544 -0
- package/dist/packages/data/src/images/png.js +310 -0
- package/dist/packages/data/src/images/service.js +334 -0
- package/dist/packages/data/src/images/types.js +27 -0
- package/dist/packages/data/src/index.js +208 -0
- package/dist/packages/data/src/intel/helius-history.js +59 -0
- package/dist/packages/data/src/intel/holder-class.js +125 -0
- package/dist/packages/data/src/intel/rules.js +421 -0
- package/dist/packages/data/src/intel/scan.js +170 -0
- package/dist/packages/data/src/larp/evidence.js +288 -0
- package/dist/packages/data/src/larp/links.js +137 -0
- package/dist/packages/data/src/larp/robots.js +90 -0
- package/dist/packages/data/src/larp/safe-fetch.js +389 -0
- package/dist/packages/data/src/limiter.js +341 -0
- package/dist/packages/data/src/model.js +12 -0
- package/dist/packages/data/src/paper.js +331 -0
- package/dist/packages/data/src/rpc-config.js +126 -0
- package/dist/packages/data/src/rpc-pool.js +166 -0
- package/dist/packages/data/src/social/chain.js +83 -0
- package/dist/packages/data/src/social/dedupe.js +205 -0
- package/dist/packages/data/src/social/feed.js +462 -0
- package/dist/packages/data/src/social/firewall.js +110 -0
- package/dist/packages/data/src/social/index.js +11 -0
- package/dist/packages/data/src/social/kol.js +269 -0
- package/dist/packages/data/src/social/model.js +11 -0
- package/dist/packages/data/src/social/rank.js +184 -0
- package/dist/packages/data/src/social/rss.js +113 -0
- package/dist/packages/data/src/social/summary.js +104 -0
- package/dist/packages/data/src/social/venues.js +186 -0
- package/dist/packages/data/src/sources/dexscreener.js +170 -0
- package/dist/packages/data/src/sources/geckoterminal.js +133 -0
- package/dist/packages/data/src/sources/goplus.js +161 -0
- package/dist/packages/data/src/sources/jupiter-swap.js +236 -0
- package/dist/packages/data/src/sources/jupiter.js +198 -0
- package/dist/packages/data/src/sources/pda.js +113 -0
- package/dist/packages/data/src/sources/pump-curve.js +58 -0
- package/dist/packages/data/src/sources/pumpportal.js +247 -0
- package/dist/packages/data/src/sources/rugcheck.js +71 -0
- package/dist/packages/data/src/sources/solana-rpc.js +299 -0
- package/dist/packages/data/src/sources/solana-ws.js +287 -0
- package/dist/packages/data/src/store.js +289 -0
- package/dist/packages/data/src/trade/bridge.js +464 -0
- package/dist/packages/data/src/trade/caps.js +323 -0
- package/dist/packages/data/src/trade/copy/auto.js +385 -0
- package/dist/packages/data/src/trade/copy/follows.js +103 -0
- package/dist/packages/data/src/trade/copy/index.js +9 -0
- package/dist/packages/data/src/trade/copy/mirror.js +91 -0
- package/dist/packages/data/src/trade/copy/stats.js +120 -0
- package/dist/packages/data/src/trade/copy/tracker.js +158 -0
- package/dist/packages/data/src/trade/copy/trades.js +87 -0
- package/dist/packages/data/src/trade/copy/types.js +6 -0
- package/dist/packages/data/src/trade/executor.js +933 -0
- package/dist/packages/data/src/trade/kill-switch.js +47 -0
- package/dist/packages/data/src/trade/link-store.js +52 -0
- package/dist/packages/data/src/trade/plans/confirm.js +157 -0
- package/dist/packages/data/src/trade/plans/engine.js +784 -0
- package/dist/packages/data/src/trade/plans/executor-live.js +220 -0
- package/dist/packages/data/src/trade/plans/executor-paper.js +44 -0
- package/dist/packages/data/src/trade/plans/executor.js +11 -0
- package/dist/packages/data/src/trade/plans/index.js +12 -0
- package/dist/packages/data/src/trade/plans/journal.js +77 -0
- package/dist/packages/data/src/trade/plans/mandate.js +129 -0
- package/dist/packages/data/src/trade/plans/plan.js +179 -0
- package/dist/packages/data/src/trade/plans/service.js +168 -0
- package/dist/packages/data/src/trade/plans/types.js +6 -0
- package/dist/packages/data/src/trade/rpc-exec.js +229 -0
- package/dist/packages/data/src/trade/signer/client.js +197 -0
- package/dist/packages/data/src/trade/signer/fast-wallet.js +154 -0
- package/dist/packages/data/src/trade/signer/host.js +97 -0
- package/dist/packages/data/src/trade/signer/main.js +121 -0
- package/dist/packages/data/src/trade/signer/probe-builder.js +85 -0
- package/dist/packages/data/src/trade/signer/protocol.js +92 -0
- package/dist/packages/data/src/trade/signer/service.js +400 -0
- package/dist/packages/data/src/trade/signer/sol-usd-feed.js +98 -0
- package/dist/packages/data/src/trade/sol-usd.js +86 -0
- package/dist/packages/data/src/trade/swap-build.js +108 -0
- package/dist/packages/data/src/trade/token-delta.js +201 -0
- package/dist/packages/data/src/trade/tx-decode.js +632 -0
- package/dist/packages/data/src/util.js +24 -0
- package/dist/packages/data/src/watchlist.js +176 -0
- package/dist/packages/data/src/x/api.js +256 -0
- package/dist/packages/data/src/x/budget.js +66 -0
- package/dist/packages/data/src/x/config.js +49 -0
- package/dist/packages/data/src/x/index.js +10 -0
- package/dist/packages/data/src/x/mock-server.js +217 -0
- package/dist/packages/data/src/x/oauth.js +242 -0
- package/dist/packages/data/src/x/pkce.js +21 -0
- package/dist/packages/data/src/x/source.js +285 -0
- package/dist/packages/data/src/x/token-store.js +134 -0
- package/dist/packages/data/src/x/transport.js +71 -0
- package/dist/packages/data/vendor/solana-web3/LICENSE +20 -0
- package/dist/packages/data/vendor/solana-web3/NOTICE.md +13 -0
- package/dist/packages/data/vendor/solana-web3/index.iife.min.js +20 -0
- package/dist/packages/mcp/assets/cases.json +1194 -0
- package/dist/packages/mcp/assets/kb/entries/cabal-and-coordinated-launches.md +28 -0
- package/dist/packages/mcp/assets/kb/entries/common-rug-patterns.md +40 -0
- package/dist/packages/mcp/assets/kb/entries/dbc-mechanics.md +30 -0
- package/dist/packages/mcp/assets/kb/entries/dev-wallet-behaviour.md +33 -0
- package/dist/packages/mcp/assets/kb/entries/glossary.md +35 -0
- package/dist/packages/mcp/assets/kb/entries/holder-concentration.md +30 -0
- package/dist/packages/mcp/assets/kb/entries/jito-bundles.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/launchlab-curve.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/lp-burn-vs-lock.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/metadata-and-copycats.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/mint-freeze-authority.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/narrative-lifecycle.md +32 -0
- package/dist/packages/mcp/assets/kb/entries/pump-bonding-curve.md +33 -0
- package/dist/packages/mcp/assets/kb/entries/pumpswap-migration.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/sniping-and-snipers.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/token2022-extensions-risk.md +32 -0
- package/dist/packages/mcp/assets/kb/entries/wash-trading-fake-volume.md +33 -0
- package/dist/packages/mcp/assets/skills/rug-check.md +52 -0
- package/dist/packages/mcp/assets/skills/token-dossier.md +57 -0
- package/dist/packages/mcp/assets/skills/trench-scan.md +40 -0
- package/dist/packages/mcp/bin/dexterm-mcp.mjs +16 -0
- package/dist/packages/mcp/src/dossier.js +78 -0
- package/dist/packages/mcp/src/firewall.js +21 -0
- package/dist/packages/mcp/src/index.js +22 -0
- package/dist/packages/mcp/src/server.js +849 -0
- package/dist/packages/mcp/src/user-content.js +434 -0
- package/dist/packages/packs/src/backtest.js +183 -0
- package/dist/packages/packs/src/digest.js +95 -0
- package/dist/packages/packs/src/firewall.js +57 -0
- package/dist/packages/packs/src/index.js +19 -0
- package/dist/packages/packs/src/larp-eval.js +100 -0
- package/dist/packages/packs/src/narrative.js +309 -0
- package/dist/packages/packs/src/reason.js +135 -0
- package/dist/packages/packs/src/rug-model.js +137 -0
- package/dist/packages/packs/src/schema.js +54 -0
- package/dist/packages/packs/src/token-dossier.js +174 -0
- package/dist/packages/packs/src/verdicts.js +17 -0
- package/dist/packages/packs/src/wallet-dossier.js +105 -0
- package/dist/packages/popup/src/config.js +71 -0
- package/dist/packages/popup/src/dock.js +271 -0
- package/dist/packages/popup/src/host.js +210 -0
- package/dist/packages/popup/src/index.js +12 -0
- package/dist/packages/popup/src/loopback.js +53 -0
- package/dist/packages/popup/src/open.js +183 -0
- package/dist/packages/popup/src/pages/bubbles-lib.js +160 -0
- package/dist/packages/popup/src/pages/bubbles.css +23 -0
- package/dist/packages/popup/src/pages/bubbles.html +36 -0
- package/dist/packages/popup/src/pages/bubbles.js +214 -0
- package/dist/packages/popup/src/pages/chart-lib.js +262 -0
- package/dist/packages/popup/src/pages/chart.css +50 -0
- package/dist/packages/popup/src/pages/chart.html +56 -0
- package/dist/packages/popup/src/pages/chart.js +554 -0
- package/dist/packages/popup/src/pages/common.css +34 -0
- package/dist/packages/popup/src/pages/common.js +187 -0
- package/dist/packages/popup/src/pages/feeds.html +20 -0
- package/dist/packages/popup/src/pages/feeds.js +77 -0
- package/dist/packages/popup/src/pages/narrative.html +20 -0
- package/dist/packages/popup/src/pages/narrative.js +61 -0
- package/dist/packages/popup/src/pages/plans.css +30 -0
- package/dist/packages/popup/src/pages/plans.html +26 -0
- package/dist/packages/popup/src/pages/plans.js +82 -0
- package/dist/packages/popup/src/pages/roster.css +41 -0
- package/dist/packages/popup/src/pages/roster.html +25 -0
- package/dist/packages/popup/src/pages/roster.js +75 -0
- package/dist/packages/popup/src/pages/token.html +20 -0
- package/dist/packages/popup/src/pages/token.js +112 -0
- package/dist/packages/popup/src/pages/widget-sdk.js +224 -0
- package/dist/packages/popup/src/pages/widget.css +5 -0
- package/dist/packages/popup/src/pages/widget.html +21 -0
- package/dist/packages/popup/src/pages/widget.js +85 -0
- package/dist/packages/popup/src/protocol.js +31 -0
- package/dist/packages/popup/src/selftest.js +156 -0
- package/dist/packages/popup/src/server.js +582 -0
- package/dist/packages/popup/src/sinkproxy.js +167 -0
- package/dist/packages/popup/src/widget-html.js +231 -0
- package/dist/packages/popup/vendor/lightweight-charts/LICENSE +201 -0
- package/dist/packages/popup/vendor/lightweight-charts/NOTICE +2 -0
- package/dist/packages/popup/vendor/lightweight-charts/README.md +30 -0
- package/dist/packages/popup/vendor/lightweight-charts/VERSION +1 -0
- package/dist/packages/popup/vendor/lightweight-charts/lightweight-charts.standalone.production.js +7 -0
- package/dist/packages/recorder/bin/dexterm-recorder.mjs +11 -0
- package/dist/packages/recorder/src/cli.js +113 -0
- package/dist/packages/recorder/src/export.js +71 -0
- package/dist/packages/recorder/src/index.js +20 -0
- package/dist/packages/recorder/src/labels.js +151 -0
- package/dist/packages/recorder/src/open.js +29 -0
- package/dist/packages/recorder/src/recorder.js +553 -0
- package/dist/packages/recorder/src/schedule.js +43 -0
- package/dist/packages/recorder/src/schema.js +135 -0
- package/dist/packages/recorder/src/snapshot.js +191 -0
- package/dist/packages/recorder/src/sweep.js +52 -0
- package/dist/packages/recorder/src/verdicts.js +52 -0
- package/dist/packages/tui/src/bubbles.js +415 -0
- package/dist/packages/tui/src/canvas.js +370 -0
- package/dist/packages/tui/src/chrome.js +85 -0
- package/dist/packages/tui/src/color.js +133 -0
- package/dist/packages/tui/src/flush.js +62 -0
- package/dist/packages/tui/src/fmt.js +188 -0
- package/dist/packages/tui/src/fx-trade.js +278 -0
- package/dist/packages/tui/src/fx.js +527 -0
- package/dist/packages/tui/src/images/blocks.js +141 -0
- package/dist/packages/tui/src/images/coin.js +22 -0
- package/dist/packages/tui/src/images/detect.js +140 -0
- package/dist/packages/tui/src/images/index.js +7 -0
- package/dist/packages/tui/src/images/layer.js +146 -0
- package/dist/packages/tui/src/images/pixels.js +157 -0
- package/dist/packages/tui/src/images/protocols.js +87 -0
- package/dist/packages/tui/src/index.js +15 -0
- package/dist/packages/tui/src/keymap.js +355 -0
- package/dist/packages/tui/src/layout.js +11 -0
- package/dist/packages/tui/src/sound.js +177 -0
- package/dist/packages/tui/src/theme.js +182 -0
- package/dist/packages/tui/src/widgets.js +598 -0
- package/dist/packages/voice/src/controller.js +395 -0
- package/dist/packages/voice/src/doctor.js +63 -0
- package/dist/packages/voice/src/engines.js +44 -0
- package/dist/packages/voice/src/index.js +16 -0
- package/dist/packages/voice/src/router.js +502 -0
- package/dist/packages/voice/src/stt/apple-helper.swift.js +103 -0
- package/dist/packages/voice/src/stt/apple.js +179 -0
- package/dist/packages/voice/src/stt/fake.js +34 -0
- package/dist/packages/voice/src/stt/whisper.js +102 -0
- package/dist/packages/voice/src/summary.js +44 -0
- package/dist/packages/voice/src/tts/fake.js +30 -0
- package/dist/packages/voice/src/tts/say.js +96 -0
- package/dist/packages/voice/src/types.js +3 -0
- package/package.json +42 -4
- package/README.md +0 -3
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
// task-1360 Tier 3 — CAPPED AUTO-MIRROR (research/wallet-intel design §6 Stage 2), the riskiest piece, built last.
|
|
2
|
+
//
|
|
3
|
+
// A COPY POLICY per followed wallet (per-trade cap, daily cap, max open mirrors, cooldown, size factor, auto-disarm
|
|
4
|
+
// streak, time-to-live) is DEFAULT OFF. Enabling it is a physical gesture (keypress / Phantom) that also arms a
|
|
5
|
+
// standing COPY MANDATE in the plan engine — the signer trust domain — drafted with exactly those caps (clamped to
|
|
6
|
+
// the engine's hard ceilings), buy-only, `maxTier: 'hands-free'`. It is further gated on the engine's own "this is
|
|
7
|
+
// less safe" popup (`Settings.handsFreeAllowed`, keypress-accepted, 24 h expiry): without it nothing auto-arms.
|
|
8
|
+
// Each leader BUY then goes through the Tier 2 mirror checks, becomes a PROPOSED plan (by 'copy'), and is armed
|
|
9
|
+
// with a `copy-auto` proof — the WEAKEST confirmation tier, so the engine sizes it at ¼ of the per-trade cap and
|
|
10
|
+
// re-checks every cap, the allow-list and the panic state at arming and again on every fill. This service holds
|
|
11
|
+
// no key, cannot raise a cap (its hard ceilings are below the engine's), and comes back OFF after a restart.
|
|
12
|
+
//
|
|
13
|
+
// Honest limits: mirror-SELL is not built (a mirror buy is closed by the user); auto-disarm therefore counts
|
|
14
|
+
// consecutive refused / failed mirrors, not realised losses, until exits exist.
|
|
15
|
+
import { existsSync } from 'node:fs';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { systemClock } from "../../clock.js";
|
|
19
|
+
import { Journal } from "../plans/journal.js";
|
|
20
|
+
import { HARD, TIERS, mandateAlive } from "../plans/mandate.js";
|
|
21
|
+
import { PolicyRefusal, TERMINAL } from "../plans/engine.js";
|
|
22
|
+
import { PlanError } from "../plans/plan.js";
|
|
23
|
+
import { MandateError } from "../plans/mandate.js";
|
|
24
|
+
import { ConfirmError } from "../plans/confirm.js";
|
|
25
|
+
import { buildMirror, leaderSoldSince } from "./mirror.js";
|
|
26
|
+
/** Ceilings for an auto policy: stricter than the engine's (HARD), which clamp again underneath. */
|
|
27
|
+
export const AUTO_HARD = Object.freeze({
|
|
28
|
+
perTradeUsd: Math.min(100, HARD.perTradeUsd),
|
|
29
|
+
dailyUsd: Math.min(500, HARD.totalUsd),
|
|
30
|
+
maxOpen: 10,
|
|
31
|
+
minCooldownMs: 10_000,
|
|
32
|
+
maxK: 2,
|
|
33
|
+
maxDisarmStreak: 10,
|
|
34
|
+
ttlMs: Math.min(24 * 3600_000, HARD.ttlMs),
|
|
35
|
+
});
|
|
36
|
+
export const AUTO_DEFAULTS = Object.freeze({ perTradeUsd: 20, dailyUsd: 100, maxOpen: 3, cooldownMs: 60_000, k: 1, autoDisarmStreak: 3, ttlMs: 8 * 3600_000 });
|
|
37
|
+
/** The tier an auto-mirror arms at (the weakest), and so the fraction of the per-trade cap it may use. */
|
|
38
|
+
export const AUTO_TIER = 'hands-free';
|
|
39
|
+
export const AUTO_SIZE_FRACTION = TIERS[AUTO_TIER].perTradeMultiplier;
|
|
40
|
+
export const AUTO_GESTURE = 'copy-auto';
|
|
41
|
+
/** The most one auto-mirror may be: ¼ (the hands-free tier) of the mandate's per-trade cap, which the engine clamps to its total (= the daily cap). */
|
|
42
|
+
export function autoSizeCap(p) { return Math.round(Math.min(p.perTradeUsd, p.dailyUsd) * AUTO_SIZE_FRACTION * 100) / 100; }
|
|
43
|
+
export class CopyAutoError extends Error {
|
|
44
|
+
reason;
|
|
45
|
+
constructor(reason, message) { super(message); this.name = 'CopyAutoError'; this.reason = reason; }
|
|
46
|
+
}
|
|
47
|
+
export function defaultCopyJournalPath(env = process.env) {
|
|
48
|
+
const home = env.DEXTERM_HOME && env.DEXTERM_HOME.length ? env.DEXTERM_HOME : join(homedir(), '.dexterm');
|
|
49
|
+
return join(home, 'copy.journal');
|
|
50
|
+
}
|
|
51
|
+
function clampNum(v, dflt, min, max) {
|
|
52
|
+
const n = typeof v === 'number' ? v : Number(v);
|
|
53
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
54
|
+
return dflt;
|
|
55
|
+
return Math.min(max, Math.max(min, n));
|
|
56
|
+
}
|
|
57
|
+
/** Validate an untrusted policy input: every cap clamped DOWN to AUTO_HARD, never coerced upward. */
|
|
58
|
+
export function draftPolicy(wallet, input) {
|
|
59
|
+
const x = input ?? {};
|
|
60
|
+
const perTradeUsd = clampNum(x.perTradeUsd, AUTO_DEFAULTS.perTradeUsd, 0.01, AUTO_HARD.perTradeUsd);
|
|
61
|
+
// independent caps: a daily cap below the per-trade cap simply means fewer / smaller mirrors (the mandate clamps
|
|
62
|
+
// its per-trade down to its total on its own; auto fires at ¼ of per-trade anyway)
|
|
63
|
+
const dailyUsd = clampNum(x.dailyUsd, AUTO_DEFAULTS.dailyUsd, 0.01, AUTO_HARD.dailyUsd);
|
|
64
|
+
return Object.freeze({
|
|
65
|
+
wallet, perTradeUsd, dailyUsd,
|
|
66
|
+
maxOpen: Math.round(clampNum(x.maxOpen, AUTO_DEFAULTS.maxOpen, 1, AUTO_HARD.maxOpen)),
|
|
67
|
+
cooldownMs: clampNum(x.cooldownMs, AUTO_DEFAULTS.cooldownMs, AUTO_HARD.minCooldownMs, 24 * 3600_000),
|
|
68
|
+
k: clampNum(x.k, AUTO_DEFAULTS.k, 0.01, AUTO_HARD.maxK),
|
|
69
|
+
autoDisarmStreak: Math.round(clampNum(x.autoDisarmStreak, AUTO_DEFAULTS.autoDisarmStreak, 1, AUTO_HARD.maxDisarmStreak)),
|
|
70
|
+
ttlMs: clampNum(x.ttlMs, AUTO_DEFAULTS.ttlMs, 60_000, AUTO_HARD.ttlMs),
|
|
71
|
+
enabled: false, mandateId: null, enabledAt: null, offReason: null,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export class CopyAuto {
|
|
75
|
+
journal;
|
|
76
|
+
plans;
|
|
77
|
+
clock;
|
|
78
|
+
market;
|
|
79
|
+
leaderIntel;
|
|
80
|
+
trades;
|
|
81
|
+
isFollowed;
|
|
82
|
+
policies = new Map();
|
|
83
|
+
autoPlans = new Map();
|
|
84
|
+
lastAt = new Map();
|
|
85
|
+
streak = new Map();
|
|
86
|
+
last = new Map();
|
|
87
|
+
panicked = false;
|
|
88
|
+
queue = Promise.resolve();
|
|
89
|
+
#runners = new Map();
|
|
90
|
+
constructor(deps) {
|
|
91
|
+
this.plans = deps.plans;
|
|
92
|
+
this.clock = deps.clock ?? systemClock;
|
|
93
|
+
this.market = deps.market;
|
|
94
|
+
this.leaderIntel = deps.leaderIntel ?? (() => null);
|
|
95
|
+
this.trades = deps.trades;
|
|
96
|
+
this.isFollowed = deps.isFollowed ?? (() => true);
|
|
97
|
+
const path = deps.journalPath === undefined ? null : deps.journalPath;
|
|
98
|
+
this.journal = new Journal({ path, clock: this.clock });
|
|
99
|
+
if (path && existsSync(path))
|
|
100
|
+
this.recover();
|
|
101
|
+
}
|
|
102
|
+
/** Rebuild policies (always OFF after a restart) and the auto-plan ledger from the journal. */
|
|
103
|
+
recover() {
|
|
104
|
+
const v = this.journal.verify();
|
|
105
|
+
if (!v.ok)
|
|
106
|
+
throw new Error(`copy journal chain broken at seq ${v.atSeq}: ${v.why}`);
|
|
107
|
+
const live = new Map(); // wallet → copy mandate that was ON at the crash/quit
|
|
108
|
+
for (const e of this.journal.events) {
|
|
109
|
+
const d = (e.data ?? {});
|
|
110
|
+
if (e.type === 'copy.policy' && typeof d.wallet === 'string')
|
|
111
|
+
this.policies.set(d.wallet, { ...draftPolicy(d.wallet, d.policy), offReason: 'off after restart: re-enable with a keypress' });
|
|
112
|
+
else if (e.type === 'copy.auto.proposed' && typeof e.planId === 'string')
|
|
113
|
+
this.autoPlans.set(e.planId, { planId: e.planId, wallet: String(d.wallet), sizeUsd: Number(d.sizeUsd) || 0, at: e.ts, armed: false, judged: false });
|
|
114
|
+
else if (e.type === 'copy.auto.armed' && typeof e.planId === 'string') {
|
|
115
|
+
const r = this.autoPlans.get(e.planId);
|
|
116
|
+
if (r)
|
|
117
|
+
r.armed = true;
|
|
118
|
+
}
|
|
119
|
+
else if (e.type === 'copy.auto.enabled' && typeof d.wallet === 'string' && typeof e.mandateId === 'string')
|
|
120
|
+
live.set(d.wallet, e.mandateId);
|
|
121
|
+
else if (e.type === 'copy.auto.disabled' && typeof d.wallet === 'string')
|
|
122
|
+
live.delete(d.wallet);
|
|
123
|
+
else if (e.type === 'copy.policy.removed' && typeof d.wallet === 'string') {
|
|
124
|
+
this.policies.delete(d.wallet);
|
|
125
|
+
live.delete(d.wallet);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Auto-mirror never survives a restart: a copy mandate the plan engine recovered as armed is revoked here, so
|
|
129
|
+
// nothing can fire until a person enables it again with a keypress (and the less-safe popup is fresh).
|
|
130
|
+
for (const [wallet, mandateId] of live) {
|
|
131
|
+
try {
|
|
132
|
+
this.plans.revokeMandate(mandateId, 'copy auto off after restart');
|
|
133
|
+
}
|
|
134
|
+
catch { /* already gone */ }
|
|
135
|
+
this.journal.append('copy.auto.disabled', { mandateId, data: { wallet, reason: 'off after restart' } });
|
|
136
|
+
}
|
|
137
|
+
this.journal.append('copy.recovered', { data: { policies: this.policies.size, autoPlans: this.autoPlans.size, revoked: live.size, note: 'auto-mirror is OFF after a restart' } });
|
|
138
|
+
}
|
|
139
|
+
// ───────────── policies ─────────────
|
|
140
|
+
policy(wallet) { return this.policies.get(wallet) ?? null; }
|
|
141
|
+
list() { return [...this.policies.values()]; }
|
|
142
|
+
/** Draft (or redraft) the caps for a wallet. Refused while enabled: a different cap is disable + a new gesture. */
|
|
143
|
+
draft(wallet, input) {
|
|
144
|
+
const cur = this.policies.get(wallet);
|
|
145
|
+
if (cur?.enabled)
|
|
146
|
+
throw new CopyAutoError('enabled', 'auto-mirror is ON for this wallet; disable it before changing its caps');
|
|
147
|
+
const p = draftPolicy(wallet, input);
|
|
148
|
+
this.policies.set(wallet, p);
|
|
149
|
+
this.journal.append('copy.policy', { data: { wallet, policy: { perTradeUsd: p.perTradeUsd, dailyUsd: p.dailyUsd, maxOpen: p.maxOpen, cooldownMs: p.cooldownMs, k: p.k, autoDisarmStreak: p.autoDisarmStreak, ttlMs: p.ttlMs } } });
|
|
150
|
+
return p;
|
|
151
|
+
}
|
|
152
|
+
remove(wallet) {
|
|
153
|
+
const cur = this.policies.get(wallet);
|
|
154
|
+
if (!cur)
|
|
155
|
+
return false;
|
|
156
|
+
if (cur.enabled)
|
|
157
|
+
this.disable(wallet, 'policy removed');
|
|
158
|
+
this.policies.delete(wallet);
|
|
159
|
+
this.journal.append('copy.policy.removed', { data: { wallet } });
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Turn auto-mirror ON for a wallet. Needs: a physical gesture (keypress / Phantom — the same rule as arming any
|
|
164
|
+
* mandate), the engine's "less safe" popup accepted (hands-free allowed), no panic, a followed wallet, a drafted
|
|
165
|
+
* policy. Drafts + arms the COPY MANDATE with the policy's caps in one gesture.
|
|
166
|
+
*/
|
|
167
|
+
enable(wallet, gesture) {
|
|
168
|
+
const cur = this.policies.get(wallet);
|
|
169
|
+
if (!cur)
|
|
170
|
+
throw new CopyAutoError('no-policy', 'draft the caps first (dexterm follow auto <addr> --per-trade … or the A key)');
|
|
171
|
+
if (cur.enabled)
|
|
172
|
+
return cur;
|
|
173
|
+
if (gesture?.gesture !== 'keypress' && gesture?.gesture !== 'phantom')
|
|
174
|
+
throw new CopyAutoError('needs-keypress', 'enabling auto-mirror needs a physical keypress (or Phantom approval); voice, agents and pipes cannot');
|
|
175
|
+
if (this.panicked)
|
|
176
|
+
throw new CopyAutoError('panicked', 'panic engaged; auto-mirror stays off');
|
|
177
|
+
if (!this.isFollowed(wallet))
|
|
178
|
+
throw new CopyAutoError('not-followed', 'follow the wallet first');
|
|
179
|
+
if (!this.plans.riskStatus().active)
|
|
180
|
+
throw new CopyAutoError('gate-on', 'the keypress gate is ON: accept the "this is less safe" popup first (G on the O screen); auto-mirror fires at the hands-free tier');
|
|
181
|
+
const now = this.clock.now();
|
|
182
|
+
const m = this.plans.draftMandate({
|
|
183
|
+
totalUsd: cur.dailyUsd, perTradeUsd: cur.perTradeUsd, perCoinUsd: cur.perTradeUsd, dailyLossUsd: cur.dailyUsd,
|
|
184
|
+
maxOpenPositions: cur.maxOpen, maxArmedPlans: cur.maxOpen, actions: ['buy'], maxTier: AUTO_TIER, ttlMs: cur.ttlMs,
|
|
185
|
+
});
|
|
186
|
+
const engine = this.plans.engine;
|
|
187
|
+
this.#runners.set(wallet, engine.authorizeCopy(m.id, engine.issueMandateProof(m.id, gesture.gesture), wallet));
|
|
188
|
+
const p = Object.freeze({ ...cur, enabled: true, mandateId: m.id, enabledAt: now, offReason: null });
|
|
189
|
+
this.policies.set(wallet, p);
|
|
190
|
+
this.streak.set(wallet, 0);
|
|
191
|
+
this.journal.append('copy.auto.enabled', { mandateId: m.id, data: { wallet, by: gesture.gesture, perTradeUsd: cur.perTradeUsd, autoSizeUsd: autoSizeCap(cur), dailyUsd: cur.dailyUsd, maxOpen: cur.maxOpen, cooldownMs: cur.cooldownMs, expiresAt: m.expiresAt } });
|
|
192
|
+
return p;
|
|
193
|
+
}
|
|
194
|
+
/** Turn it OFF (always allowed, no gesture): revokes the copy mandate, which cancels its unfilled mirrors. */
|
|
195
|
+
disable(wallet, reason = 'user') {
|
|
196
|
+
const cur = this.policies.get(wallet);
|
|
197
|
+
if (!cur)
|
|
198
|
+
return null;
|
|
199
|
+
if (!cur.enabled)
|
|
200
|
+
return cur;
|
|
201
|
+
this.#runners.delete(wallet);
|
|
202
|
+
try {
|
|
203
|
+
if (cur.mandateId)
|
|
204
|
+
this.plans.revokeMandate(cur.mandateId, `copy auto off: ${reason}`);
|
|
205
|
+
}
|
|
206
|
+
catch { /* already gone */ }
|
|
207
|
+
const p = Object.freeze({ ...cur, enabled: false, mandateId: null, enabledAt: null, offReason: reason });
|
|
208
|
+
this.policies.set(wallet, p);
|
|
209
|
+
this.journal.append('copy.auto.disabled', { mandateId: cur.mandateId, data: { wallet, reason } });
|
|
210
|
+
return p;
|
|
211
|
+
}
|
|
212
|
+
/** PANIC: every policy off, every copy mandate revoked, and the plan engine's own panic (revokes ALL mandates). */
|
|
213
|
+
panic(reason = 'panic key') {
|
|
214
|
+
this.panicked = true;
|
|
215
|
+
for (const p of this.policies.values())
|
|
216
|
+
if (p.enabled)
|
|
217
|
+
this.disable(p.wallet, `panic: ${reason}`);
|
|
218
|
+
this.journal.append('copy.panic', { data: { reason } });
|
|
219
|
+
try {
|
|
220
|
+
this.plans.panic(reason);
|
|
221
|
+
}
|
|
222
|
+
catch { /* engine already panicked */ }
|
|
223
|
+
}
|
|
224
|
+
get isPanicked() { return this.panicked; }
|
|
225
|
+
// ───────────── accounting ─────────────
|
|
226
|
+
recsFor(wallet) { return [...this.autoPlans.values()].filter((r) => r.wallet === wallet); }
|
|
227
|
+
planStatus(planId) { return this.plans.engine.status(planId); }
|
|
228
|
+
spent24h(wallet, now = this.clock.now()) {
|
|
229
|
+
return this.recsFor(wallet).filter((r) => r.armed && now - r.at < 24 * 3600_000).filter((r) => { const s = this.planStatus(r.planId); return s !== null && s !== 'cancelled' && s !== 'failed'; }).reduce((s, r) => s + r.sizeUsd, 0);
|
|
230
|
+
}
|
|
231
|
+
openCount(wallet) {
|
|
232
|
+
return this.recsFor(wallet).filter((r) => r.armed).filter((r) => { const s = this.planStatus(r.planId); return s !== null && !TERMINAL.has(s) && s !== 'proposed'; }).length;
|
|
233
|
+
}
|
|
234
|
+
status(wallet) {
|
|
235
|
+
const p = this.policies.get(wallet) ?? null;
|
|
236
|
+
const now = this.clock.now();
|
|
237
|
+
const m = p?.mandateId ? this.plans.engine.mandates.get(p.mandateId)?.m ?? null : null;
|
|
238
|
+
const last = this.lastAt.get(wallet) ?? 0;
|
|
239
|
+
return {
|
|
240
|
+
policy: p, enabled: !!p?.enabled, gateActive: this.plans.riskStatus().active,
|
|
241
|
+
mandateAlive: m ? mandateAlive(m, now).ok : false,
|
|
242
|
+
spent24hUsd: this.spent24h(wallet, now), open: this.openCount(wallet),
|
|
243
|
+
cooldownLeftMs: p ? Math.max(0, p.cooldownMs - (now - last)) : 0,
|
|
244
|
+
streak: this.streak.get(wallet) ?? 0, lastOutcome: this.last.get(wallet) ?? null, panicked: this.panicked,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/** Plans THIS service proposed (never a voice/agent/user plan), newest first. */
|
|
248
|
+
autoPlanIds(wallet) { return [...this.autoPlans.values()].filter((r) => !wallet || r.wallet === wallet).sort((a, b) => b.at - a.at).map((r) => r.planId); }
|
|
249
|
+
// ───────────── the auto path ─────────────
|
|
250
|
+
/** A tracker alert. Serialised; never throws (every outcome is journaled and kept for status()). */
|
|
251
|
+
onAlert(alert) {
|
|
252
|
+
const run = this.queue.then(() => this.handle(alert)).catch((e) => ({ kind: 'refused', reason: 'error', message: e instanceof Error ? e.message : String(e), planId: null }));
|
|
253
|
+
this.queue = run;
|
|
254
|
+
return run;
|
|
255
|
+
}
|
|
256
|
+
settle(wallet, out) {
|
|
257
|
+
this.last.set(wallet, out);
|
|
258
|
+
if (out.kind === 'refused') {
|
|
259
|
+
const n = (this.streak.get(wallet) ?? 0) + 1;
|
|
260
|
+
this.streak.set(wallet, n);
|
|
261
|
+
const p = this.policies.get(wallet);
|
|
262
|
+
this.journal.append('copy.auto.refused', { planId: out.planId, data: { wallet, reason: out.reason, message: out.message, streak: n } });
|
|
263
|
+
if (p?.enabled && n >= p.autoDisarmStreak)
|
|
264
|
+
this.disable(wallet, `auto-disarm: ${n} consecutive refused/failed mirrors (last: ${out.reason})`);
|
|
265
|
+
}
|
|
266
|
+
else if (out.kind === 'armed')
|
|
267
|
+
this.streak.set(wallet, 0);
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
async handle(alert) {
|
|
271
|
+
const wallet = alert.wallet;
|
|
272
|
+
const p = this.policies.get(wallet);
|
|
273
|
+
if (!p || !p.enabled)
|
|
274
|
+
return { kind: 'ignored', why: 'auto-mirror off for this wallet' };
|
|
275
|
+
if (alert.trade.side !== 'buy')
|
|
276
|
+
return { kind: 'ignored', why: `a ${alert.trade.side} is never mirrored` };
|
|
277
|
+
const refuse = (reason, message, planId = null) => this.settle(wallet, { kind: 'refused', reason, message, planId });
|
|
278
|
+
const gate = () => {
|
|
279
|
+
if (this.panicked)
|
|
280
|
+
return refuse('panicked', 'panic engaged');
|
|
281
|
+
if (!this.isFollowed(wallet)) {
|
|
282
|
+
this.disable(wallet, 'wallet unfollowed');
|
|
283
|
+
return refuse('not-followed', 'the wallet is no longer followed');
|
|
284
|
+
}
|
|
285
|
+
if (!this.plans.riskStatus().active)
|
|
286
|
+
return refuse('gate-on', 'the keypress gate is on (the less-safe acceptance expired or was restored); auto-mirror paused');
|
|
287
|
+
const m = p.mandateId ? this.plans.engine.mandates.get(p.mandateId)?.m ?? null : null;
|
|
288
|
+
const alive = m ? mandateAlive(m, this.clock.now()) : { ok: false, reason: 'no-mandate' };
|
|
289
|
+
if (!alive.ok) {
|
|
290
|
+
this.disable(wallet, `copy mandate ${alive.reason}`);
|
|
291
|
+
return refuse(alive.reason, 'the copy mandate is no longer alive; auto-mirror off');
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
};
|
|
295
|
+
const g0 = gate();
|
|
296
|
+
if (g0)
|
|
297
|
+
return g0;
|
|
298
|
+
const now = this.clock.now();
|
|
299
|
+
const sinceLast = now - (this.lastAt.get(wallet) ?? -Infinity);
|
|
300
|
+
if (sinceLast < p.cooldownMs)
|
|
301
|
+
return refuse('cooldown', `cooldown: ${Math.ceil((p.cooldownMs - sinceLast) / 1000)} s left`);
|
|
302
|
+
const open = this.openCount(wallet);
|
|
303
|
+
if (open >= p.maxOpen)
|
|
304
|
+
return refuse('max-open', `${open} open mirrors (max ${p.maxOpen})`);
|
|
305
|
+
const spent = this.spent24h(wallet, now);
|
|
306
|
+
const dailyLeft = p.dailyUsd - spent;
|
|
307
|
+
if (dailyLeft < 0.01)
|
|
308
|
+
return refuse('daily-cap', `daily cap: $${spent.toFixed(2)} of $${p.dailyUsd} used in 24 h`);
|
|
309
|
+
let market = null;
|
|
310
|
+
try {
|
|
311
|
+
market = await this.market(alert.trade.mint);
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
market = null;
|
|
315
|
+
}
|
|
316
|
+
const g1 = gate();
|
|
317
|
+
if (g1)
|
|
318
|
+
return g1; // re-check after the await
|
|
319
|
+
const capUsd = Math.min(autoSizeCap(p), dailyLeft);
|
|
320
|
+
const r = buildMirror(alert, {
|
|
321
|
+
now: this.clock.now(), capUsd, k: p.k, market,
|
|
322
|
+
leaderSoldSince: this.trades ? leaderSoldSince(alert, this.trades(wallet)) : null,
|
|
323
|
+
leaderIntel: this.leaderIntel(alert.trade.mint, wallet),
|
|
324
|
+
});
|
|
325
|
+
if (!r.ok)
|
|
326
|
+
return this.settle(wallet, { kind: 'refused', reason: r.reason, message: r.message, planId: null, checks: r });
|
|
327
|
+
let planId = null;
|
|
328
|
+
try {
|
|
329
|
+
const runner = this.#runners.get(wallet);
|
|
330
|
+
if (!runner)
|
|
331
|
+
return refuse('no-authority', 'enable copy automation with a fresh physical gesture');
|
|
332
|
+
const authorized = runner(r.input, r.supply, alert.trade.sig.slice(0, 16));
|
|
333
|
+
const plan = authorized.plan;
|
|
334
|
+
planId = plan.id;
|
|
335
|
+
this.autoPlans.set(plan.id, { planId: plan.id, wallet, sizeUsd: plan.sizeUsd, at: this.clock.now(), armed: false, judged: false });
|
|
336
|
+
this.journal.append('copy.auto.proposed', { planId: plan.id, data: { wallet, sizeUsd: plan.sizeUsd, mint: plan.mint, ref: plan.source.ref } });
|
|
337
|
+
const g2 = gate();
|
|
338
|
+
if (g2) {
|
|
339
|
+
this.plans.cancelPlan(plan.id, 'copy auto: gate closed');
|
|
340
|
+
return g2;
|
|
341
|
+
}
|
|
342
|
+
// Only the engine-held operation for this new proposal can arm it. No proof leaves the engine.
|
|
343
|
+
authorized.arm();
|
|
344
|
+
const rec = this.autoPlans.get(plan.id);
|
|
345
|
+
rec.armed = true;
|
|
346
|
+
this.lastAt.set(wallet, this.clock.now());
|
|
347
|
+
this.journal.append('copy.auto.armed', { planId: plan.id, mandateId: p.mandateId, data: { wallet, sizeUsd: plan.sizeUsd, tier: AUTO_TIER, lagMs: r.lagMs, driftPct: r.driftPct } });
|
|
348
|
+
return this.settle(wallet, { kind: 'armed', planId: plan.id, sizeUsd: plan.sizeUsd, mandateId: p.mandateId });
|
|
349
|
+
}
|
|
350
|
+
catch (e) {
|
|
351
|
+
if (planId) {
|
|
352
|
+
try {
|
|
353
|
+
this.plans.cancelPlan(planId, 'copy auto: arm refused');
|
|
354
|
+
}
|
|
355
|
+
catch { /* ignore */ }
|
|
356
|
+
}
|
|
357
|
+
const reason = e instanceof PolicyRefusal || e instanceof PlanError || e instanceof MandateError || e instanceof ConfirmError ? e.reason : 'error';
|
|
358
|
+
return refuse(reason, e instanceof Error ? e.message : String(e), planId);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/** Judge settled auto plans for the disarm streak (a FAILED mirror counts; a done one resets). Call on a timer. */
|
|
362
|
+
tick() {
|
|
363
|
+
for (const r of this.autoPlans.values()) {
|
|
364
|
+
if (r.judged || !r.armed)
|
|
365
|
+
continue;
|
|
366
|
+
const s = this.planStatus(r.planId);
|
|
367
|
+
if (s === null || !TERMINAL.has(s))
|
|
368
|
+
continue;
|
|
369
|
+
r.judged = true;
|
|
370
|
+
const p = this.policies.get(r.wallet);
|
|
371
|
+
if (s === 'failed') {
|
|
372
|
+
const n = (this.streak.get(r.wallet) ?? 0) + 1;
|
|
373
|
+
this.streak.set(r.wallet, n);
|
|
374
|
+
this.journal.append('copy.auto.judged', { planId: r.planId, data: { wallet: r.wallet, status: s, streak: n } });
|
|
375
|
+
if (p?.enabled && n >= p.autoDisarmStreak)
|
|
376
|
+
this.disable(r.wallet, `auto-disarm: ${n} consecutive refused/failed mirrors (last: entry failed)`);
|
|
377
|
+
}
|
|
378
|
+
else if (s === 'done') {
|
|
379
|
+
this.streak.set(r.wallet, 0);
|
|
380
|
+
this.journal.append('copy.auto.judged', { planId: r.planId, data: { wallet: r.wallet, status: s, streak: 0 } });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
verifyJournal() { return this.journal.verify(); }
|
|
385
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// task-1360 Tier 1 — the FOLLOW LIST: wallets the user chose to watch, kept in a local JSON file
|
|
2
|
+
// (~/.dexterm/follows.json, mode 600). The label policy (research/wallet-intel design §4.3/§5) binds: a name is
|
|
3
|
+
// user-typed only (`by: 'user'`), never inferred from X/Telegram, never shared or exported, deletable in one
|
|
4
|
+
// command (`dexterm follow purge`). Names pass the prompt firewall on the way in: an instruction-like label is
|
|
5
|
+
// refused, every other label is control-stripped and clipped, so a wallet name can never carry a prompt to the
|
|
6
|
+
// analyst or an escape sequence to the terminal.
|
|
7
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
import { systemClock } from "../../clock.js";
|
|
11
|
+
import { firewallText } from "../../social/firewall.js";
|
|
12
|
+
const ADDR_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
13
|
+
export const FOLLOW_NAME_MAX = 24;
|
|
14
|
+
export const FOLLOWS_MAX = 50;
|
|
15
|
+
export function defaultFollowsPath(env = process.env) {
|
|
16
|
+
const home = env.DEXTERM_HOME && env.DEXTERM_HOME.length ? env.DEXTERM_HOME : join(homedir(), '.dexterm');
|
|
17
|
+
return join(home, 'follows.json');
|
|
18
|
+
}
|
|
19
|
+
export function isWalletAddress(s) { return typeof s === 'string' && ADDR_RE.test(s); }
|
|
20
|
+
/** Firewall + clip a user-typed wallet label. Returns null for an instruction-like or empty label. */
|
|
21
|
+
export function cleanFollowName(raw) {
|
|
22
|
+
if (raw === null || raw === undefined)
|
|
23
|
+
return { name: null, refused: null };
|
|
24
|
+
const r = firewallText(String(raw), { max: FOLLOW_NAME_MAX, keepUrls: false });
|
|
25
|
+
if (r.injection)
|
|
26
|
+
return { name: null, refused: 'label reads like an instruction; not stored' };
|
|
27
|
+
const name = r.text.trim().slice(0, FOLLOW_NAME_MAX);
|
|
28
|
+
return { name: name.length ? name : null, refused: null };
|
|
29
|
+
}
|
|
30
|
+
export class FollowStore {
|
|
31
|
+
path;
|
|
32
|
+
clock;
|
|
33
|
+
rows = [];
|
|
34
|
+
constructor(opts = {}) {
|
|
35
|
+
this.path = opts.path ?? null;
|
|
36
|
+
this.clock = opts.clock ?? systemClock;
|
|
37
|
+
if (this.path && existsSync(this.path))
|
|
38
|
+
this.load();
|
|
39
|
+
}
|
|
40
|
+
load() {
|
|
41
|
+
try {
|
|
42
|
+
const j = JSON.parse(readFileSync(this.path, 'utf8'));
|
|
43
|
+
this.rows = (Array.isArray(j.follows) ? j.follows : []).flatMap((x) => {
|
|
44
|
+
const r = x;
|
|
45
|
+
if (!isWalletAddress(r.addr))
|
|
46
|
+
return [];
|
|
47
|
+
return [{ addr: r.addr, name: cleanFollowName(r.name).name, since: typeof r.since === 'number' ? r.since : 0, tier: 'poll' }];
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
this.rows = [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
save() {
|
|
55
|
+
if (!this.path)
|
|
56
|
+
return;
|
|
57
|
+
mkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });
|
|
58
|
+
writeFileSync(this.path, JSON.stringify({ v: 1, follows: this.rows }, null, 2) + '\n', { mode: 0o600 });
|
|
59
|
+
try {
|
|
60
|
+
chmodSync(this.path, 0o600);
|
|
61
|
+
}
|
|
62
|
+
catch { /* best effort */ }
|
|
63
|
+
}
|
|
64
|
+
list() { return this.rows.map((r) => ({ ...r })); }
|
|
65
|
+
get(addr) { const r = this.rows.find((x) => x.addr === addr); return r ? { ...r } : null; }
|
|
66
|
+
has(addr) { return this.rows.some((x) => x.addr === addr); }
|
|
67
|
+
add(addr, name) {
|
|
68
|
+
if (!isWalletAddress(addr))
|
|
69
|
+
return { ok: false, reason: 'not a base58 wallet address' };
|
|
70
|
+
if (this.rows.some((x) => x.addr === addr))
|
|
71
|
+
return { ok: false, reason: 'already followed' };
|
|
72
|
+
if (this.rows.length >= FOLLOWS_MAX)
|
|
73
|
+
return { ok: false, reason: `at most ${FOLLOWS_MAX} followed wallets (the poll budget)` };
|
|
74
|
+
const n = cleanFollowName(name);
|
|
75
|
+
if (n.refused)
|
|
76
|
+
return { ok: false, reason: n.refused };
|
|
77
|
+
const follow = { addr, name: n.name, since: this.clock.now(), tier: 'poll' };
|
|
78
|
+
this.rows.push(follow);
|
|
79
|
+
this.save();
|
|
80
|
+
return { ok: true, follow: { ...follow } };
|
|
81
|
+
}
|
|
82
|
+
rename(addr, name) {
|
|
83
|
+
const r = this.rows.find((x) => x.addr === addr);
|
|
84
|
+
if (!r)
|
|
85
|
+
return { ok: false, reason: 'not followed' };
|
|
86
|
+
const n = cleanFollowName(name);
|
|
87
|
+
if (n.refused)
|
|
88
|
+
return { ok: false, reason: n.refused };
|
|
89
|
+
r.name = n.name;
|
|
90
|
+
this.save();
|
|
91
|
+
return { ok: true, follow: { ...r } };
|
|
92
|
+
}
|
|
93
|
+
remove(addr) {
|
|
94
|
+
const before = this.rows.length;
|
|
95
|
+
this.rows = this.rows.filter((x) => x.addr !== addr);
|
|
96
|
+
if (this.rows.length !== before)
|
|
97
|
+
this.save();
|
|
98
|
+
return this.rows.length !== before;
|
|
99
|
+
}
|
|
100
|
+
/** Delete every follow and label (the local-only, deletable label store the design requires). */
|
|
101
|
+
purge() { const n = this.rows.length; this.rows = []; this.save(); return n; }
|
|
102
|
+
}
|
|
103
|
+
export function shortAddr(a) { return a.length > 10 ? `${a.slice(0, 4)}…${a.slice(-4)}` : a; }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// task-1360 — COPY TRADING (staged): wallet tracker + alerts (Tier 1), the one-key mirror PROPOSAL (Tier 2), the
|
|
2
|
+
// capped auto-mirror copy mandate (Tier 3). Personal, self-directed, non-custodial; paper/devnet only; nothing
|
|
3
|
+
// here signs, holds a key, or arms anything without the plan engine's own keypress gate.
|
|
4
|
+
export { SOL_LEG_MIN_LAMPORTS, tokenDeltasForOwner, lamportsDeltaFor, decodeTx, decodeWalletTrades } from "./trades.js";
|
|
5
|
+
export { WIN_RATE_MIN_N, positionsOf, earlyEntryScore, walletStats } from "./stats.js";
|
|
6
|
+
export { FOLLOW_NAME_MAX, FOLLOWS_MAX, defaultFollowsPath, isWalletAddress, cleanFollowName, FollowStore, shortAddr } from "./follows.js";
|
|
7
|
+
export { WalletTracker, TRACKER_DEFAULTS } from "./tracker.js";
|
|
8
|
+
export { MIRROR_LIMITS, leaderFillPriceUsd, buildMirror, leaderSoldSince } from "./mirror.js";
|
|
9
|
+
export { AUTO_HARD, AUTO_DEFAULTS, AUTO_TIER, AUTO_SIZE_FRACTION, AUTO_GESTURE, autoSizeCap, CopyAutoError, defaultCopyJournalPath, draftPolicy, CopyAuto } from "./auto.js";
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// task-1360 Tier 2 — the one-key MIRROR TICKET (research/wallet-intel design §6 Stage 1). A followed wallet's
|
|
2
|
+
// BUY alert becomes a PROPOSED order plan sized by the user's own cap, never larger than k × the leader's size
|
|
3
|
+
// and never more than 1 % of the pool. The three numbers the user must see are computed here and refused on:
|
|
4
|
+
// LAG (now − the leader's block) > 30 s, DRIFT (price now vs the leader's fill) > +5 %, liquidity < $25k, token
|
|
5
|
+
// age < 60 s, the leader sold since, the leader is in the token's dev cluster or holds > 5 % (they are
|
|
6
|
+
// distributing to you). Unknown facts are shown as unknown, never guessed. The output is a PlanInput: an inert
|
|
7
|
+
// proposal for the plan engine, armed only through its own keypress / Phantom gate. Nothing here trades.
|
|
8
|
+
export const MIRROR_LIMITS = Object.freeze({
|
|
9
|
+
maxLagMs: 30_000,
|
|
10
|
+
maxDriftPct: 5,
|
|
11
|
+
minLiqUsd: 25_000,
|
|
12
|
+
minAgeMs: 60_000,
|
|
13
|
+
maxLeaderHoldPct: 5,
|
|
14
|
+
/** a mirror never takes more than this fraction of the pool's liquidity */
|
|
15
|
+
maxLiqFraction: 0.01,
|
|
16
|
+
/** a mirror proposal is time-sensitive: it expires fast (plan TTL) */
|
|
17
|
+
ttlMs: 10 * 60_000,
|
|
18
|
+
slippageBps: 300,
|
|
19
|
+
maxPriceImpactPct: 3,
|
|
20
|
+
});
|
|
21
|
+
const pct = (n) => `${n >= 0 ? '+' : ''}${n.toFixed(1)}%`;
|
|
22
|
+
const usd = (n) => (n >= 1000 ? `$${(n / 1000).toFixed(1)}k` : `$${n.toFixed(2)}`);
|
|
23
|
+
/** The leader's fill price in USD per whole token, from the alerted trade (null without decimals / USD). */
|
|
24
|
+
export function leaderFillPriceUsd(alert, decimals) {
|
|
25
|
+
const tr = alert.trade;
|
|
26
|
+
if (tr.usd === null || decimals === null)
|
|
27
|
+
return null;
|
|
28
|
+
const raw = Math.abs(Number(tr.tokenDelta));
|
|
29
|
+
if (!(raw > 0))
|
|
30
|
+
return null;
|
|
31
|
+
return tr.usd / (raw / 10 ** decimals);
|
|
32
|
+
}
|
|
33
|
+
/** Build the mirror proposal (or the refusal with every check listed). Pure. */
|
|
34
|
+
export function buildMirror(alert, ctx) {
|
|
35
|
+
const checks = [];
|
|
36
|
+
const tr = alert.trade;
|
|
37
|
+
const refuse = (reason, message) => ({ ok: false, reason, message, checks });
|
|
38
|
+
if (tr.side !== 'buy')
|
|
39
|
+
return refuse('not-a-buy', `only a leader BUY can be mirrored (this is a ${tr.side})`);
|
|
40
|
+
if (tr.usd === null)
|
|
41
|
+
return refuse('unpriced', 'the leader\'s fill has no USD value (token-swap leg or no SOL price); nothing to size from');
|
|
42
|
+
const m = ctx.market;
|
|
43
|
+
if (!m || m.priceUsd === null || !(m.priceUsd > 0))
|
|
44
|
+
return refuse('no-market', 'no price for this token yet (open it, or wait for the board)');
|
|
45
|
+
// lag
|
|
46
|
+
const lagMs = tr.tKnown ? Math.max(0, ctx.now - tr.t) : null;
|
|
47
|
+
checks.push({ name: 'lag', ok: lagMs === null ? null : lagMs <= MIRROR_LIMITS.maxLagMs, detail: lagMs === null ? 'block time unknown' : `${(lagMs / 1000).toFixed(0)} s since the leader\'s block (max ${MIRROR_LIMITS.maxLagMs / 1000} s)` });
|
|
48
|
+
// drift
|
|
49
|
+
const leaderPrice = leaderFillPriceUsd(alert, m.decimals);
|
|
50
|
+
const driftPct = leaderPrice === null ? null : ((m.priceUsd - leaderPrice) / leaderPrice) * 100;
|
|
51
|
+
const priceAge = m.priceAt === null ? null : Math.max(0, ctx.now - m.priceAt);
|
|
52
|
+
checks.push({ name: 'drift', ok: driftPct === null ? null : driftPct <= MIRROR_LIMITS.maxDriftPct, detail: driftPct === null ? (m.decimals === null ? 'token decimals unknown' : 'leader fill price unknown') : `${pct(driftPct)} vs the leader\'s fill $${leaderPrice.toPrecision(3)} (now $${m.priceUsd.toPrecision(3)}${priceAge !== null ? `, ${Math.round(priceAge / 1000)} s old` : ''}; max +${MIRROR_LIMITS.maxDriftPct}%)` });
|
|
53
|
+
// liquidity
|
|
54
|
+
checks.push({ name: 'liquidity', ok: m.liqUsd === null ? null : m.liqUsd >= MIRROR_LIMITS.minLiqUsd, detail: m.liqUsd === null ? 'liquidity unknown' : `${usd(m.liqUsd)} (min ${usd(MIRROR_LIMITS.minLiqUsd)})` });
|
|
55
|
+
// age
|
|
56
|
+
checks.push({ name: 'age', ok: m.ageMs === null ? null : m.ageMs >= MIRROR_LIMITS.minAgeMs, detail: m.ageMs === null ? 'pair age unknown' : `${Math.round(m.ageMs / 1000)} s old (min ${MIRROR_LIMITS.minAgeMs / 1000} s)` });
|
|
57
|
+
// leader sold since
|
|
58
|
+
checks.push({ name: 'leader-sold', ok: ctx.leaderSoldSince === null ? null : !ctx.leaderSoldSince, detail: ctx.leaderSoldSince === null ? 'unknown' : ctx.leaderSoldSince ? 'the leader already SOLD this mint since the buy' : 'the leader still holds (as far as fetched history shows)' });
|
|
59
|
+
// leader intel
|
|
60
|
+
const li = ctx.leaderIntel;
|
|
61
|
+
checks.push({ name: 'leader-dev', ok: li === null ? null : !li.inDevCluster, detail: li === null ? 'token not scanned (M on the token)' : li.inDevCluster ? 'the leader is linked to the dev cluster: they may be distributing to you' : 'not in the dev cluster' });
|
|
62
|
+
checks.push({ name: 'leader-holds', ok: li === null || li.holdsPct === null ? null : li.holdsPct <= MIRROR_LIMITS.maxLeaderHoldPct, detail: li === null || li.holdsPct === null ? 'leader\'s share unknown' : `${li.holdsPct.toFixed(1)}% of supply (max ${MIRROR_LIMITS.maxLeaderHoldPct}%)` });
|
|
63
|
+
for (const c of checks) {
|
|
64
|
+
if (c.ok === false) {
|
|
65
|
+
const reason = c.name;
|
|
66
|
+
return refuse(reason, `refused: ${c.name} — ${c.detail}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (m.decimals === null)
|
|
70
|
+
return refuse('no-decimals', 'token decimals unknown: open the token so its decimals and price are known, then mirror again');
|
|
71
|
+
// size = min(own cap, k × leader size, 1 % of liquidity)
|
|
72
|
+
const k = ctx.k && ctx.k > 0 ? ctx.k : 1;
|
|
73
|
+
const parts = [[ctx.capUsd, 'your cap'], [k * tr.usd, `${k}× the leader\'s ${usd(tr.usd)}`]];
|
|
74
|
+
if (m.liqUsd !== null)
|
|
75
|
+
parts.push([m.liqUsd * MIRROR_LIMITS.maxLiqFraction, '1% of liquidity']);
|
|
76
|
+
parts.sort((a, b) => a[0] - b[0]);
|
|
77
|
+
const sizeUsd = Math.floor(parts[0][0] * 100) / 100;
|
|
78
|
+
if (!(sizeUsd >= 0.01))
|
|
79
|
+
return refuse('cap', 'the mirror size rounds to nothing (raise your cap or wait for a bigger leader buy)');
|
|
80
|
+
const supply = m.mcapUsd !== null && m.mcapUsd > 0 ? m.mcapUsd / m.priceUsd : 1e9;
|
|
81
|
+
const input = {
|
|
82
|
+
mint: tr.mint, symbol: m.symbol ?? tr.mint.slice(0, 4), decimals: m.decimals, side: 'buy', sizeUsd,
|
|
83
|
+
entry: { kind: 'immediate' }, exits: [], stop: null,
|
|
84
|
+
slippageBps: MIRROR_LIMITS.slippageBps, maxPriceImpactPct: MIRROR_LIMITS.maxPriceImpactPct, ttlMs: MIRROR_LIMITS.ttlMs,
|
|
85
|
+
};
|
|
86
|
+
return { ok: true, sizeUsd, sizeWhy: `min(${parts.map((p) => `${p[1]} ${usd(p[0])}`).join(', ')})`, lagMs, driftPct, leaderPriceUsd: leaderPrice, checks, input, supply, ref: `mirror:${alert.wallet}:${tr.sig.slice(0, 16)}` };
|
|
87
|
+
}
|
|
88
|
+
/** Did the leader sell `mint` after the alerted buy, as far as the tracker's trades show? */
|
|
89
|
+
export function leaderSoldSince(alert, trades) {
|
|
90
|
+
return trades.some((t) => t.mint === alert.trade.mint && t.side === 'sell' && (t.slot > alert.trade.slot || (t.slot === alert.trade.slot && t.sig > alert.trade.sig)));
|
|
91
|
+
}
|