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,515 @@
|
|
|
1
|
+
// @dexterm/custom — config schema, validation, JSON-patch ops (with inverses for undo), the actor policy
|
|
2
|
+
// (which paths a non-user actor may touch) and versioned migrations (task-1358, from the task-1288 design).
|
|
3
|
+
//
|
|
4
|
+
// Zero dependencies: the validator is a tiny hand-written subset of JSON Schema so the app keeps its
|
|
5
|
+
// "no runtime deps" rule. KEYS NEVER APPEAR IN THIS SCHEMA: there is no `keys` / `signer` / `trading`
|
|
6
|
+
// section at all (additional:false makes an attempt to add one a precise refusal), and the v1 migration
|
|
7
|
+
// demonstrates moving a key OUT of the config file into `~/.dexterm/<name>.key`.
|
|
8
|
+
export const CONFIG_VERSION = 4;
|
|
9
|
+
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
10
|
+
/** Validate `value` against a schema (a node or the name of one in `refs`); returns the error list. */
|
|
11
|
+
export function validate(schema, value, path = '', errors = [], refs = SCHEMAS) {
|
|
12
|
+
let S = schema;
|
|
13
|
+
for (let hops = 0; typeof S === 'string' && hops < 8; hops++)
|
|
14
|
+
S = refs[S];
|
|
15
|
+
if (!S || typeof S === 'string') {
|
|
16
|
+
errors.push({ path, msg: `unknown schema ${String(schema)}` });
|
|
17
|
+
return errors;
|
|
18
|
+
}
|
|
19
|
+
if ('oneOf' in S) {
|
|
20
|
+
const attempts = S.oneOf.map((s) => validate(s, value, path, [], refs));
|
|
21
|
+
if (!attempts.some((e) => e.length === 0))
|
|
22
|
+
errors.push({ path, msg: `matches none of ${S.oneOf.length} shapes: ${attempts.map((e) => e[0]?.msg).join(' | ')}` });
|
|
23
|
+
return errors;
|
|
24
|
+
}
|
|
25
|
+
switch (S.type) {
|
|
26
|
+
case 'object': {
|
|
27
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
28
|
+
errors.push({ path, msg: 'expected object' });
|
|
29
|
+
return errors;
|
|
30
|
+
}
|
|
31
|
+
const obj = value;
|
|
32
|
+
for (const k of S.required ?? [])
|
|
33
|
+
if (!(k in obj))
|
|
34
|
+
errors.push({ path: `${path}/${k}`, msg: 'required' });
|
|
35
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
36
|
+
if (FORBIDDEN_KEYS.has(k)) {
|
|
37
|
+
errors.push({ path: `${path}/${k}`, msg: 'forbidden key' });
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const sub = S.props && Object.hasOwn(S.props, k) ? S.props[k] : S.values;
|
|
41
|
+
if (!sub) {
|
|
42
|
+
if (S.additional === false)
|
|
43
|
+
errors.push({ path: `${path}/${k}`, msg: 'unknown key' });
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
validate(sub, v, `${path}/${k}`, errors, refs);
|
|
47
|
+
}
|
|
48
|
+
return errors;
|
|
49
|
+
}
|
|
50
|
+
case 'array': {
|
|
51
|
+
if (!Array.isArray(value)) {
|
|
52
|
+
errors.push({ path, msg: 'expected array' });
|
|
53
|
+
return errors;
|
|
54
|
+
}
|
|
55
|
+
if (S.max !== undefined && value.length > S.max)
|
|
56
|
+
errors.push({ path, msg: `more than ${S.max} items` });
|
|
57
|
+
value.forEach((v, i) => validate(S.items, v, `${path}/${i}`, errors, refs));
|
|
58
|
+
return errors;
|
|
59
|
+
}
|
|
60
|
+
case 'string': {
|
|
61
|
+
if (typeof value !== 'string') {
|
|
62
|
+
errors.push({ path, msg: 'expected string' });
|
|
63
|
+
return errors;
|
|
64
|
+
}
|
|
65
|
+
if (S.enum && !S.enum.includes(value))
|
|
66
|
+
errors.push({ path, msg: `expected one of ${S.enum.join(', ')}` });
|
|
67
|
+
if (S.pattern && !new RegExp(S.pattern).test(value))
|
|
68
|
+
errors.push({ path, msg: `does not match ${S.pattern}` });
|
|
69
|
+
if (S.max !== undefined && value.length > S.max)
|
|
70
|
+
errors.push({ path, msg: `longer than ${S.max}` });
|
|
71
|
+
return errors;
|
|
72
|
+
}
|
|
73
|
+
case 'number': {
|
|
74
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
75
|
+
errors.push({ path, msg: 'expected finite number' });
|
|
76
|
+
return errors;
|
|
77
|
+
}
|
|
78
|
+
if (S.int && !Number.isInteger(value))
|
|
79
|
+
errors.push({ path, msg: 'expected integer' });
|
|
80
|
+
if (S.min !== undefined && value < S.min)
|
|
81
|
+
errors.push({ path, msg: `below ${S.min}` });
|
|
82
|
+
if (S.max !== undefined && value > S.max)
|
|
83
|
+
errors.push({ path, msg: `above ${S.max}` });
|
|
84
|
+
return errors;
|
|
85
|
+
}
|
|
86
|
+
case 'boolean':
|
|
87
|
+
if (typeof value !== 'boolean')
|
|
88
|
+
errors.push({ path, msg: 'expected boolean' });
|
|
89
|
+
return errors;
|
|
90
|
+
case 'null':
|
|
91
|
+
if (value !== null)
|
|
92
|
+
errors.push({ path, msg: 'expected null' });
|
|
93
|
+
return errors;
|
|
94
|
+
}
|
|
95
|
+
return errors;
|
|
96
|
+
}
|
|
97
|
+
export const HEX = '^#[0-9a-fA-F]{6}$';
|
|
98
|
+
export const NAME = '^[a-z][a-z0-9-]{0,39}$';
|
|
99
|
+
export const WS_NAME = '^[a-z][a-z0-9-]{0,39}(/[a-z][a-z0-9-]{0,39})?$';
|
|
100
|
+
export const MINT = '^[1-9A-HJ-NP-Za-km-z]{32,44}$';
|
|
101
|
+
/** A key id as `Keymap.keyId()` produces it, optionally a two-key chord ("ctrl+w s"). */
|
|
102
|
+
export const KEY = '^((ctrl|alt|shift)\\+)*([a-zA-Z0-9]|[-=\\[\\]\\\\;\',./`~!@#$%^&*()_+{}|:"<>?]|enter|esc|tab|backspace|delete|space|up|down|left|right|home|end|pageup|pagedown|f[1-9]|f1[0-2])( ((ctrl|alt|shift)\\+)*([a-zA-Z0-9]|enter|esc|tab|space|up|down|left|right))?$';
|
|
103
|
+
/** Theme colour tokens (SPEC §7.1 + VISUAL-V2 §2) and the four ramps. A user theme may override any subset. */
|
|
104
|
+
export const THEME_TOKENS = ['bg', 'panel', 'panelAlt', 'border', 'borderFocus', 'text', 'muted', 'dim', 'accent', 'accentSoft', 'up', 'upSoft', 'down', 'downSoft', 'warn', 'warnSoft', 'ai', 'aiSoft', 'pump', 'dbc', 'raydium', 'meteora', 'bonk', 'selBg', 'headBg', 'tabActiveBg', 'sparkUp', 'sparkDown', 'magenta', 'cyan', 'coral', 'acid', 'glow', 'scan'];
|
|
105
|
+
export const THEME_RAMPS = ['heat', 'rain', 'rainPump', 'rainDump'];
|
|
106
|
+
export const BASE_THEMES = ['cyberspace', 'midnight', 'ember', 'paper'];
|
|
107
|
+
/** Built-in widget ids a pane may show. Plugin widgets are `plugin:<pkg>/<id>` and must be declared in a manifest. */
|
|
108
|
+
export const BUILTIN_WIDGETS = {
|
|
109
|
+
'screener.table': { title: 'SCREENER', min: { w: 60, h: 8 } },
|
|
110
|
+
'screener.spotlight': { title: 'SPOTLIGHT', min: { w: 36, h: 12 } },
|
|
111
|
+
'token.chart': { title: 'CHART', min: { w: 40, h: 8 } },
|
|
112
|
+
'token.onchain': { title: 'ON-CHAIN', min: { w: 28, h: 10 } },
|
|
113
|
+
'token.holders': { title: 'HOLDERS', min: { w: 36, h: 8 } },
|
|
114
|
+
'token.flow': { title: 'BUY/SELL FLOW', min: { w: 36, h: 6 } },
|
|
115
|
+
'token.tape': { title: 'TAPE', min: { w: 36, h: 6 } },
|
|
116
|
+
'analyst.panel': { title: '✦ ANALYST', min: { w: 38, h: 10 } },
|
|
117
|
+
'analyst.context': { title: 'CONTEXT', min: { w: 30, h: 10 } },
|
|
118
|
+
'newpairs.table': { title: 'NEW PAIRS', min: { w: 60, h: 8 } },
|
|
119
|
+
'newpairs.spotlight': { title: 'PAIR SPOTLIGHT', min: { w: 36, h: 10 } },
|
|
120
|
+
'watchlist.table': { title: 'WATCHLIST', min: { w: 60, h: 6 } },
|
|
121
|
+
'alerts.log': { title: 'ALERTS', min: { w: 40, h: 5 } },
|
|
122
|
+
'analyst.digest': { title: '✦ ANALYST DIGEST', min: { w: 40, h: 6 } },
|
|
123
|
+
'narrative.board': { title: 'NARRATIVES', min: { w: 44, h: 8 } },
|
|
124
|
+
'positions.table': { title: 'POSITIONS', min: { w: 50, h: 5 } },
|
|
125
|
+
'kol.feed': { title: 'KOL FEED', min: { w: 40, h: 6 } },
|
|
126
|
+
'rain.zone': { title: '', min: { w: 1, h: 1 } },
|
|
127
|
+
};
|
|
128
|
+
const WIDGET_ID = '^([a-z]+\\.[a-z-]+|plugin:[a-z][a-z0-9-]*/[a-z][a-z0-9-]*)$';
|
|
129
|
+
/** Popup kinds a workspace may open: exactly the popup host's kinds (task-1290/1340/1355/1356). */
|
|
130
|
+
export const POPUP_KINDS = ['chart', 'token', 'feeds', 'bubbles', 'roster', 'narrative', 'plans', 'widget'];
|
|
131
|
+
/** task-1358 Tier 5: the topics a pack widget may subscribe to (mirrors @dexterm/popup WIDGET_TOPICS; the cli tests assert they match). */
|
|
132
|
+
export const WIDGET_TOPICS = ['rows', 'token', 'narrative', 'positions', 'alerts', 'theme'];
|
|
133
|
+
/** A workspace popup of kind `widget` names an installed pack widget: `<pack>/<widget>[/<id>]`. */
|
|
134
|
+
export const WIDGET_REF = '^[a-z][a-z0-9-]{0,39}/[a-z][a-z0-9-]{0,39}(/[a-z][a-z0-9-]{0,39})?$';
|
|
135
|
+
export const WORKSPACE_SCREENS = ['screener', 'token', 'newpairs', 'watchlist', 'analyst', 'positions', 'narrative', 'plans', 'trending'];
|
|
136
|
+
export const LAYOUT_SCREENS = ['screener', 'token', 'newpairs', 'watchlist', 'analyst', 'positions'];
|
|
137
|
+
/**
|
|
138
|
+
* task-1366: how token prices are DISPLAYED. `price` (default) = USD per token; `mcap` = market cap (price × supply,
|
|
139
|
+
* supply from the data layer). A display mode only: the same data underneath, nothing that trades reads it.
|
|
140
|
+
*/
|
|
141
|
+
export const PRICE_MODES = ['price', 'mcap'];
|
|
142
|
+
export const DEFAULT_PRICE_MODE = 'price';
|
|
143
|
+
const tokenProps = () => Object.fromEntries([...THEME_TOKENS.map((t) => [t, 'hex']), ...THEME_RAMPS.map((t) => [t, 'ramp'])]);
|
|
144
|
+
export const SCHEMAS = {
|
|
145
|
+
hex: { type: 'string', pattern: HEX },
|
|
146
|
+
ramp: { type: 'array', items: 'hex', max: 4 },
|
|
147
|
+
breakpoint: { type: 'string', enum: ['compact', 'standard', 'wide'] },
|
|
148
|
+
layoutNode: {
|
|
149
|
+
oneOf: [
|
|
150
|
+
{ type: 'object', additional: false, required: ['kind', 'widget'], props: {
|
|
151
|
+
kind: { type: 'string', enum: ['pane'] },
|
|
152
|
+
widget: { type: 'string', pattern: WIDGET_ID },
|
|
153
|
+
/** 0 never drops; higher numbers drop first when the rect cannot satisfy minimums (same rule as table columns). */
|
|
154
|
+
priority: { type: 'number', int: true, min: 0, max: 9 },
|
|
155
|
+
collapseBelow: 'breakpoint',
|
|
156
|
+
title: { type: 'string', max: 40 },
|
|
157
|
+
/** Widget-specific settings the widget validates itself; plain JSON only. */
|
|
158
|
+
settings: { type: 'object', additional: true },
|
|
159
|
+
} },
|
|
160
|
+
{ type: 'object', additional: false, required: ['kind', 'dir', 'children'], props: {
|
|
161
|
+
kind: { type: 'string', enum: ['split'] },
|
|
162
|
+
dir: { type: 'string', enum: ['row', 'col'] },
|
|
163
|
+
children: { type: 'array', max: 8, items: { type: 'object', additional: false, required: ['node'], props: {
|
|
164
|
+
node: 'layoutNode',
|
|
165
|
+
weight: { type: 'number', min: 0.05, max: 100 },
|
|
166
|
+
fixed: { type: 'number', int: true, min: 1, max: 400 },
|
|
167
|
+
} } },
|
|
168
|
+
gutter: { type: 'boolean' },
|
|
169
|
+
} },
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
popupSpec: { type: 'object', additional: false, required: ['kind'], props: {
|
|
173
|
+
kind: { type: 'string', enum: POPUP_KINDS },
|
|
174
|
+
/** task-1358 Tier 5: for kind `widget`, the installed pack widget to open (`<pack>/<widget>[/<id>]`; must be enabled). */
|
|
175
|
+
widget: { type: 'string', pattern: WIDGET_REF },
|
|
176
|
+
/** 'selected' binds to the row under the cursor at open time; a mint pins the popup. */
|
|
177
|
+
mint: { type: 'string', pattern: '^(selected|[1-9A-HJ-NP-Za-km-z]{32,44})$' },
|
|
178
|
+
x: { type: 'number', min: 0, max: 1 }, y: { type: 'number', min: 0, max: 1 }, w: { type: 'number', min: 0.05, max: 1 }, h: { type: 'number', min: 0.05, max: 1 },
|
|
179
|
+
ontop: { type: 'boolean' }, clickthrough: { type: 'boolean' },
|
|
180
|
+
} },
|
|
181
|
+
workspace: { type: 'object', additional: false, required: ['name'], props: {
|
|
182
|
+
/** Pack workspaces are namespaced `<pack>/<name>`. */
|
|
183
|
+
name: { type: 'string', pattern: WS_NAME },
|
|
184
|
+
description: { type: 'string', max: 200 },
|
|
185
|
+
/** Per screen either one tree (with collapseBelow on panes) or a tree per breakpoint. */
|
|
186
|
+
screens: { type: 'object', additional: false, props: Object.fromEntries(LAYOUT_SCREENS.map((s) => [s, 'screenLayout'])) },
|
|
187
|
+
/** HUD windows this workspace opens (task-1290 popups), e.g. "show me everything". */
|
|
188
|
+
popups: { type: 'array', max: 12, items: 'popupSpec' },
|
|
189
|
+
/** Screen to land on when the workspace is activated. */
|
|
190
|
+
start: { type: 'string', enum: WORKSPACE_SCREENS },
|
|
191
|
+
/** Preset / filter / sort the screener opens with. */
|
|
192
|
+
screener: { type: 'object', additional: false, props: { preset: { type: 'string', pattern: NAME }, filter: { type: 'string', max: 200 }, sort: { type: 'string', pattern: '^[a-z0-9]+(:(asc|desc))?$' } } },
|
|
193
|
+
/** Theme name (built-in or user) and effects intensity the workspace switches to. */
|
|
194
|
+
theme: { type: 'string', pattern: NAME },
|
|
195
|
+
intensity: { type: 'string', enum: ['off', 'normal', 'fever'] },
|
|
196
|
+
/** task-1366: the price display mode the workspace switches to (overrides config.display.priceMode while active). */
|
|
197
|
+
priceMode: { type: 'string', enum: PRICE_MODES },
|
|
198
|
+
/** Token-screen panel toggles. */
|
|
199
|
+
token: { type: 'object', additional: false, props: { tape: { type: 'boolean' }, social: { type: 'boolean' }, analyst: { type: 'boolean' } } },
|
|
200
|
+
} },
|
|
201
|
+
screenLayout: { oneOf: ['layoutNode', { type: 'object', additional: false, props: { wide: 'layoutNode', standard: 'layoutNode', compact: 'layoutNode' } }] },
|
|
202
|
+
ruleJson: { type: 'object', additional: false, required: ['id', 'when', 'then'], props: {
|
|
203
|
+
id: { type: 'string', pattern: NAME },
|
|
204
|
+
name: { type: 'string', max: 80 },
|
|
205
|
+
enabled: { type: 'boolean' },
|
|
206
|
+
/** The condition in the rule grammar (parsed, never eval'd). */
|
|
207
|
+
when: { type: 'string', max: 400 },
|
|
208
|
+
for: { type: 'string', pattern: '^[0-9]+ ?(s|m|h|ticks?)$' },
|
|
209
|
+
then: { type: 'array', max: 6, items: { type: 'object', additional: false, required: ['action'], props: {
|
|
210
|
+
action: { type: 'string', enum: ['alert', 'ticket', 'ask', 'popup', 'tag', 'sound', 'watch'] },
|
|
211
|
+
args: { type: 'object', additional: true },
|
|
212
|
+
} } },
|
|
213
|
+
scope: { type: 'string', pattern: '^(screener|watchlist|newpairs|positions|mint:[1-9A-HJ-NP-Za-km-z]{32,44})$' },
|
|
214
|
+
cooldown: { type: 'string', pattern: '^[0-9]+ ?(s|m|h)$' },
|
|
215
|
+
maxPerHour: { type: 'number', int: true, min: 1, max: 120 },
|
|
216
|
+
window: { type: 'string', pattern: '^[0-9]+ ?(s|m|h)$' },
|
|
217
|
+
source: { type: 'string', pattern: '^(user|pack:[a-z][a-z0-9-]*|jarvis|plugin:[a-z][a-z0-9-]*)$' },
|
|
218
|
+
} },
|
|
219
|
+
userTheme: { type: 'object', additional: false, required: ['extends'], props: {
|
|
220
|
+
name: { type: 'string', pattern: NAME },
|
|
221
|
+
extends: { type: 'string', enum: BASE_THEMES },
|
|
222
|
+
tokens: { type: 'object', additional: false, props: { ...tokenProps(), logo: { type: 'array', items: 'hex', max: 3 } } },
|
|
223
|
+
} },
|
|
224
|
+
keymap: { type: 'object', additional: false, props: {
|
|
225
|
+
global: { type: 'object', values: { type: 'array', max: 4, items: { type: 'string', pattern: KEY } } },
|
|
226
|
+
screens: { type: 'object', values: { type: 'object', values: { type: 'array', max: 4, items: { type: 'string', pattern: KEY } } } },
|
|
227
|
+
} },
|
|
228
|
+
config: { type: 'object', additional: false, required: ['version'], props: {
|
|
229
|
+
version: { type: 'number', int: true, min: CONFIG_VERSION, max: CONFIG_VERSION },
|
|
230
|
+
theme: { type: 'object', additional: false, props: {
|
|
231
|
+
name: { type: 'string', pattern: NAME },
|
|
232
|
+
overrides: { type: 'object', additional: false, props: tokenProps() },
|
|
233
|
+
user: { type: 'object', values: 'userTheme' },
|
|
234
|
+
} },
|
|
235
|
+
fx: { type: 'object', additional: false, props: { intensity: { type: 'string', enum: ['off', 'normal', 'fever'] }, sound: { type: 'boolean' } } },
|
|
236
|
+
/** task-1366: display options. `priceMode` price | mcap (the `$` key toggles and persists it). */
|
|
237
|
+
display: { type: 'object', additional: false, props: { priceMode: { type: 'string', enum: PRICE_MODES } } },
|
|
238
|
+
layout: { type: 'object', additional: false, props: {
|
|
239
|
+
workspace: { type: 'string', pattern: WS_NAME },
|
|
240
|
+
workspaces: { type: 'object', values: 'workspace' },
|
|
241
|
+
} },
|
|
242
|
+
keymap: 'keymap',
|
|
243
|
+
rules: { type: 'array', max: 200, items: 'ruleJson' },
|
|
244
|
+
plugins: { type: 'object', values: { type: 'object', additional: false, props: {
|
|
245
|
+
enabled: { type: 'boolean' },
|
|
246
|
+
/** Capabilities the USER granted (⊆ manifest). Only the user may edit this (see DENIED_FOR_MODELS). */
|
|
247
|
+
capabilities: { type: 'array', max: 20, items: 'capability' },
|
|
248
|
+
settings: { type: 'object', additional: true },
|
|
249
|
+
} } },
|
|
250
|
+
packs: { type: 'array', max: 50, items: { type: 'object', additional: false, required: ['name', 'version', 'hash', 'trust'], props: {
|
|
251
|
+
name: { type: 'string', pattern: NAME }, version: { type: 'string', pattern: '^[0-9]+\\.[0-9]+\\.[0-9]+$' },
|
|
252
|
+
source: { type: 'string', max: 200 }, hash: { type: 'string', pattern: '^sha256:[0-9a-f]{64}$' },
|
|
253
|
+
trust: { type: 'string', enum: ['reviewed', 'signed', 'unsigned'] }, publisher: { type: 'string', max: 80 }, installedAt: { type: 'number' },
|
|
254
|
+
} } },
|
|
255
|
+
jarvis: { type: 'object', additional: false, props: {
|
|
256
|
+
configAssistant: { type: 'boolean' },
|
|
257
|
+
proposalTtlS: { type: 'number', int: true, min: 10, max: 600 },
|
|
258
|
+
/** Reserved: the trading gate is NOT here (signer policy owns it); listed so a proposal that tries gets a precise refusal. */
|
|
259
|
+
gate: { type: 'string', enum: ['keypress'] },
|
|
260
|
+
} },
|
|
261
|
+
scan: { type: 'object', additional: true },
|
|
262
|
+
popups: { type: 'object', additional: false, props: { rain: { type: 'object', additional: false, props: { enabled: { type: 'boolean' }, speedScale: { type: 'number', min: 0, max: 4 }, density: { type: 'number', min: 0, max: 1 } } } } },
|
|
263
|
+
/** Forward-compat bag: unknown sections from a newer dexterm survive a downgrade here. */
|
|
264
|
+
'x-unknown': { type: 'object', additional: true },
|
|
265
|
+
} },
|
|
266
|
+
capability: { type: 'string', enum: [
|
|
267
|
+
'rows.read', 'token.read', 'candles.read', 'holders.read', 'watchlist.read', 'alerts.read', 'config.read',
|
|
268
|
+
'watchlist.annotate', 'alerts.propose', 'ticket.propose', 'ask.propose',
|
|
269
|
+
'net.fetch', 'storage.kv', 'ui.column', 'ui.panel', 'ui.toast', 'ui.popup',
|
|
270
|
+
] },
|
|
271
|
+
widgetManifest: { type: 'object', additional: false, required: ['name', 'version', 'api', 'widgets'], props: {
|
|
272
|
+
name: { type: 'string', pattern: NAME }, version: { type: 'string', pattern: '^[0-9]+\\.[0-9]+\\.[0-9]+$' },
|
|
273
|
+
api: { type: 'number', int: true, min: 1, max: 1 },
|
|
274
|
+
description: { type: 'string', max: 200 },
|
|
275
|
+
/** A popup widget is DATA (an html page served by the popup host under its own id) + a view-model contract; never an entry script the terminal runs. */
|
|
276
|
+
widgets: { type: 'array', max: 8, items: { type: 'object', additional: false, required: ['id', 'title', 'page', 'topics'], props: {
|
|
277
|
+
id: { type: 'string', pattern: NAME }, title: { type: 'string', max: 30 },
|
|
278
|
+
page: { type: 'string', pattern: '^[a-z0-9-]+\\.html$' },
|
|
279
|
+
topics: { type: 'array', max: 6, items: { type: 'string', enum: WIDGET_TOPICS } },
|
|
280
|
+
min: { type: 'object', additional: false, props: { w: { type: 'number', int: true, min: 1 }, h: { type: 'number', int: true, min: 1 } } },
|
|
281
|
+
} } },
|
|
282
|
+
} },
|
|
283
|
+
packManifest: { type: 'object', additional: false, required: ['name', 'version', 'publisher', 'contents'], props: {
|
|
284
|
+
name: { type: 'string', pattern: NAME }, version: { type: 'string', pattern: '^[0-9]+\\.[0-9]+\\.[0-9]+$' },
|
|
285
|
+
publisher: { type: 'string', max: 80 }, description: { type: 'string', max: 300 },
|
|
286
|
+
requires: { type: 'object', additional: false, props: { dexterm: { type: 'string', max: 20 }, api: { type: 'number', int: true } } },
|
|
287
|
+
contents: { type: 'object', additional: false, props: {
|
|
288
|
+
workspaces: { type: 'array', max: 10, items: { type: 'string', pattern: '^[a-z0-9/_-]+\\.json$' } },
|
|
289
|
+
themes: { type: 'array', max: 10, items: { type: 'string', pattern: '^[a-z0-9/_-]+\\.json$' } },
|
|
290
|
+
rules: { type: 'array', max: 10, items: { type: 'string', pattern: '^[a-z0-9/_-]+\\.(rules|json)$' } },
|
|
291
|
+
keymap: { type: 'string', pattern: '^[a-z0-9/_-]+\\.json$' },
|
|
292
|
+
widgets: { type: 'array', max: 10, items: { type: 'string', pattern: '^[a-z0-9/_-]+$' } },
|
|
293
|
+
} },
|
|
294
|
+
} },
|
|
295
|
+
};
|
|
296
|
+
export function validateConfig(cfg) { return validate('config', cfg); }
|
|
297
|
+
export function defaultConfig() {
|
|
298
|
+
return {
|
|
299
|
+
version: CONFIG_VERSION,
|
|
300
|
+
theme: { name: 'cyberspace', overrides: {}, user: {} },
|
|
301
|
+
fx: { intensity: 'fever', sound: false },
|
|
302
|
+
display: { priceMode: DEFAULT_PRICE_MODE },
|
|
303
|
+
layout: { workspace: 'default', workspaces: {} },
|
|
304
|
+
keymap: { global: {}, screens: {} },
|
|
305
|
+
rules: [],
|
|
306
|
+
plugins: {},
|
|
307
|
+
packs: [],
|
|
308
|
+
jarvis: { configAssistant: true, proposalTtlS: 60 },
|
|
309
|
+
popups: { rain: { enabled: true, speedScale: 1, density: 0.6 } },
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
export function parsePointer(p) {
|
|
313
|
+
if (p === '')
|
|
314
|
+
return [];
|
|
315
|
+
if (!p.startsWith('/'))
|
|
316
|
+
throw new Error(`bad pointer ${p}`);
|
|
317
|
+
return p.slice(1).split('/').map((s) => s.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
318
|
+
}
|
|
319
|
+
const esc = (s) => s.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
320
|
+
export function getAt(doc, pointer) {
|
|
321
|
+
let cur = doc;
|
|
322
|
+
for (const seg of parsePointer(pointer)) {
|
|
323
|
+
if (cur === null || typeof cur !== 'object')
|
|
324
|
+
return undefined;
|
|
325
|
+
if (FORBIDDEN_KEYS.has(seg))
|
|
326
|
+
return undefined;
|
|
327
|
+
cur = Array.isArray(cur) ? cur[Number(seg)] : (Object.hasOwn(cur, seg) ? cur[seg] : undefined);
|
|
328
|
+
}
|
|
329
|
+
return cur;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Apply ops to a deep copy; returns { doc, inverse } where `inverse` reverts the change (for undo).
|
|
333
|
+
* Supported: add, replace, remove, test. Throws on a bad op; nothing is partially applied.
|
|
334
|
+
*/
|
|
335
|
+
export function applyOps(doc, ops) {
|
|
336
|
+
const out = structuredClone(doc);
|
|
337
|
+
const inverse = [];
|
|
338
|
+
for (const op of ops) {
|
|
339
|
+
const segs = parsePointer(op.path);
|
|
340
|
+
if (segs.some((s) => FORBIDDEN_KEYS.has(s)))
|
|
341
|
+
throw new Error(`forbidden segment in ${op.path}`);
|
|
342
|
+
if (op.op === 'test') {
|
|
343
|
+
if (JSON.stringify(getAt(out, op.path)) !== JSON.stringify(op.value))
|
|
344
|
+
throw new Error(`test failed at ${op.path}`);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (segs.length === 0)
|
|
348
|
+
throw new Error('root ops are not allowed');
|
|
349
|
+
const parentPath = '/' + segs.slice(0, -1).map(esc).join('/');
|
|
350
|
+
const parent = (segs.length === 1 ? out : getAt(out, parentPath));
|
|
351
|
+
const key = segs[segs.length - 1];
|
|
352
|
+
if (parent === undefined || parent === null || typeof parent !== 'object')
|
|
353
|
+
throw new Error(`no parent for ${op.path}`);
|
|
354
|
+
if (Array.isArray(parent)) {
|
|
355
|
+
const idx = key === '-' ? parent.length : Number(key);
|
|
356
|
+
if (!Number.isInteger(idx) || idx < 0 || idx > parent.length)
|
|
357
|
+
throw new Error(`bad index ${op.path}`);
|
|
358
|
+
if (op.op === 'add') {
|
|
359
|
+
parent.splice(idx, 0, structuredClone(op.value));
|
|
360
|
+
inverse.unshift({ op: 'remove', path: `${parentPath}/${idx}` });
|
|
361
|
+
}
|
|
362
|
+
else if (op.op === 'replace') {
|
|
363
|
+
if (idx >= parent.length)
|
|
364
|
+
throw new Error(`replace past end ${op.path}`);
|
|
365
|
+
inverse.unshift({ op: 'replace', path: op.path, value: structuredClone(parent[idx]) });
|
|
366
|
+
parent[idx] = structuredClone(op.value);
|
|
367
|
+
}
|
|
368
|
+
else if (op.op === 'remove') {
|
|
369
|
+
if (idx >= parent.length)
|
|
370
|
+
throw new Error(`remove past end ${op.path}`);
|
|
371
|
+
inverse.unshift({ op: 'add', path: op.path, value: structuredClone(parent[idx]) });
|
|
372
|
+
parent.splice(idx, 1);
|
|
373
|
+
}
|
|
374
|
+
else
|
|
375
|
+
throw new Error(`unknown op ${String(op.op)}`);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const obj = parent;
|
|
379
|
+
const had = Object.hasOwn(obj, key);
|
|
380
|
+
if (op.op === 'add' || op.op === 'replace') {
|
|
381
|
+
if (op.op === 'replace' && !had)
|
|
382
|
+
throw new Error(`replace of missing ${op.path}`);
|
|
383
|
+
inverse.unshift(had ? { op: 'replace', path: op.path, value: structuredClone(obj[key]) } : { op: 'remove', path: op.path });
|
|
384
|
+
obj[key] = structuredClone(op.value);
|
|
385
|
+
}
|
|
386
|
+
else if (op.op === 'remove') {
|
|
387
|
+
if (!had)
|
|
388
|
+
throw new Error(`remove of missing ${op.path}`);
|
|
389
|
+
inverse.unshift({ op: 'add', path: op.path, value: structuredClone(obj[key]) });
|
|
390
|
+
delete obj[key];
|
|
391
|
+
}
|
|
392
|
+
else
|
|
393
|
+
throw new Error(`unknown op ${String(op.op)}`);
|
|
394
|
+
}
|
|
395
|
+
return { doc: out, inverse };
|
|
396
|
+
}
|
|
397
|
+
// ------------------------------------------------------------------ actor policy
|
|
398
|
+
/**
|
|
399
|
+
* Paths that only the USER (keyboard / editing the file) may change. Jarvis, Claude, plugins, packs and
|
|
400
|
+
* agents get a precise refusal. Money-safety sections are not in the config at all (signer policy file,
|
|
401
|
+
* passphrase-gated); they are listed so an attempt to ADD them is refused by name, not by "unknown key".
|
|
402
|
+
*/
|
|
403
|
+
export const DENIED_FOR_MODELS = [
|
|
404
|
+
/^\/signer(\/|$)/, /^\/trading(\/|$)/, /^\/keys(\/|$)/, /^\/caps(\/|$)/, /^\/mandates?(\/|$)/, /^\/wallet(\/|$)/,
|
|
405
|
+
/^\/packs(\/|$)/, /^\/plugins\/[^/]+\/(capabilities|enabled)(\/|$)/, /^\/plugins\/[^/]+$/, /^\/plugins$/,
|
|
406
|
+
/^\/jarvis\/gate(\/|$)/, /^\/jarvis\/configAssistant$/, /^\/voice(\/|$)/, /^\/agents(\/|$)/, /^\/version$/,
|
|
407
|
+
/^\/rules\/(\d+|-)\/then\/(\d+|-)\/args\/(arm|armed|auto|autoArm|confirm|execute|sign|send)(\/|$)/,
|
|
408
|
+
];
|
|
409
|
+
/** Keys a model-authored VALUE may never contain at any depth (money/secret-shaped), whatever the path. */
|
|
410
|
+
export const DENIED_VALUE_KEYS = /^(arm|armed|auto|autoArm|autoConfirm|execute|sign|send|caps?|mandates?|gate|secret|keys?|apiKey|token|password|passphrase|privateKey|seed)$/i;
|
|
411
|
+
export const MAX_OPS_PER_PROPOSAL = 20;
|
|
412
|
+
function findDeniedKey(v, path = '') {
|
|
413
|
+
if (!v || typeof v !== 'object')
|
|
414
|
+
return null;
|
|
415
|
+
for (const [k, x] of Object.entries(v)) {
|
|
416
|
+
if (DENIED_VALUE_KEYS.test(k))
|
|
417
|
+
return `${path}/${k}`;
|
|
418
|
+
const hit = findDeniedKey(x, `${path}/${k}`);
|
|
419
|
+
if (hit)
|
|
420
|
+
return hit;
|
|
421
|
+
}
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
/** Policy for a batch of ops from `actor`: non-user actors are refused on denied paths and money-shaped values. */
|
|
425
|
+
export function policyCheck(ops, actor) {
|
|
426
|
+
const errors = [];
|
|
427
|
+
if (!Array.isArray(ops) || ops.length === 0) {
|
|
428
|
+
errors.push({ path: '', msg: 'no ops' });
|
|
429
|
+
return errors;
|
|
430
|
+
}
|
|
431
|
+
if (ops.length > MAX_OPS_PER_PROPOSAL)
|
|
432
|
+
errors.push({ path: '', msg: `more than ${MAX_OPS_PER_PROPOSAL} ops` });
|
|
433
|
+
for (const raw of ops) {
|
|
434
|
+
const op = raw;
|
|
435
|
+
if (typeof op?.path !== 'string') {
|
|
436
|
+
errors.push({ path: '', msg: 'op without path' });
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (!['add', 'replace', 'remove', 'test'].includes(String(op.op)))
|
|
440
|
+
errors.push({ path: op.path, msg: `op ${String(op.op)} not allowed` });
|
|
441
|
+
if (actor !== 'user') {
|
|
442
|
+
if (DENIED_FOR_MODELS.some((re) => re.test(op.path)))
|
|
443
|
+
errors.push({ path: op.path, msg: `only the user may change this (${actor} refused)` });
|
|
444
|
+
const hit = findDeniedKey(op.value);
|
|
445
|
+
if (hit)
|
|
446
|
+
errors.push({ path: op.path + hit, msg: `only the user may change this (${actor} refused: value carries "${hit.split('/').pop()}")` });
|
|
447
|
+
}
|
|
448
|
+
if (JSON.stringify(op.value ?? null).length > 20_000)
|
|
449
|
+
errors.push({ path: op.path, msg: 'value too large' });
|
|
450
|
+
}
|
|
451
|
+
return errors;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Each step takes the previous shape and returns the next. Steps never touch disk; the loader writes the
|
|
455
|
+
* backup and the extracted key files. `notes` tell the user what changed.
|
|
456
|
+
*/
|
|
457
|
+
export const MIGRATIONS = {
|
|
458
|
+
// v1 (SPEC §2.2 era sketch): { theme: 'midnight', keys: { helius: '…' }, alerts: [...] }
|
|
459
|
+
1: (c, out) => {
|
|
460
|
+
const next = { version: 2, theme: { name: typeof c.theme === 'string' ? c.theme : 'cyberspace', overrides: c.themeOverrides ?? {}, user: {} }, alerts: c.alerts ?? [] };
|
|
461
|
+
if (c.keys && typeof c.keys === 'object') {
|
|
462
|
+
// Keys leave the config file for good. The loader writes each to ~/.dexterm/<name>.key (0600).
|
|
463
|
+
for (const [name, value] of Object.entries(c.keys))
|
|
464
|
+
if (typeof value === 'string' && value && /^[a-z][a-z0-9-]{0,30}$/.test(name))
|
|
465
|
+
out.extractedKeys[name] = value;
|
|
466
|
+
out.notes.push(`moved ${Object.keys(out.extractedKeys).length} key(s) out of config.json into ~/.dexterm/<name>.key (0600); config.json no longer holds secrets`);
|
|
467
|
+
}
|
|
468
|
+
for (const k of Object.keys(c))
|
|
469
|
+
if (!['theme', 'themeOverrides', 'keys', 'alerts', 'version'].includes(k))
|
|
470
|
+
(next['x-unknown'] ??= {})[k] = c[k];
|
|
471
|
+
return next;
|
|
472
|
+
},
|
|
473
|
+
// v2 → v3: flat alert rules {kind, op, value, mint} become rules-engine rules.
|
|
474
|
+
2: (c, out) => {
|
|
475
|
+
const alerts = Array.isArray(c.alerts) ? c.alerts : [];
|
|
476
|
+
const rules = alerts.map((a, i) => ({
|
|
477
|
+
id: `alert-${i + 1}`, name: `${a.kind} ${a.op} ${a.value}`, enabled: a.status !== 'dismissed',
|
|
478
|
+
when: `${a.kind} ${a.op} ${a.value}`, then: [{ action: 'alert', args: { text: `${a.kind} ${a.op} ${a.value}` } }],
|
|
479
|
+
scope: typeof a.mint === 'string' ? `mint:${a.mint}` : 'watchlist', cooldown: '10m', source: 'user',
|
|
480
|
+
}));
|
|
481
|
+
if (rules.length)
|
|
482
|
+
out.notes.push(`converted ${rules.length} alert(s) into rules (edit them on the Rules screen)`);
|
|
483
|
+
const next = { ...defaultConfig(), version: 3, theme: c.theme ?? defaultConfig().theme, rules };
|
|
484
|
+
delete next.display; // added by the v3 → v4 step, like every later section
|
|
485
|
+
if (c['x-unknown'])
|
|
486
|
+
next['x-unknown'] = c['x-unknown'];
|
|
487
|
+
return next;
|
|
488
|
+
},
|
|
489
|
+
// v3 → v4 (task-1366): a `display` section with the price display mode. Nothing else changes; an existing
|
|
490
|
+
// `display` (hand-written into a v3 file) is kept when it is valid, otherwise the default is used.
|
|
491
|
+
3: (c, out) => {
|
|
492
|
+
const prior = (c.display && typeof c.display === 'object' ? c.display.priceMode : undefined);
|
|
493
|
+
const priceMode = PRICE_MODES.includes(prior) ? prior : DEFAULT_PRICE_MODE;
|
|
494
|
+
out.notes.push(`added display.priceMode = ${priceMode} (press $ in the app to show market cap instead of price)`);
|
|
495
|
+
return { ...c, version: 4, display: { priceMode } };
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
export function migrate(raw) {
|
|
499
|
+
const out = { notes: [], extractedKeys: {}, from: Number(raw?.version ?? 1), to: CONFIG_VERSION };
|
|
500
|
+
let cur = raw && typeof raw === 'object' ? structuredClone(raw) : {};
|
|
501
|
+
let v = out.from;
|
|
502
|
+
if (!Number.isInteger(v) || v < 1)
|
|
503
|
+
v = 1;
|
|
504
|
+
if (v > CONFIG_VERSION)
|
|
505
|
+
throw new Error(`config version ${v} is newer than this dexterm (${CONFIG_VERSION}); upgrade dexterm or restore config.json.bak`);
|
|
506
|
+
while (v < CONFIG_VERSION) {
|
|
507
|
+
const step = MIGRATIONS[v];
|
|
508
|
+
if (!step)
|
|
509
|
+
throw new Error(`no migration from v${v}`);
|
|
510
|
+
cur = step(cur, out);
|
|
511
|
+
v = Number(cur.version);
|
|
512
|
+
}
|
|
513
|
+
const errors = validateConfig(cur);
|
|
514
|
+
return { config: cur, errors, ...out };
|
|
515
|
+
}
|