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,208 @@
|
|
|
1
|
+
// dexterm — pack WIDGETS in the app (task-1358 Tier 5). A pack widget is an html page the popup host serves in a
|
|
2
|
+
// loopback HUD window under a hash-only CSP (r2: no nonce — every inline block is allow-listed by sha256, nothing is
|
|
3
|
+
// substituted into the pack html), fed PUBLIC view models over the host's socket. This adapter is the whole of what a
|
|
4
|
+
// widget can see:
|
|
5
|
+
// * which widgets exist and are ENABLED (config.plugins['<pack>/<widget>'].enabled — the user's consent from
|
|
6
|
+
// `dexterm pack widget … --enable`; a pack cannot flip it);
|
|
7
|
+
// * the page bytes, read from the installed pack directory only after the pack re-hashes to its install record
|
|
8
|
+
// (a page edited after install is refused), never a symlink, never outside the pack, size-capped, and audited
|
|
9
|
+
// once more for loaders (<script src>, <link>, frames, <meta http-equiv>, @import, the __NONCE__ token);
|
|
10
|
+
// * the per-topic view models (rows · token · narrative · positions · alerts · theme) built from what the
|
|
11
|
+
// terminal already shows. No keys, no signer state, no caps, no mandates, no gate, no pair/source ids.
|
|
12
|
+
// Widgets receive; they cannot ask for anything (the only events a page may send are the generic close/focus/drag).
|
|
13
|
+
import { lstatSync, readFileSync, realpathSync, existsSync } from 'node:fs';
|
|
14
|
+
import { join, resolve, sep } from 'node:path';
|
|
15
|
+
import { checkIntegrity, packsDir, packWidgetKey, WIDGET_REF } from '../../custom/src/index.js';
|
|
16
|
+
import { WIDGET_HTML_MAX, auditWidgetHtml } from '../../popup/src/index.js';
|
|
17
|
+
import { popupThemeFrom } from "./popup.js";
|
|
18
|
+
const REF = new RegExp(WIDGET_REF);
|
|
19
|
+
/** Every widget of every installed pack, with its consent flag. */
|
|
20
|
+
export function installedWidgets(ui) {
|
|
21
|
+
return (ui?.packs?.widgets ?? []).map((w) => ({ key: packWidgetKey(w.pack, w.name), pack: w.pack, name: w.name, dir: w.dir, enabled: w.enabled, manifest: w.manifest }));
|
|
22
|
+
}
|
|
23
|
+
/** Split `<pack>/<widget>[/<id>]`; null when malformed. */
|
|
24
|
+
export function parseWidgetRef(ref) {
|
|
25
|
+
if (!REF.test(ref))
|
|
26
|
+
return null;
|
|
27
|
+
const [pack, name, id] = ref.split('/');
|
|
28
|
+
return { key: `${pack}/${name}`, pack, name, id: id ?? null };
|
|
29
|
+
}
|
|
30
|
+
export function widgetEnabled(ui, ref) {
|
|
31
|
+
const r = parseWidgetRef(ref);
|
|
32
|
+
if (!r)
|
|
33
|
+
return false;
|
|
34
|
+
return installedWidgets(ui).some((w) => w.key === r.key && w.enabled);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The page to serve for a widget ref, or a refusal. Consent (enabled), integrity (the installed pack still hashes to
|
|
38
|
+
* its record), containment (the page is a plain file under the widget directory) and size are all re-checked at
|
|
39
|
+
* open time, not trusted from startup.
|
|
40
|
+
*/
|
|
41
|
+
export function widgetPage(ui, ref) {
|
|
42
|
+
const r = parseWidgetRef(ref);
|
|
43
|
+
if (!r)
|
|
44
|
+
return { ok: false, reason: `bad widget ref "${ref}" (<pack>/<widget>[/<id>])` };
|
|
45
|
+
const w = installedWidgets(ui).find((x) => x.key === r.key);
|
|
46
|
+
if (!w)
|
|
47
|
+
return { ok: false, reason: `no installed widget ${r.key} (dexterm pack list)` };
|
|
48
|
+
if (!w.enabled)
|
|
49
|
+
return { ok: false, reason: `${r.key} is not enabled (dexterm pack widget ${r.pack} ${r.name} --enable)` };
|
|
50
|
+
const rec = (ui?.config.packs ?? []).find((p) => p.name === r.pack);
|
|
51
|
+
if (!rec || !ui)
|
|
52
|
+
return { ok: false, reason: `pack ${r.pack} has no install record` };
|
|
53
|
+
const packDir = join(packsDir(ui.home), `${rec.name}@${rec.version}`);
|
|
54
|
+
const integ = checkIntegrity(packDir);
|
|
55
|
+
if (!integ.ok || integ.hash !== rec.hash)
|
|
56
|
+
return { ok: false, reason: `pack ${r.pack} changed since install (${integ.ok ? 'hash differs' : integ.reason}); re-install it` };
|
|
57
|
+
const spec = r.id ? w.manifest.widgets.find((x) => x.id === r.id) : w.manifest.widgets[0];
|
|
58
|
+
if (!spec)
|
|
59
|
+
return { ok: false, reason: `${r.key} has no widget "${r.id}" (${w.manifest.widgets.map((x) => x.id).join(', ')})` };
|
|
60
|
+
const path = resolve(w.dir, spec.page);
|
|
61
|
+
let root;
|
|
62
|
+
try {
|
|
63
|
+
root = realpathSync(w.dir);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return { ok: false, reason: `${r.key}: directory missing` };
|
|
67
|
+
}
|
|
68
|
+
if (!existsSync(path) || lstatSync(path).isSymbolicLink())
|
|
69
|
+
return { ok: false, reason: `${r.key}: page ${spec.page} is missing or a symlink` };
|
|
70
|
+
const real = realpathSync(path);
|
|
71
|
+
if (!real.startsWith(root + sep) || !lstatSync(real).isFile())
|
|
72
|
+
return { ok: false, reason: `${r.key}: page ${spec.page} is outside the widget directory` };
|
|
73
|
+
const html = readFileSync(real, 'utf8');
|
|
74
|
+
if (html.length > WIDGET_HTML_MAX)
|
|
75
|
+
return { ok: false, reason: `${r.key}: page is ${html.length} chars (max ${WIDGET_HTML_MAX})` };
|
|
76
|
+
// r2: install refused loaders already; a pack installed before that check (or a hash-valid page nobody re-inspected)
|
|
77
|
+
// is refused here too, with the reason, before the host ever sees it
|
|
78
|
+
const problems = auditWidgetHtml(html);
|
|
79
|
+
if (problems.length)
|
|
80
|
+
return { ok: false, reason: `${r.key}: page ${spec.page} refused: ${problems.join('; ')}` };
|
|
81
|
+
return { ok: true, page: { key: r.id ? `${r.key}/${r.id}` : `${r.key}/${spec.id}`, title: spec.title, topics: [...spec.topics], html } };
|
|
82
|
+
}
|
|
83
|
+
// ------------------------------------------------------------------ view models (public columns only)
|
|
84
|
+
const FORBIDDEN_KEYS = /^(arm|armed|sign|signer|keys?|caps?|mandates?|gate|secret|token|apiKey|passphrase|seed|privateKey)$/i;
|
|
85
|
+
export function widgetRowsVM(state) {
|
|
86
|
+
const rows = [];
|
|
87
|
+
const seen = new Set();
|
|
88
|
+
for (const r of state.screener.rows) {
|
|
89
|
+
if (!r.mint || r.skeleton || seen.has(r.mint))
|
|
90
|
+
continue;
|
|
91
|
+
seen.add(r.mint);
|
|
92
|
+
rows.push({
|
|
93
|
+
symbol: r.symbol, name: r.name, mint: r.mint, launchpad: r.launchpad,
|
|
94
|
+
price: r.price, mcap: r.mcap, liq: r.liq, vol5m: r.vol5m, vol1h: r.vol1h, vol24: r.vol24,
|
|
95
|
+
chg: { ...r.chg }, ageMin: r.ageMin, holders: r.holders, org: r.org, top10: r.top10,
|
|
96
|
+
flags: r.flags.map((f) => String(f)), verified: r.verified,
|
|
97
|
+
});
|
|
98
|
+
if (rows.length >= 200)
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
return { feed: state.screener.preset, rows, updatedAt: state.now.getTime() };
|
|
102
|
+
}
|
|
103
|
+
export function widgetPositionsVM(state) {
|
|
104
|
+
const rows = [];
|
|
105
|
+
const live = state.live;
|
|
106
|
+
if (live) {
|
|
107
|
+
try {
|
|
108
|
+
for (const p of live.positions.view().rows)
|
|
109
|
+
rows.push({ symbol: p.symbol, mint: p.mint, side: 'long', tokens: p.tokens, valueSol: p.valueSol, unrealizedSol: p.unrealizedSol, unrealizedPct: p.unrealizedPct, book: 'paper' });
|
|
110
|
+
}
|
|
111
|
+
catch { /* no paper book on this run */ }
|
|
112
|
+
const lp = live.livePositions;
|
|
113
|
+
if (lp) {
|
|
114
|
+
try {
|
|
115
|
+
for (const p of lp.positions()) {
|
|
116
|
+
if (p.tokensRaw <= 0n)
|
|
117
|
+
continue;
|
|
118
|
+
const h = lp.held(p.mint);
|
|
119
|
+
rows.push({ symbol: p.symbol, mint: p.mint, side: 'long', tokens: h?.tokens ?? Number(p.tokensRaw) / 10 ** p.decimals, valueSol: null, unrealizedSol: null, unrealizedPct: null, book: 'live' });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch { /* ledger unreadable */ }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { rows, updatedAt: state.now.getTime() };
|
|
126
|
+
}
|
|
127
|
+
export function widgetAlertsVM(state, limit = 50) {
|
|
128
|
+
const items = [];
|
|
129
|
+
const now = state.now.getTime();
|
|
130
|
+
for (const l of state.rulesUi?.log ?? [])
|
|
131
|
+
if (l.kind === 'alert' || l.kind === 'proposal')
|
|
132
|
+
items.push({ at: l.at, level: l.level, kind: `rule:${l.kind}`, text: l.text, symbol: l.symbol || null });
|
|
133
|
+
const store = state.live?.watch.store;
|
|
134
|
+
if (store) {
|
|
135
|
+
try {
|
|
136
|
+
const symbolOf = (mint) => (mint ? state.screener.rows.find((r) => r.mint === mint)?.symbol ?? null : null);
|
|
137
|
+
for (const e of store.events(limit))
|
|
138
|
+
items.push({ at: e.t, level: e.level, kind: e.kind, text: e.text, symbol: symbolOf(e.mint) });
|
|
139
|
+
}
|
|
140
|
+
catch { /* store closed */ }
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
for (const a of state.fx.alerts)
|
|
144
|
+
items.push({ at: now - a.ago * 1000, level: a.level, kind: a.kind, text: a.text, symbol: null });
|
|
145
|
+
}
|
|
146
|
+
items.sort((a, b) => b.at - a.at);
|
|
147
|
+
return { items: items.slice(0, limit), updatedAt: now };
|
|
148
|
+
}
|
|
149
|
+
/** The payload for one topic right now, or null when that topic has nothing yet (token with no coin open, narrative before the board). */
|
|
150
|
+
export function widgetPayload(state, topic, deps = {}) {
|
|
151
|
+
switch (topic) {
|
|
152
|
+
case 'rows': return widgetRowsVM(state);
|
|
153
|
+
case 'token': return deps.token?.() ?? null;
|
|
154
|
+
case 'narrative': return deps.narrative?.() ?? null;
|
|
155
|
+
case 'positions': return widgetPositionsVM(state);
|
|
156
|
+
case 'alerts': return widgetAlertsVM(state);
|
|
157
|
+
case 'theme': return popupThemeFrom(state.theme);
|
|
158
|
+
default: return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** Defence in depth for tests and the sync path: no money-shaped key anywhere in a payload. */
|
|
162
|
+
export function payloadKeys(v, acc = []) {
|
|
163
|
+
if (v && typeof v === 'object')
|
|
164
|
+
for (const [k, x] of Object.entries(v)) {
|
|
165
|
+
acc.push(k);
|
|
166
|
+
payloadKeys(x, acc);
|
|
167
|
+
}
|
|
168
|
+
return acc;
|
|
169
|
+
}
|
|
170
|
+
export function payloadIsPublic(v) { return !payloadKeys(v).some((k) => FORBIDDEN_KEYS.test(k)); }
|
|
171
|
+
/** Minimum spacing between pushes of the same topic to the same widget (rows refresh every 10 s anyway). */
|
|
172
|
+
export const WIDGET_SYNC_MS = 1000;
|
|
173
|
+
/**
|
|
174
|
+
* Push fresh view models to every open widget (self-throttled per widget+topic). `force` ignores the throttle
|
|
175
|
+
* (first paint after `ready`, a theme change). Pure over the controller: a missing controller is a no-op.
|
|
176
|
+
*/
|
|
177
|
+
export function syncWidgetPopups(state, deps = {}, opts = {}) {
|
|
178
|
+
const p = state.popups;
|
|
179
|
+
if (!p?.openWidgets || !p.syncWidget)
|
|
180
|
+
return 0;
|
|
181
|
+
const open = p.openWidgets();
|
|
182
|
+
if (!open.length)
|
|
183
|
+
return 0;
|
|
184
|
+
const now = state.now.getTime();
|
|
185
|
+
const stamps = (state.widgetSync ??= new Map());
|
|
186
|
+
const cache = new Map();
|
|
187
|
+
let pushed = 0;
|
|
188
|
+
for (const w of open) {
|
|
189
|
+
if (opts.only && w.key !== opts.only)
|
|
190
|
+
continue;
|
|
191
|
+
for (const topic of w.topics) {
|
|
192
|
+
if (opts.topics && !opts.topics.includes(topic))
|
|
193
|
+
continue;
|
|
194
|
+
const stampKey = `${w.key}|${topic}`;
|
|
195
|
+
if (!opts.force && now - (stamps.get(stampKey) ?? -Infinity) < WIDGET_SYNC_MS)
|
|
196
|
+
continue;
|
|
197
|
+
if (!cache.has(topic))
|
|
198
|
+
cache.set(topic, widgetPayload(state, topic, deps));
|
|
199
|
+
const payload = cache.get(topic) ?? null;
|
|
200
|
+
if (!payload || !payloadIsPublic(payload))
|
|
201
|
+
continue;
|
|
202
|
+
p.syncWidget(w.key, topic, payload);
|
|
203
|
+
stamps.set(stampKey, now);
|
|
204
|
+
pushed++;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return pushed;
|
|
208
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// `dexterm workspace <list|show|remove|init> …` (task-1358 Tier 1): the workspace files under
|
|
2
|
+
// ~/.dexterm/workspaces/ (DEXTERM_HOME overrides). No network, no model, never touches a key file.
|
|
3
|
+
// list built-ins + files, with errors for files that do not validate
|
|
4
|
+
// show <name> print the workspace JSON (a built-in prints its generated form)
|
|
5
|
+
// remove <name> delete a user file (built-ins cannot be removed)
|
|
6
|
+
// init write `default` as an editable file with the shipped layout trees, if it does not exist
|
|
7
|
+
import { listWorkspaces, readWorkspace, removeWorkspace, writeWorkspace, dextermHome, builtinScreens, defaultWorkspace, workspacesDir } from '../../custom/src/index.js';
|
|
8
|
+
export const WORKSPACE_USAGE = `usage: dexterm workspace list | show <name> | remove <name> | init
|
|
9
|
+
files: ~/.dexterm/workspaces/<name>.json (DEXTERM_HOME overrides); in the app: ctrl+w then w/n/N/s/m/o/d/1-9, or --workspace <name>`;
|
|
10
|
+
export async function workspaceCommand(argv, io) {
|
|
11
|
+
const home = dextermHome(io.env);
|
|
12
|
+
const [cmd, name] = argv;
|
|
13
|
+
const bad = (s) => { io.stderr.write(`dexterm workspace: ${s}\n${WORKSPACE_USAGE}\n`); return 2; };
|
|
14
|
+
switch (cmd) {
|
|
15
|
+
case undefined:
|
|
16
|
+
case 'list': {
|
|
17
|
+
const list = listWorkspaces(home);
|
|
18
|
+
io.stdout.write(`${workspacesDir(home)}\n`);
|
|
19
|
+
for (const w of list) {
|
|
20
|
+
const tag = w.builtin ? 'built-in' : 'file';
|
|
21
|
+
const what = w.error ? `⚠ ${w.error}` : [w.workspace.description, w.workspace.start ? `starts on ${w.workspace.start}` : null, w.workspace.theme ? `theme ${w.workspace.theme}` : null, w.workspace.popups?.length ? `${w.workspace.popups.length} popup(s): ${w.workspace.popups.map((p) => p.kind).join(', ')}` : null].filter(Boolean).join(' · ');
|
|
22
|
+
io.stdout.write(` ${w.name.padEnd(24)} ${tag.padEnd(9)} ${what}\n`);
|
|
23
|
+
}
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
case 'show': {
|
|
27
|
+
if (!name)
|
|
28
|
+
return bad('show needs a name');
|
|
29
|
+
const ws = readWorkspace(home, name);
|
|
30
|
+
if (!ws) {
|
|
31
|
+
io.stderr.write(`dexterm workspace: no workspace "${name}" (see: dexterm workspace list)\n`);
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
io.stdout.write(JSON.stringify(ws, null, 2) + '\n');
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
case 'remove': {
|
|
38
|
+
if (!name)
|
|
39
|
+
return bad('remove needs a name');
|
|
40
|
+
const entry = listWorkspaces(home).find((w) => w.name === name);
|
|
41
|
+
if (!entry) {
|
|
42
|
+
io.stderr.write(`dexterm workspace: no workspace "${name}"\n`);
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
if (entry.builtin) {
|
|
46
|
+
io.stderr.write(`dexterm workspace: "${name}" is built in; it cannot be removed (save a file with that name to override it)\n`);
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
if (!removeWorkspace(home, name)) {
|
|
50
|
+
io.stderr.write(`dexterm workspace: could not remove ${entry.path}\n`);
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
io.stdout.write(`removed ${entry.path}\n`);
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
case 'init': {
|
|
57
|
+
const existing = listWorkspaces(home).find((w) => w.name === 'default' && !w.builtin);
|
|
58
|
+
if (existing) {
|
|
59
|
+
io.stdout.write(`${existing.path} already exists\n`);
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
const path = writeWorkspace(home, { ...defaultWorkspace(), description: 'The shipped layout as editable pane trees (dexterm workspace init)', screens: builtinScreens() });
|
|
63
|
+
io.stdout.write(`wrote ${path}\n edit the trees (weight / fixed / priority / collapseBelow), then ctrl+w d in the app or --workspace default\n`);
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
default: return bad(`unknown subcommand "${cmd}"`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// dexterm — `dexterm x <status|set-client-id|login|logout>` (task-1357, lane I-02). Login with X through
|
|
2
|
+
// OAuth 2.0 PKCE on the fixed loopback callback; each user brings their OWN X app Client ID (their developer
|
|
3
|
+
// account, their rate limits, their bill). Tokens live in ~/.dexterm/x/tokens.json (0600) and are never
|
|
4
|
+
// printed: `status` shows the handle, scopes, ages and file modes only. No `.write`/`dm.*` scope is ever
|
|
5
|
+
// requested; dexterm never posts.
|
|
6
|
+
import { XOAuth, XTokenStore, XAuthError, XApi, XSource, XSpendMeter, KolStore, X_SCOPES, callbackUri, redactXSecrets } from '../../data/src/index.js';
|
|
7
|
+
export const X_HELP = ` dexterm x status Login with X: client id stored? logged in as @handle? scopes, expiry, file modes
|
|
8
|
+
dexterm x set-client-id store YOUR X app's Client ID (prompted on the terminal, or one line on stdin with --stdin)
|
|
9
|
+
dexterm x login OAuth2 PKCE in your browser → tokens in ~/.dexterm/x/tokens.json (0600); read scopes only
|
|
10
|
+
dexterm x logout revoke both tokens at X and delete the local record`;
|
|
11
|
+
const fmtIn = (ms) => {
|
|
12
|
+
if (ms <= 0)
|
|
13
|
+
return 'expired';
|
|
14
|
+
const m = Math.round(ms / 60_000);
|
|
15
|
+
return m < 60 ? `${m} min` : `${Math.round(m / 6) / 10} h`;
|
|
16
|
+
};
|
|
17
|
+
/** Read one line from stdin (piped) or the TTY. Never echoes it back. */
|
|
18
|
+
function readLine(io, prompt) {
|
|
19
|
+
const stdin = io.stdin;
|
|
20
|
+
if (!stdin)
|
|
21
|
+
return Promise.resolve(null);
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
let buf = '';
|
|
24
|
+
let done = false;
|
|
25
|
+
const finish = (v) => { if (done)
|
|
26
|
+
return; done = true; try {
|
|
27
|
+
stdin.off?.('data', onData);
|
|
28
|
+
}
|
|
29
|
+
catch { /* ignore */ } try {
|
|
30
|
+
stdin.pause?.();
|
|
31
|
+
}
|
|
32
|
+
catch { /* ignore */ } resolve(v); };
|
|
33
|
+
const onData = (chunk) => { buf += chunk.toString(); const nl = buf.indexOf('\n'); if (nl >= 0)
|
|
34
|
+
finish(buf.slice(0, nl).trim()); };
|
|
35
|
+
if (stdin.isTTY)
|
|
36
|
+
io.stderr.write(prompt);
|
|
37
|
+
stdin.setEncoding?.('utf8');
|
|
38
|
+
stdin.on('data', onData);
|
|
39
|
+
stdin.once?.('end', () => finish(buf.trim() || null));
|
|
40
|
+
stdin.resume?.();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function printStatus(io, store, now) {
|
|
44
|
+
const meta = store.meta();
|
|
45
|
+
const modes = store.modes();
|
|
46
|
+
const clientId = store.clientId(io.env);
|
|
47
|
+
const oct = (m) => (m === null ? '—' : m.toString(8).padStart(3, '0'));
|
|
48
|
+
io.stdout.write(`client id: ${clientId ? `stored (${io.env.DEXTERM_X_CLIENT_ID ? 'DEXTERM_X_CLIENT_ID' : store.clientIdPath})` : 'missing — run `dexterm x set-client-id` (create a Native App with callback ' + callbackUri() + ' in your X developer portal)'}\n`);
|
|
49
|
+
if (!meta.loggedIn) {
|
|
50
|
+
io.stdout.write(`login: not logged in — run \`dexterm x login\`${clientId ? '' : ' after storing the client id'}\n`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
io.stdout.write(`login: ${meta.user ? `@${meta.user.username} (id ${meta.user.id})` : 'logged in (handle unknown)'} · scopes ${meta.scopes.join(' ') || '—'} · access token ${meta.expiresAt !== null ? (meta.expiresAt > now ? `valid ${fmtIn(meta.expiresAt - now)}` : 'expired (refreshes on next use)') : '?'} · refresh ${meta.hasRefresh ? 'yes' : 'no (login again)'}\n`);
|
|
54
|
+
}
|
|
55
|
+
io.stdout.write(`files: ${store.dir} (mode ${oct(modes.dir)}) · tokens.json ${modes.tokens === null ? 'absent' : `mode ${oct(modes.tokens)}`} · client-id ${modes.clientId === null ? 'absent' : `mode ${oct(modes.clientId)}`}\n`);
|
|
56
|
+
if ((modes.tokens !== null && modes.tokens !== 0o600) || (modes.dir !== null && modes.dir !== 0o700))
|
|
57
|
+
io.stderr.write('⚠ permissions are looser than 0600/0700; dexterm will tighten them on the next write (or chmod them now)\n');
|
|
58
|
+
io.stdout.write(`callback: ${callbackUri()} · tokens are never printed, logged, or sent to a model · X reads are user-context only (your rate limits, your bill)\n`);
|
|
59
|
+
}
|
|
60
|
+
/** Run the `x` subcommand. Returns a process exit code. */
|
|
61
|
+
export async function runX(argv, io, over = {}) {
|
|
62
|
+
const sub = argv[0] ?? 'status';
|
|
63
|
+
const store = over.store ?? new XTokenStore({ env: io.env });
|
|
64
|
+
const now = over.now ?? (() => Date.now());
|
|
65
|
+
if (sub === 'status') {
|
|
66
|
+
printStatus(io, store, now());
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
if (sub === 'set-client-id') {
|
|
70
|
+
const useStdin = argv.includes('--stdin');
|
|
71
|
+
if (!useStdin && !(io.stdin && io.stdin.isTTY)) {
|
|
72
|
+
io.stderr.write('dexterm x set-client-id: paste the id on a terminal, or pipe ONE line with --stdin (never put it on the command line)\n');
|
|
73
|
+
return 2;
|
|
74
|
+
}
|
|
75
|
+
const v = await readLine(io, 'paste your X app Client ID and press enter: ');
|
|
76
|
+
if (!v) {
|
|
77
|
+
io.stderr.write('dexterm x set-client-id: nothing read\n');
|
|
78
|
+
return 2;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
store.setClientId(v);
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
io.stderr.write(`dexterm x set-client-id: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
85
|
+
return 2;
|
|
86
|
+
}
|
|
87
|
+
io.stdout.write(`stored (${v.length} chars) at ${store.clientIdPath} (mode 600). Next: dexterm x login\n`);
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
if (sub === 'login') {
|
|
91
|
+
const oauth = new XOAuth({ store, endpoints: over.endpoints, open: over.open, port: over.port, loginTimeoutMs: over.loginTimeoutMs, now: over.now });
|
|
92
|
+
if (!store.clientId(io.env)) {
|
|
93
|
+
io.stderr.write(`dexterm x login: no client id stored. Create a Native App (OAuth 2.0, callback ${callbackUri(over.port)}) in your X developer portal, then run \`dexterm x set-client-id\`.\n`);
|
|
94
|
+
return 2;
|
|
95
|
+
}
|
|
96
|
+
io.stdout.write(`Opening your browser to X (scopes: ${X_SCOPES.join(' ')} — read only; dexterm never posts). Callback ${callbackUri(over.port)}.\n`);
|
|
97
|
+
const t = setTimeout(() => { if (oauth.lastAuthorizeUrl)
|
|
98
|
+
io.stderr.write(`If the browser did not open, paste this URL into it:\n${oauth.lastAuthorizeUrl}\n`); }, 4000);
|
|
99
|
+
t.unref?.();
|
|
100
|
+
try {
|
|
101
|
+
const res = await oauth.login({ env: io.env });
|
|
102
|
+
clearTimeout(t);
|
|
103
|
+
io.stdout.write(`✓ logged in${res.user ? ` as @${res.user.username}` : ''} · scopes ${res.scopes.join(' ')} · access token valid ${fmtIn(res.expiresAt - now())} · refresh ${res.hasRefresh ? 'yes' : 'no'} · browser ${Math.round(res.callbackMs / 100) / 10}s · exchange ${res.exchangeMs}ms\n`);
|
|
104
|
+
io.stdout.write(`tokens: ${store.tokensPath} (mode 600). The SOCIAL panel and TRENDING board now read X with your own limits.\n`);
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
clearTimeout(t);
|
|
109
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
110
|
+
io.stderr.write(`dexterm x login failed: ${redactXSecrets(msg)}\n`);
|
|
111
|
+
if (e instanceof XAuthError && e.kind === 'exchange' && /invalid_client/.test(msg))
|
|
112
|
+
io.stderr.write(' the Client ID is not one X recognises — check the app in your developer portal and re-run `dexterm x set-client-id`\n');
|
|
113
|
+
if (e instanceof XAuthError && e.kind === 'port-busy')
|
|
114
|
+
io.stderr.write(` port ${over.port ?? 41287} is in use; close the other login (or the process on that port) and retry\n`);
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (sub === 'logout') {
|
|
119
|
+
const oauth = new XOAuth({ store, endpoints: over.endpoints, open: over.open, port: over.port, now: over.now });
|
|
120
|
+
const out = await oauth.logout(io.env);
|
|
121
|
+
if (!out.hadTokens) {
|
|
122
|
+
io.stdout.write('not logged in; nothing to revoke.\n');
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
const parts = Object.entries(out.revoked).map(([k, s]) => `${k.replace('_token', '')} ${s === 200 ? 'revoked' : s === 0 ? 'unreachable' : `HTTP ${s}`}`);
|
|
126
|
+
io.stdout.write(`✓ logged out · ${parts.join(' · ') || 'nothing to revoke'} · local tokens deleted. You can also check x.com/settings/connected_apps.\n`);
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
io.stderr.write(`dexterm x: unknown subcommand ${JSON.stringify(sub)} (expected status | set-client-id | login | logout)\n`);
|
|
130
|
+
return 2;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* `dexterm kol import-x` (task-1357): the user's OWN X lists → `own-x-list` KOL lists (handles only). Needs a
|
|
134
|
+
* login; a degraded X (401/429/403/budget) is reported as text, never as an empty import.
|
|
135
|
+
*/
|
|
136
|
+
export async function importOwnXLists(opts) {
|
|
137
|
+
const store = opts.store ?? new XTokenStore({ env: opts.env });
|
|
138
|
+
if (!store.meta().loggedIn)
|
|
139
|
+
return { ok: false, error: 'not logged in to X — run `dexterm x login` first (your own X app, read scopes only)' };
|
|
140
|
+
const oauth = new XOAuth({ store, endpoints: opts.endpoints, now: opts.now });
|
|
141
|
+
const api = new XApi({ oauth, endpoints: opts.endpoints, cache: opts.cache, spend: new XSpendMeter({ cache: opts.cache, env: opts.env, now: opts.now }), env: opts.env });
|
|
142
|
+
const kol = new KolStore({ cache: opts.cache });
|
|
143
|
+
const x = new XSource({ api, store, kol, now: opts.now });
|
|
144
|
+
try {
|
|
145
|
+
const r = await x.importOwnLists();
|
|
146
|
+
return { ok: true, lists: r.lists };
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
return { ok: false, error: redactXSecrets(e instanceof Error ? e.message : String(e)) };
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
x.close();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// @dexterm/custom — the config loader (task-1358 Tier 1). ~/.dexterm/config.json is the canonical,
|
|
2
|
+
// machine-written config (0600, `version: 4`); workspaces are one JSON file each in ~/.dexterm/workspaces/,
|
|
3
|
+
// user themes in ~/.dexterm/themes/. THIS MODULE NEVER READS A KEY FILE: `<name>.key` files are the data
|
|
4
|
+
// layer's business, and the only thing the loader does with a key is move one OUT of an old config.
|
|
5
|
+
//
|
|
6
|
+
// Failure model: a missing config is the defaults (nothing is written until the user saves something); an
|
|
7
|
+
// unreadable or invalid config keeps the defaults and reports the error so the app can show a toast and
|
|
8
|
+
// `dexterm doctor` can name the file. Nothing here throws on I/O except `saveConfig` / `writeWorkspace`.
|
|
9
|
+
import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync, renameSync, unlinkSync, statSync, appendFileSync } from 'node:fs';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { createHash } from 'node:crypto';
|
|
13
|
+
import { CONFIG_VERSION, defaultConfig, migrate, validateConfig } from "./schema.js";
|
|
14
|
+
import { applyOps } from "./schema.js";
|
|
15
|
+
import { builtinWorkspaces, parseWorkspace, validateWorkspace } from "./workspace.js";
|
|
16
|
+
/** `DEXTERM_HOME` overrides `~/.dexterm` (tests point it at a temp dir). Same rule as the CLI's boot-state. */
|
|
17
|
+
export function dextermHome(env = process.env) {
|
|
18
|
+
return env.DEXTERM_HOME && env.DEXTERM_HOME.trim() ? env.DEXTERM_HOME : join(homedir(), '.dexterm');
|
|
19
|
+
}
|
|
20
|
+
export const configPath = (home) => join(home, 'config.json');
|
|
21
|
+
export const journalPath = (home) => join(home, 'config.journal.jsonl');
|
|
22
|
+
export const workspacesDir = (home) => join(home, 'workspaces');
|
|
23
|
+
export const packsDir = (home) => join(home, 'packs');
|
|
24
|
+
const sha = (s) => createHash('sha256').update(s).digest('hex');
|
|
25
|
+
/** Load (and migrate if needed) ~/.dexterm/config.json. Never throws. */
|
|
26
|
+
export function loadConfig(home) {
|
|
27
|
+
const path = configPath(home);
|
|
28
|
+
const base = { config: defaultConfig(), path, missing: false, ok: true, errors: [], notes: [], extractedKeyFiles: [], hash: sha('') };
|
|
29
|
+
let text;
|
|
30
|
+
try {
|
|
31
|
+
text = readFileSync(path, 'utf8');
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
if (e.code === 'ENOENT')
|
|
35
|
+
return { ...base, missing: true };
|
|
36
|
+
return { ...base, ok: false, errors: [`${path}: ${e.message}`] };
|
|
37
|
+
}
|
|
38
|
+
base.hash = sha(text);
|
|
39
|
+
let raw;
|
|
40
|
+
try {
|
|
41
|
+
raw = JSON.parse(text);
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
return { ...base, ok: false, errors: [`${path}: not valid JSON (${e.message}); keeping defaults`] };
|
|
45
|
+
}
|
|
46
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
47
|
+
return { ...base, ok: false, errors: [`${path}: expected an object; keeping defaults`] };
|
|
48
|
+
const version = Number(raw.version ?? 1);
|
|
49
|
+
if (version === CONFIG_VERSION) {
|
|
50
|
+
const errors = validateConfig(raw);
|
|
51
|
+
if (errors.length)
|
|
52
|
+
return { ...base, ok: false, errors: errors.slice(0, 5).map((e) => `${path}: ${e.path || '/'} ${e.msg}`) };
|
|
53
|
+
return { ...base, config: raw };
|
|
54
|
+
}
|
|
55
|
+
// older (or newer) config: migrate, back up, write the new one, write extracted keys to <name>.key (0600)
|
|
56
|
+
let m;
|
|
57
|
+
try {
|
|
58
|
+
m = migrate(raw);
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
return { ...base, ok: false, errors: [`${path}: ${e.message}`] };
|
|
62
|
+
}
|
|
63
|
+
if (m.errors.length)
|
|
64
|
+
return { ...base, ok: false, errors: m.errors.slice(0, 5).map((e) => `${path}: after migration ${e.path || '/'} ${e.msg}`) };
|
|
65
|
+
const out = { ...base, config: m.config, notes: [...m.notes] };
|
|
66
|
+
try {
|
|
67
|
+
writeFileSync(`${path}.bak-v${m.from}`, text, { mode: 0o600 });
|
|
68
|
+
for (const [name, value] of Object.entries(m.extractedKeys)) {
|
|
69
|
+
const kf = join(home, `${name}.key`);
|
|
70
|
+
if (!existsSync(kf)) {
|
|
71
|
+
writeFileSync(kf, value + '\n', { mode: 0o600 });
|
|
72
|
+
out.extractedKeyFiles.push(`${name}.key`);
|
|
73
|
+
}
|
|
74
|
+
else
|
|
75
|
+
out.notes.push(`${name}.key already exists; the key in config.json was dropped, not copied`);
|
|
76
|
+
}
|
|
77
|
+
saveConfig(home, m.config);
|
|
78
|
+
out.notes.push(`migrated config.json v${m.from} → v${CONFIG_VERSION} (backup: config.json.bak-v${m.from})`);
|
|
79
|
+
out.hash = sha(readFileSync(path, 'utf8'));
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
out.ok = false;
|
|
83
|
+
out.errors.push(`${path}: migration could not be written (${e.message}); using the migrated config for this run only`);
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
/** Validate + write config.json atomically (0600, directory 0700). Throws on a schema error or I/O failure. */
|
|
88
|
+
export function saveConfig(home, config) {
|
|
89
|
+
const errors = validateConfig(config);
|
|
90
|
+
if (errors.length)
|
|
91
|
+
throw new Error(`config invalid: ${errors[0].path || '/'} ${errors[0].msg}`);
|
|
92
|
+
mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
93
|
+
const path = configPath(home);
|
|
94
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
95
|
+
writeFileSync(tmp, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
96
|
+
renameSync(tmp, path);
|
|
97
|
+
}
|
|
98
|
+
export function readJournal(home) {
|
|
99
|
+
try {
|
|
100
|
+
return readFileSync(journalPath(home), 'utf8').split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Apply ops to the stored config, journal them with the inverse, and save. Throws (and writes nothing) on a bad op or schema error. */
|
|
107
|
+
export function applyAndJournal(home, ops, meta) {
|
|
108
|
+
const loaded = loadConfig(home);
|
|
109
|
+
if (!loaded.ok)
|
|
110
|
+
throw new Error(loaded.errors[0]);
|
|
111
|
+
const { doc, inverse } = applyOps(loaded.config, ops);
|
|
112
|
+
const errors = validateConfig(doc);
|
|
113
|
+
if (errors.length)
|
|
114
|
+
throw new Error(`refused: ${errors[0].path || '/'} ${errors[0].msg}`);
|
|
115
|
+
saveConfig(home, doc);
|
|
116
|
+
const after = sha(readFileSync(configPath(home), 'utf8'));
|
|
117
|
+
const seq = (readJournal(home).at(-1)?.seq ?? 0) + 1;
|
|
118
|
+
const entry = { seq, at: meta.now ?? Date.now(), kind: meta.kind ?? 'apply', actor: meta.actor, why: meta.why, ops, inverse, hashBefore: loaded.hash, hashAfter: after };
|
|
119
|
+
appendFileSync(journalPath(home), JSON.stringify(entry) + '\n', { mode: 0o600 });
|
|
120
|
+
return { config: doc, entry };
|
|
121
|
+
}
|
|
122
|
+
/** Undo the last applied/redone change if the config has not been edited by hand since. */
|
|
123
|
+
export function undoLast(home, now = Date.now()) {
|
|
124
|
+
const j = readJournal(home);
|
|
125
|
+
// the last entry that can be undone: walk back skipping already-undone applies
|
|
126
|
+
let depth = 0;
|
|
127
|
+
for (let i = j.length - 1; i >= 0; i--) {
|
|
128
|
+
const e = j[i];
|
|
129
|
+
if (e.kind === 'undo') {
|
|
130
|
+
depth++;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (depth > 0) {
|
|
134
|
+
depth--;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const current = loadConfig(home);
|
|
138
|
+
if (current.hash !== e.hashAfter)
|
|
139
|
+
return { ok: false, reason: 'cannot undo: config.json changed since (edited by hand?)' };
|
|
140
|
+
try {
|
|
141
|
+
const { entry } = applyAndJournal(home, e.inverse, { actor: 'user', why: `undo #${e.seq}`, kind: 'undo', now });
|
|
142
|
+
return { ok: true, entry };
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
return { ok: false, reason: `cannot undo: ${err.message}` };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { ok: false, reason: 'nothing to undo' };
|
|
149
|
+
}
|
|
150
|
+
// ------------------------------------------------------------------ workspaces on disk
|
|
151
|
+
export const WS_FILE = /^[a-z][a-z0-9-]{0,39}\.json$/;
|
|
152
|
+
/** Built-ins first (default, morning-hud), then the user's files; a user file with a built-in's name REPLACES it. Bad files are listed with their error. */
|
|
153
|
+
export function listWorkspaces(home) {
|
|
154
|
+
const out = builtinWorkspaces().map((w) => ({ name: w.name, path: null, workspace: w, builtin: true }));
|
|
155
|
+
const dir = workspacesDir(home);
|
|
156
|
+
let files = [];
|
|
157
|
+
try {
|
|
158
|
+
files = readdirSync(dir).filter((f) => WS_FILE.test(f)).sort();
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
for (const f of files) {
|
|
164
|
+
const path = join(dir, f);
|
|
165
|
+
const name = f.slice(0, -5);
|
|
166
|
+
try {
|
|
167
|
+
if (statSync(path).isSymbolicLink())
|
|
168
|
+
continue;
|
|
169
|
+
const ws = parseWorkspace(readFileSync(path, 'utf8'), f);
|
|
170
|
+
if (ws.name !== name)
|
|
171
|
+
throw new Error(`${f}: "name" must be "${name}" (the file name)`);
|
|
172
|
+
const i = out.findIndex((w) => w.name === name);
|
|
173
|
+
const entry = { name, path, workspace: ws, builtin: false };
|
|
174
|
+
if (i >= 0)
|
|
175
|
+
out[i] = entry;
|
|
176
|
+
else
|
|
177
|
+
out.push(entry);
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
out.push({ name, path, workspace: { name }, builtin: false, error: e.message });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
export function readWorkspace(home, name) {
|
|
186
|
+
return listWorkspaces(home).find((w) => w.name === name && !w.error)?.workspace ?? null;
|
|
187
|
+
}
|
|
188
|
+
/** Write a workspace file (validated, 0600). Throws on a bad document or name. */
|
|
189
|
+
export function writeWorkspace(home, ws) {
|
|
190
|
+
const errors = validateWorkspace(ws);
|
|
191
|
+
if (errors.length)
|
|
192
|
+
throw new Error(`workspace invalid: ${errors[0].path || '/'} ${errors[0].msg}`);
|
|
193
|
+
if (ws.name.includes('/'))
|
|
194
|
+
throw new Error('a user workspace name cannot contain "/" (reserved for packs)');
|
|
195
|
+
mkdirSync(workspacesDir(home), { recursive: true, mode: 0o700 });
|
|
196
|
+
const path = join(workspacesDir(home), `${ws.name}.json`);
|
|
197
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
198
|
+
writeFileSync(tmp, JSON.stringify(ws, null, 2) + '\n', { mode: 0o600 });
|
|
199
|
+
renameSync(tmp, path);
|
|
200
|
+
return path;
|
|
201
|
+
}
|
|
202
|
+
export function removeWorkspace(home, name) {
|
|
203
|
+
if (!/^[a-z][a-z0-9-]{0,39}$/.test(name))
|
|
204
|
+
return false;
|
|
205
|
+
const path = join(workspacesDir(home), `${name}.json`);
|
|
206
|
+
try {
|
|
207
|
+
unlinkSync(path);
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// @dexterm/custom — public surface (task-1358).
|
|
2
|
+
export * from "./schema.js";
|
|
3
|
+
export * from "./layout.js";
|
|
4
|
+
export * from "./workspace.js";
|
|
5
|
+
export * from "./config.js";
|
|
6
|
+
export * from "./themes.js";
|
|
7
|
+
export * from "./keymap.js";
|
|
8
|
+
export * from "./rules.js";
|
|
9
|
+
export * from "./packs.js";
|
|
10
|
+
export * from "./widget-html.js";
|