dexterm-cli 0.0.0-stage → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/dist/packages/agents/src/audit.js +122 -0
- package/dist/packages/agents/src/bus.js +394 -0
- package/dist/packages/agents/src/executor.js +202 -0
- package/dist/packages/agents/src/gate.js +179 -0
- package/dist/packages/agents/src/identity.js +218 -0
- package/dist/packages/agents/src/index.js +15 -0
- package/dist/packages/agents/src/proposals.js +153 -0
- package/dist/packages/agents/src/schema.js +87 -0
- package/dist/packages/agents/src/sdk.js +140 -0
- package/dist/packages/agents/src/tiers.js +119 -0
- package/dist/packages/agents/src/tools.js +72 -0
- package/dist/packages/agents/src/ws.js +158 -0
- package/dist/packages/cli/assets/claude/analyst-system.md +76 -0
- package/dist/packages/cli/assets/claude/plugin/.claude-plugin/plugin.json +5 -0
- package/dist/packages/cli/assets/claude/plugin/skills/REGISTRY.json +16 -0
- package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +75 -0
- package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +74 -0
- package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +63 -0
- package/dist/packages/cli/assets/claude/plugin/skills/trench-scan/SKILL.md +44 -0
- package/dist/packages/cli/assets/claude/schemas/larp.schema.json +117 -0
- package/dist/packages/cli/assets/claude/schemas/trench-scan.schema.json +71 -0
- package/dist/packages/cli/assets/claude/schemas/verdict.schema.json +115 -0
- package/dist/packages/cli/bin/dexterm.mjs +11 -0
- package/dist/packages/cli/src/agent-cmd.js +336 -0
- package/dist/packages/cli/src/agent-roster.js +26 -0
- package/dist/packages/cli/src/alerts/adapters/desktop.js +40 -0
- package/dist/packages/cli/src/alerts/adapters/discord.js +36 -0
- package/dist/packages/cli/src/alerts/adapters/telegram.js +43 -0
- package/dist/packages/cli/src/alerts/adapters/toast.js +16 -0
- package/dist/packages/cli/src/alerts/creds.js +83 -0
- package/dist/packages/cli/src/alerts/format.js +41 -0
- package/dist/packages/cli/src/alerts/index.js +10 -0
- package/dist/packages/cli/src/alerts/notifier.js +53 -0
- package/dist/packages/cli/src/alerts/route.js +19 -0
- package/dist/packages/cli/src/alerts/types.js +1 -0
- package/dist/packages/cli/src/alerts-cmd.js +212 -0
- package/dist/packages/cli/src/analyst/backend.js +526 -0
- package/dist/packages/cli/src/analyst/results.js +38 -0
- package/dist/packages/cli/src/analyst/session.js +319 -0
- package/dist/packages/cli/src/audit.js +51 -0
- package/dist/packages/cli/src/backtest-cmd.js +208 -0
- package/dist/packages/cli/src/boot-state.js +62 -0
- package/dist/packages/cli/src/coin-target.js +2 -0
- package/dist/packages/cli/src/custom.js +261 -0
- package/dist/packages/cli/src/display.js +191 -0
- package/dist/packages/cli/src/doctor.js +375 -0
- package/dist/packages/cli/src/export.js +235 -0
- package/dist/packages/cli/src/feed.js +634 -0
- package/dist/packages/cli/src/fixtures.js +275 -0
- package/dist/packages/cli/src/follow-cmd.js +221 -0
- package/dist/packages/cli/src/images-config.js +59 -0
- package/dist/packages/cli/src/index.js +31 -0
- package/dist/packages/cli/src/kb-skill-cmd.js +240 -0
- package/dist/packages/cli/src/larp-record.js +21 -0
- package/dist/packages/cli/src/live/copy.js +80 -0
- package/dist/packages/cli/src/live/flow.js +233 -0
- package/dist/packages/cli/src/live/index.js +511 -0
- package/dist/packages/cli/src/live/intel.js +227 -0
- package/dist/packages/cli/src/live/newpairs.js +334 -0
- package/dist/packages/cli/src/live/screens/newpairs-live.js +219 -0
- package/dist/packages/cli/src/live/screens/token-live.js +459 -0
- package/dist/packages/cli/src/live/screens/watchlist-live.js +149 -0
- package/dist/packages/cli/src/live/token.js +585 -0
- package/dist/packages/cli/src/live/trade.js +929 -0
- package/dist/packages/cli/src/live/watchlist.js +178 -0
- package/dist/packages/cli/src/main.js +5785 -0
- package/dist/packages/cli/src/mcp-cmd.js +326 -0
- package/dist/packages/cli/src/narrative-cmd.js +76 -0
- package/dist/packages/cli/src/pack-cmd.js +323 -0
- package/dist/packages/cli/src/plan-cmd.js +278 -0
- package/dist/packages/cli/src/popup.js +810 -0
- package/dist/packages/cli/src/rules-cmd.js +131 -0
- package/dist/packages/cli/src/rules.js +358 -0
- package/dist/packages/cli/src/scan/command.js +57 -0
- package/dist/packages/cli/src/scan/engine.js +419 -0
- package/dist/packages/cli/src/scan/observe.js +64 -0
- package/dist/packages/cli/src/scan/ui.js +98 -0
- package/dist/packages/cli/src/screener/filter.js +241 -0
- package/dist/packages/cli/src/screener/presets.js +80 -0
- package/dist/packages/cli/src/screener/rows.js +146 -0
- package/dist/packages/cli/src/screener/rug.js +50 -0
- package/dist/packages/cli/src/screener/sort.js +52 -0
- package/dist/packages/cli/src/screener/view.js +183 -0
- package/dist/packages/cli/src/screens/analyst-session.js +343 -0
- package/dist/packages/cli/src/screens/analyst.js +326 -0
- package/dist/packages/cli/src/screens/boot.js +74 -0
- package/dist/packages/cli/src/screens/bubbles.js +255 -0
- package/dist/packages/cli/src/screens/coin.js +33 -0
- package/dist/packages/cli/src/screens/follows.js +201 -0
- package/dist/packages/cli/src/screens/kol.js +90 -0
- package/dist/packages/cli/src/screens/narrative.js +90 -0
- package/dist/packages/cli/src/screens/newpairs.js +150 -0
- package/dist/packages/cli/src/screens/overlay.js +257 -0
- package/dist/packages/cli/src/screens/plans.js +126 -0
- package/dist/packages/cli/src/screens/positions.js +176 -0
- package/dist/packages/cli/src/screens/rules.js +170 -0
- package/dist/packages/cli/src/screens/screener.js +459 -0
- package/dist/packages/cli/src/screens/social-panel.js +107 -0
- package/dist/packages/cli/src/screens/ticket.js +342 -0
- package/dist/packages/cli/src/screens/token.js +312 -0
- package/dist/packages/cli/src/screens/trending.js +181 -0
- package/dist/packages/cli/src/screens/watchlist.js +104 -0
- package/dist/packages/cli/src/skill-eval-build.js +109 -0
- package/dist/packages/cli/src/social/demo.js +96 -0
- package/dist/packages/cli/src/social/view.js +250 -0
- package/dist/packages/cli/src/state.js +463 -0
- package/dist/packages/cli/src/theme-cmd.js +126 -0
- package/dist/packages/cli/src/trade.js +319 -0
- package/dist/packages/cli/src/update.js +294 -0
- package/dist/packages/cli/src/version.js +3 -0
- package/dist/packages/cli/src/voice-cmd.js +119 -0
- package/dist/packages/cli/src/voice-hud.js +42 -0
- package/dist/packages/cli/src/wallet-cmd.js +440 -0
- package/dist/packages/cli/src/wallet-command.js +31 -0
- package/dist/packages/cli/src/watch-cmd.js +190 -0
- package/dist/packages/cli/src/widgets.js +208 -0
- package/dist/packages/cli/src/workspace-cmd.js +68 -0
- package/dist/packages/cli/src/x-cmd.js +154 -0
- package/dist/packages/custom/src/config.js +213 -0
- package/dist/packages/custom/src/index.js +10 -0
- package/dist/packages/custom/src/keymap.js +107 -0
- package/dist/packages/custom/src/layout.js +235 -0
- package/dist/packages/custom/src/packs.js +633 -0
- package/dist/packages/custom/src/rules.js +704 -0
- package/dist/packages/custom/src/schema.js +538 -0
- package/dist/packages/custom/src/themes.js +176 -0
- package/dist/packages/custom/src/widget-html.js +222 -0
- package/dist/packages/custom/src/workspace.js +141 -0
- package/dist/packages/data/src/analyst-actions.js +133 -0
- package/dist/packages/data/src/budget.js +88 -0
- package/dist/packages/data/src/cache.js +142 -0
- package/dist/packages/data/src/candles.js +168 -0
- package/dist/packages/data/src/clock.js +64 -0
- package/dist/packages/data/src/coin-target.js +82 -0
- package/dist/packages/data/src/credits.js +137 -0
- package/dist/packages/data/src/detail.js +97 -0
- package/dist/packages/data/src/fast-lane.js +321 -0
- package/dist/packages/data/src/fixtures.js +135 -0
- package/dist/packages/data/src/health.js +245 -0
- package/dist/packages/data/src/http.js +277 -0
- package/dist/packages/data/src/images/cache.js +141 -0
- package/dist/packages/data/src/images/decode-worker.js +28 -0
- package/dist/packages/data/src/images/decode.js +81 -0
- package/dist/packages/data/src/images/fetch.js +162 -0
- package/dist/packages/data/src/images/gif.js +214 -0
- package/dist/packages/data/src/images/index.js +10 -0
- package/dist/packages/data/src/images/jpeg.js +544 -0
- package/dist/packages/data/src/images/png.js +310 -0
- package/dist/packages/data/src/images/service.js +334 -0
- package/dist/packages/data/src/images/types.js +27 -0
- package/dist/packages/data/src/index.js +211 -0
- package/dist/packages/data/src/intel/helius-history.js +70 -0
- package/dist/packages/data/src/intel/holder-class.js +125 -0
- package/dist/packages/data/src/intel/rules.js +421 -0
- package/dist/packages/data/src/intel/scan.js +170 -0
- package/dist/packages/data/src/larp/evidence.js +288 -0
- package/dist/packages/data/src/larp/link-baseline.js +196 -0
- package/dist/packages/data/src/larp/links.js +137 -0
- package/dist/packages/data/src/larp/robots.js +90 -0
- package/dist/packages/data/src/larp/safe-fetch.js +389 -0
- package/dist/packages/data/src/limiter.js +341 -0
- package/dist/packages/data/src/model.js +12 -0
- package/dist/packages/data/src/paper.js +331 -0
- package/dist/packages/data/src/rpc-config.js +126 -0
- package/dist/packages/data/src/rpc-pool.js +166 -0
- package/dist/packages/data/src/social/chain.js +83 -0
- package/dist/packages/data/src/social/dedupe.js +205 -0
- package/dist/packages/data/src/social/feed.js +462 -0
- package/dist/packages/data/src/social/firewall.js +110 -0
- package/dist/packages/data/src/social/index.js +11 -0
- package/dist/packages/data/src/social/kol.js +269 -0
- package/dist/packages/data/src/social/model.js +11 -0
- package/dist/packages/data/src/social/rank.js +184 -0
- package/dist/packages/data/src/social/rss.js +113 -0
- package/dist/packages/data/src/social/summary.js +104 -0
- package/dist/packages/data/src/social/venues.js +186 -0
- package/dist/packages/data/src/sources/dexscreener.js +184 -0
- package/dist/packages/data/src/sources/geckoterminal.js +133 -0
- package/dist/packages/data/src/sources/goplus.js +161 -0
- package/dist/packages/data/src/sources/jupiter-swap.js +236 -0
- package/dist/packages/data/src/sources/jupiter.js +198 -0
- package/dist/packages/data/src/sources/pda.js +113 -0
- package/dist/packages/data/src/sources/pump-curve.js +58 -0
- package/dist/packages/data/src/sources/pumpportal.js +252 -0
- package/dist/packages/data/src/sources/rugcheck.js +71 -0
- package/dist/packages/data/src/sources/solana-rpc.js +299 -0
- package/dist/packages/data/src/sources/solana-ws.js +287 -0
- package/dist/packages/data/src/store.js +289 -0
- package/dist/packages/data/src/trade/bridge.js +464 -0
- package/dist/packages/data/src/trade/caps.js +323 -0
- package/dist/packages/data/src/trade/copy/auto.js +385 -0
- package/dist/packages/data/src/trade/copy/follows.js +103 -0
- package/dist/packages/data/src/trade/copy/index.js +9 -0
- package/dist/packages/data/src/trade/copy/mirror.js +91 -0
- package/dist/packages/data/src/trade/copy/stats.js +120 -0
- package/dist/packages/data/src/trade/copy/tracker.js +158 -0
- package/dist/packages/data/src/trade/copy/trades.js +87 -0
- package/dist/packages/data/src/trade/copy/types.js +6 -0
- package/dist/packages/data/src/trade/executor.js +933 -0
- package/dist/packages/data/src/trade/kill-switch.js +47 -0
- package/dist/packages/data/src/trade/link-store.js +52 -0
- package/dist/packages/data/src/trade/plans/confirm.js +157 -0
- package/dist/packages/data/src/trade/plans/engine.js +784 -0
- package/dist/packages/data/src/trade/plans/executor-live.js +220 -0
- package/dist/packages/data/src/trade/plans/executor-paper.js +44 -0
- package/dist/packages/data/src/trade/plans/executor.js +11 -0
- package/dist/packages/data/src/trade/plans/index.js +12 -0
- package/dist/packages/data/src/trade/plans/journal.js +77 -0
- package/dist/packages/data/src/trade/plans/mandate.js +129 -0
- package/dist/packages/data/src/trade/plans/plan.js +179 -0
- package/dist/packages/data/src/trade/plans/service.js +168 -0
- package/dist/packages/data/src/trade/plans/types.js +6 -0
- package/dist/packages/data/src/trade/rpc-exec.js +229 -0
- package/dist/packages/data/src/trade/signer/client.js +197 -0
- package/dist/packages/data/src/trade/signer/fast-wallet.js +154 -0
- package/dist/packages/data/src/trade/signer/host.js +97 -0
- package/dist/packages/data/src/trade/signer/main.js +121 -0
- package/dist/packages/data/src/trade/signer/probe-builder.js +85 -0
- package/dist/packages/data/src/trade/signer/protocol.js +92 -0
- package/dist/packages/data/src/trade/signer/service.js +400 -0
- package/dist/packages/data/src/trade/signer/sol-usd-feed.js +98 -0
- package/dist/packages/data/src/trade/sol-usd.js +86 -0
- package/dist/packages/data/src/trade/swap-build.js +108 -0
- package/dist/packages/data/src/trade/token-delta.js +201 -0
- package/dist/packages/data/src/trade/tx-decode.js +632 -0
- package/dist/packages/data/src/util.js +24 -0
- package/dist/packages/data/src/watchlist.js +176 -0
- package/dist/packages/data/src/x/api.js +256 -0
- package/dist/packages/data/src/x/budget.js +66 -0
- package/dist/packages/data/src/x/config.js +49 -0
- package/dist/packages/data/src/x/index.js +10 -0
- package/dist/packages/data/src/x/mock-server.js +217 -0
- package/dist/packages/data/src/x/oauth.js +242 -0
- package/dist/packages/data/src/x/pkce.js +21 -0
- package/dist/packages/data/src/x/source.js +285 -0
- package/dist/packages/data/src/x/token-store.js +134 -0
- package/dist/packages/data/src/x/transport.js +71 -0
- package/dist/packages/data/vendor/solana-web3/LICENSE +20 -0
- package/dist/packages/data/vendor/solana-web3/NOTICE.md +13 -0
- package/dist/packages/data/vendor/solana-web3/index.iife.min.js +20 -0
- package/dist/packages/mcp/assets/cases.json +1194 -0
- package/dist/packages/mcp/assets/kb/entries/cabal-and-coordinated-launches.md +28 -0
- package/dist/packages/mcp/assets/kb/entries/common-rug-patterns.md +40 -0
- package/dist/packages/mcp/assets/kb/entries/dbc-mechanics.md +30 -0
- package/dist/packages/mcp/assets/kb/entries/dev-wallet-behaviour.md +33 -0
- package/dist/packages/mcp/assets/kb/entries/glossary.md +35 -0
- package/dist/packages/mcp/assets/kb/entries/holder-concentration.md +30 -0
- package/dist/packages/mcp/assets/kb/entries/jito-bundles.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/launchlab-curve.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/lp-burn-vs-lock.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/metadata-and-copycats.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/mint-freeze-authority.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/narrative-lifecycle.md +32 -0
- package/dist/packages/mcp/assets/kb/entries/pump-bonding-curve.md +33 -0
- package/dist/packages/mcp/assets/kb/entries/pumpswap-migration.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/sniping-and-snipers.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/token2022-extensions-risk.md +32 -0
- package/dist/packages/mcp/assets/kb/entries/wash-trading-fake-volume.md +33 -0
- package/dist/packages/mcp/assets/skills/rug-check.md +52 -0
- package/dist/packages/mcp/assets/skills/token-dossier.md +57 -0
- package/dist/packages/mcp/assets/skills/trench-scan.md +40 -0
- package/dist/packages/mcp/bin/dexterm-mcp.mjs +16 -0
- package/dist/packages/mcp/src/dossier.js +78 -0
- package/dist/packages/mcp/src/firewall.js +21 -0
- package/dist/packages/mcp/src/index.js +22 -0
- package/dist/packages/mcp/src/server.js +936 -0
- package/dist/packages/mcp/src/user-content.js +434 -0
- package/dist/packages/packs/src/backtest.js +183 -0
- package/dist/packages/packs/src/digest.js +95 -0
- package/dist/packages/packs/src/firewall.js +57 -0
- package/dist/packages/packs/src/index.js +20 -0
- package/dist/packages/packs/src/larp-eval.js +100 -0
- package/dist/packages/packs/src/narrative.js +309 -0
- package/dist/packages/packs/src/reason.js +135 -0
- package/dist/packages/packs/src/rug-model.js +137 -0
- package/dist/packages/packs/src/schema.js +54 -0
- package/dist/packages/packs/src/skill-backtest.js +300 -0
- package/dist/packages/packs/src/token-dossier.js +174 -0
- package/dist/packages/packs/src/verdicts.js +17 -0
- package/dist/packages/packs/src/wallet-dossier.js +105 -0
- package/dist/packages/popup/src/config.js +71 -0
- package/dist/packages/popup/src/dock.js +271 -0
- package/dist/packages/popup/src/host.js +210 -0
- package/dist/packages/popup/src/index.js +12 -0
- package/dist/packages/popup/src/loopback.js +53 -0
- package/dist/packages/popup/src/open.js +183 -0
- package/dist/packages/popup/src/pages/bubbles-lib.js +160 -0
- package/dist/packages/popup/src/pages/bubbles.css +23 -0
- package/dist/packages/popup/src/pages/bubbles.html +36 -0
- package/dist/packages/popup/src/pages/bubbles.js +214 -0
- package/dist/packages/popup/src/pages/chart-lib.js +262 -0
- package/dist/packages/popup/src/pages/chart.css +50 -0
- package/dist/packages/popup/src/pages/chart.html +56 -0
- package/dist/packages/popup/src/pages/chart.js +554 -0
- package/dist/packages/popup/src/pages/common.css +34 -0
- package/dist/packages/popup/src/pages/common.js +187 -0
- package/dist/packages/popup/src/pages/feeds.html +20 -0
- package/dist/packages/popup/src/pages/feeds.js +77 -0
- package/dist/packages/popup/src/pages/narrative.html +20 -0
- package/dist/packages/popup/src/pages/narrative.js +61 -0
- package/dist/packages/popup/src/pages/plans.css +30 -0
- package/dist/packages/popup/src/pages/plans.html +26 -0
- package/dist/packages/popup/src/pages/plans.js +82 -0
- package/dist/packages/popup/src/pages/roster.css +41 -0
- package/dist/packages/popup/src/pages/roster.html +25 -0
- package/dist/packages/popup/src/pages/roster.js +75 -0
- package/dist/packages/popup/src/pages/token.html +20 -0
- package/dist/packages/popup/src/pages/token.js +112 -0
- package/dist/packages/popup/src/pages/widget-sdk.js +224 -0
- package/dist/packages/popup/src/pages/widget.css +5 -0
- package/dist/packages/popup/src/pages/widget.html +21 -0
- package/dist/packages/popup/src/pages/widget.js +85 -0
- package/dist/packages/popup/src/protocol.js +31 -0
- package/dist/packages/popup/src/selftest.js +156 -0
- package/dist/packages/popup/src/server.js +582 -0
- package/dist/packages/popup/src/sinkproxy.js +167 -0
- package/dist/packages/popup/src/widget-html.js +231 -0
- package/dist/packages/popup/vendor/lightweight-charts/LICENSE +201 -0
- package/dist/packages/popup/vendor/lightweight-charts/NOTICE +2 -0
- package/dist/packages/popup/vendor/lightweight-charts/README.md +30 -0
- package/dist/packages/popup/vendor/lightweight-charts/VERSION +1 -0
- package/dist/packages/popup/vendor/lightweight-charts/lightweight-charts.standalone.production.js +7 -0
- package/dist/packages/recorder/bin/dexterm-recorder.mjs +11 -0
- package/dist/packages/recorder/src/cli.js +113 -0
- package/dist/packages/recorder/src/export.js +71 -0
- package/dist/packages/recorder/src/index.js +20 -0
- package/dist/packages/recorder/src/labels.js +151 -0
- package/dist/packages/recorder/src/open.js +29 -0
- package/dist/packages/recorder/src/recorder.js +553 -0
- package/dist/packages/recorder/src/schedule.js +43 -0
- package/dist/packages/recorder/src/schema.js +135 -0
- package/dist/packages/recorder/src/snapshot.js +191 -0
- package/dist/packages/recorder/src/sweep.js +52 -0
- package/dist/packages/recorder/src/verdicts.js +52 -0
- package/dist/packages/tui/src/bubbles.js +415 -0
- package/dist/packages/tui/src/canvas.js +370 -0
- package/dist/packages/tui/src/chrome.js +85 -0
- package/dist/packages/tui/src/color.js +133 -0
- package/dist/packages/tui/src/flush.js +62 -0
- package/dist/packages/tui/src/fmt.js +188 -0
- package/dist/packages/tui/src/fx-trade.js +278 -0
- package/dist/packages/tui/src/fx.js +527 -0
- package/dist/packages/tui/src/images/blocks.js +141 -0
- package/dist/packages/tui/src/images/coin.js +22 -0
- package/dist/packages/tui/src/images/detect.js +140 -0
- package/dist/packages/tui/src/images/index.js +7 -0
- package/dist/packages/tui/src/images/layer.js +146 -0
- package/dist/packages/tui/src/images/pixels.js +157 -0
- package/dist/packages/tui/src/images/protocols.js +87 -0
- package/dist/packages/tui/src/index.js +18 -0
- package/dist/packages/tui/src/keymap.js +357 -0
- package/dist/packages/tui/src/layout.js +11 -0
- package/dist/packages/tui/src/raster-font.js +195 -0
- package/dist/packages/tui/src/raster.js +213 -0
- package/dist/packages/tui/src/sound.js +177 -0
- package/dist/packages/tui/src/theme.js +182 -0
- package/dist/packages/tui/src/widgets.js +598 -0
- package/dist/packages/voice/src/controller.js +407 -0
- package/dist/packages/voice/src/doctor.js +131 -0
- package/dist/packages/voice/src/download.js +184 -0
- package/dist/packages/voice/src/engines.js +84 -0
- package/dist/packages/voice/src/index.js +20 -0
- package/dist/packages/voice/src/router.js +502 -0
- package/dist/packages/voice/src/stt/apple-helper.swift.js +103 -0
- package/dist/packages/voice/src/stt/apple.js +179 -0
- package/dist/packages/voice/src/stt/fake.js +34 -0
- package/dist/packages/voice/src/stt/whisper.js +112 -0
- package/dist/packages/voice/src/summary.js +44 -0
- package/dist/packages/voice/src/tts/espeak.js +131 -0
- package/dist/packages/voice/src/tts/fake.js +30 -0
- package/dist/packages/voice/src/tts/piper.js +206 -0
- package/dist/packages/voice/src/tts/say.js +132 -0
- package/dist/packages/voice/src/tts/voices.js +206 -0
- package/dist/packages/voice/src/types.js +3 -0
- package/package.json +38 -4
- package/README.md +0 -3
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
// @dexterm/custom — PACKS (task-1358 Tier 4, from the task-1288 design §9 and prototype `packs/pack.mjs`).
|
|
2
|
+
//
|
|
3
|
+
// A pack is a directory: pack.json + workspaces/ themes/ rules/ keymap.json widgets/<name>/{widget.json, *.html}.
|
|
4
|
+
// Integrity is a lock file (sha256 per file + a pack hash); authenticity is an SSH signature over the lock
|
|
5
|
+
// (`ssh-keygen -Y`, present on every macOS/Linux box, zero new dependencies); "reviewed" is a hash allow-list
|
|
6
|
+
// dexterm ships (the task-1301 trust page publishes the same list). Trust tiers decide DEFAULTS, never powers:
|
|
7
|
+
// • rules from a reviewed/signed pack load enabled, from an unsigned pack disabled (the user enables each one);
|
|
8
|
+
// • a pack keymap is never merged at install — it is shown, and merged only by an explicit command;
|
|
9
|
+
// • a pack widget (an html page the popup host would serve) is registered DISABLED with no capabilities; the
|
|
10
|
+
// user enables it by name (that is the consent: a pack never gets code run on install or on load);
|
|
11
|
+
// • nothing in a pack can reach keys, the signer, caps, mandates or the gate: there is no such field, the
|
|
12
|
+
// config schema refuses unknown sections by name, and pack rules go through the same `normalizeAction`
|
|
13
|
+
// refusals as typed ones (money-shaped args die at load).
|
|
14
|
+
// Everything here is plain files + sha256 + one optional ssh-keygen spawn for sign/verify. No network.
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
16
|
+
import { readFileSync, writeFileSync, readdirSync, lstatSync, existsSync, mkdirSync, cpSync, realpathSync, rmSync, appendFileSync, statSync } from 'node:fs';
|
|
17
|
+
import { join, relative, resolve, sep, basename } from 'node:path';
|
|
18
|
+
import { spawnSync } from 'node:child_process';
|
|
19
|
+
import { validate } from "./schema.js";
|
|
20
|
+
import { parseWorkspace } from "./workspace.js";
|
|
21
|
+
import { parseRulesFile, ruleToJson, ruleToText, slug, RulesEngine } from "./rules.js";
|
|
22
|
+
import { packsDir } from "./config.js";
|
|
23
|
+
import { auditWidgetHtml } from "./widget-html.js";
|
|
24
|
+
export const PACK_MANIFEST = 'pack.json';
|
|
25
|
+
export const PACK_LOCK = 'pack.lock';
|
|
26
|
+
export const PACK_SIG = 'pack.lock.sig';
|
|
27
|
+
export const PACK_NAMESPACE = 'dexterm-pack';
|
|
28
|
+
export const WIDGET_MANIFEST = 'widget.json';
|
|
29
|
+
/** Hard caps so a pack can never be a disk / memory problem: 200 files, 2 MB total, 512 KB per file. */
|
|
30
|
+
export const PACK_LIMITS = { maxFiles: 200, maxBytes: 2_000_000, maxFileBytes: 512_000 };
|
|
31
|
+
const SKIP = new Set([PACK_LOCK, PACK_SIG, '.DS_Store']);
|
|
32
|
+
const sha = (buf) => createHash('sha256').update(buf).digest('hex');
|
|
33
|
+
export class PackError extends Error {
|
|
34
|
+
code;
|
|
35
|
+
constructor(msg, code = 'manifest') { super(msg); this.name = 'PackError'; this.code = code; }
|
|
36
|
+
}
|
|
37
|
+
/** Shipped reviewed-pack hashes (`sha256:…`). Empty until a first-party pack is published AND reviewed; `~/.dexterm/reviewed-packs.json` adds to it. */
|
|
38
|
+
export const REVIEWED_PACKS = [];
|
|
39
|
+
// ------------------------------------------------------------------ files + lock
|
|
40
|
+
/**
|
|
41
|
+
* Every regular file in the pack, sorted, as pack-relative posix paths. REFUSES (does not skip) symlinks of any
|
|
42
|
+
* kind, `node_modules`, `.git`, dotfiles, paths that are not plain `[a-z0-9/_.-]`, and the size caps: a pack that
|
|
43
|
+
* tries any of these is not a pack. (A symlink copied into ~/.dexterm/packs would be followed on load.)
|
|
44
|
+
*/
|
|
45
|
+
export function listPackFiles(dir) {
|
|
46
|
+
const out = [];
|
|
47
|
+
let bytes = 0;
|
|
48
|
+
const walk = (d) => {
|
|
49
|
+
for (const f of readdirSync(d).sort()) {
|
|
50
|
+
const p = join(d, f);
|
|
51
|
+
if (SKIP.has(f) && d === dir)
|
|
52
|
+
continue;
|
|
53
|
+
const rel = relative(dir, p).split(sep).join('/');
|
|
54
|
+
if (f === 'node_modules' || f === '.git')
|
|
55
|
+
throw new PackError(`refused: ${rel} (packs are plain files; no dependencies, no repos)`, 'files');
|
|
56
|
+
if (f.startsWith('.'))
|
|
57
|
+
throw new PackError(`refused: hidden entry ${rel}`, 'files');
|
|
58
|
+
const st = lstatSync(p);
|
|
59
|
+
if (st.isSymbolicLink())
|
|
60
|
+
throw new PackError(`refused: ${rel} is a symlink (packs may not link outside themselves)`, 'files');
|
|
61
|
+
if (st.isDirectory()) {
|
|
62
|
+
walk(p);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (!st.isFile())
|
|
66
|
+
throw new PackError(`refused: ${rel} is not a regular file`, 'files');
|
|
67
|
+
if (!/^[a-z0-9][a-z0-9/_.-]*$/.test(rel) || rel.includes('..') || rel.includes('//'))
|
|
68
|
+
throw new PackError(`refused: file name ${rel} (lowercase letters, digits, - _ . / only)`, 'files');
|
|
69
|
+
if (st.size > PACK_LIMITS.maxFileBytes)
|
|
70
|
+
throw new PackError(`refused: ${rel} is ${st.size} bytes (max ${PACK_LIMITS.maxFileBytes})`, 'files');
|
|
71
|
+
bytes += st.size;
|
|
72
|
+
out.push(rel);
|
|
73
|
+
if (out.length > PACK_LIMITS.maxFiles)
|
|
74
|
+
throw new PackError(`refused: more than ${PACK_LIMITS.maxFiles} files`, 'files');
|
|
75
|
+
if (bytes > PACK_LIMITS.maxBytes)
|
|
76
|
+
throw new PackError(`refused: pack is larger than ${PACK_LIMITS.maxBytes} bytes`, 'files');
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
if (!existsSync(dir) || !lstatSync(dir).isDirectory())
|
|
80
|
+
throw new PackError(`${dir}: not a directory`, 'io');
|
|
81
|
+
walk(dir);
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
/** Compute the lock: per-file hashes and the pack hash (hash of the sorted "path sha" lines). */
|
|
85
|
+
export function computeLock(dir) {
|
|
86
|
+
const files = listPackFiles(dir).map((path) => { const buf = readFileSync(join(dir, path)); return { path, sha256: sha(buf), bytes: buf.length }; });
|
|
87
|
+
const hash = 'sha256:' + sha(files.map((f) => `${f.path} ${f.sha256}`).join('\n'));
|
|
88
|
+
return { version: 1, files, hash };
|
|
89
|
+
}
|
|
90
|
+
export function writeLock(dir) {
|
|
91
|
+
const lock = computeLock(dir);
|
|
92
|
+
writeFileSync(join(dir, PACK_LOCK), JSON.stringify(lock, null, 2) + '\n');
|
|
93
|
+
return lock;
|
|
94
|
+
}
|
|
95
|
+
export function readLock(dir) {
|
|
96
|
+
const p = join(dir, PACK_LOCK);
|
|
97
|
+
if (!existsSync(p))
|
|
98
|
+
return null;
|
|
99
|
+
const raw = JSON.parse(readFileSync(p, 'utf8'));
|
|
100
|
+
if (!raw || raw.version !== 1 || !Array.isArray(raw.files) || typeof raw.hash !== 'string')
|
|
101
|
+
throw new PackError('pack.lock is not a v1 lock', 'integrity');
|
|
102
|
+
return raw;
|
|
103
|
+
}
|
|
104
|
+
/** Integrity: the lock on disk must match the files on disk. */
|
|
105
|
+
export function checkIntegrity(dir) {
|
|
106
|
+
let stored;
|
|
107
|
+
try {
|
|
108
|
+
stored = readLock(dir);
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
return { ok: false, reason: e.message };
|
|
112
|
+
}
|
|
113
|
+
if (!stored)
|
|
114
|
+
return { ok: false, reason: 'no pack.lock (run: dexterm pack lock <dir>)' };
|
|
115
|
+
let now;
|
|
116
|
+
try {
|
|
117
|
+
now = computeLock(dir);
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
return { ok: false, reason: e.message };
|
|
121
|
+
}
|
|
122
|
+
if (stored.hash !== now.hash) {
|
|
123
|
+
const changed = now.files.filter((f) => { const s = stored.files.find((x) => x.path === f.path); return s && s.sha256 !== f.sha256; }).map((f) => f.path);
|
|
124
|
+
const missing = stored.files.filter((s) => !now.files.find((f) => f.path === s.path)).map((s) => s.path);
|
|
125
|
+
const added = now.files.filter((f) => !stored.files.find((s) => s.path === f.path)).map((f) => f.path);
|
|
126
|
+
return { ok: false, reason: 'files differ from pack.lock', changed, missing, added, hash: now.hash };
|
|
127
|
+
}
|
|
128
|
+
return { ok: true, hash: now.hash };
|
|
129
|
+
}
|
|
130
|
+
// ------------------------------------------------------------------ manifest
|
|
131
|
+
const JSON_FORBIDDEN = /"(__proto__|constructor|prototype)"\s*:/;
|
|
132
|
+
/** Parse JSON from a pack file: a prototype-pollution key anywhere is a refusal, whatever the schema says later. */
|
|
133
|
+
export function readPackJson(dir, rel) {
|
|
134
|
+
const p = join(dir, rel);
|
|
135
|
+
let text;
|
|
136
|
+
try {
|
|
137
|
+
text = readFileSync(p, 'utf8');
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
throw new PackError(`${rel}: ${e.message}`, 'io');
|
|
141
|
+
}
|
|
142
|
+
if (JSON_FORBIDDEN.test(text))
|
|
143
|
+
throw new PackError(`${rel}: forbidden key (__proto__ / constructor / prototype)`, 'manifest');
|
|
144
|
+
try {
|
|
145
|
+
return JSON.parse(text);
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
throw new PackError(`${rel}: not valid JSON (${e.message})`, 'manifest');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** Is `rel` a regular file inside `dir` (after realpath; no traversal, no link)? */
|
|
152
|
+
function insidePack(dir, rel) {
|
|
153
|
+
const root = realpathSync(dir);
|
|
154
|
+
const p = resolve(dir, rel);
|
|
155
|
+
if (!existsSync(p))
|
|
156
|
+
return false;
|
|
157
|
+
if (lstatSync(p).isSymbolicLink())
|
|
158
|
+
return false;
|
|
159
|
+
const real = realpathSync(p);
|
|
160
|
+
return real === root || (real.startsWith(root + sep) && statSync(real).isFile());
|
|
161
|
+
}
|
|
162
|
+
export function readManifest(dir) {
|
|
163
|
+
const m = readPackJson(dir, PACK_MANIFEST);
|
|
164
|
+
const errors = validate('packManifest', m);
|
|
165
|
+
if (errors.length)
|
|
166
|
+
throw new PackError(`pack.json invalid: ${errors.slice(0, 3).map((e) => `${e.path || '/'} ${e.msg}`).join('; ')}`, 'manifest');
|
|
167
|
+
const man = m;
|
|
168
|
+
if (man.requires?.api !== undefined && man.requires.api > 1)
|
|
169
|
+
throw new PackError(`pack.json needs api ${man.requires.api}; this dexterm speaks api 1`, 'manifest');
|
|
170
|
+
const refs = [...(man.contents.workspaces ?? []), ...(man.contents.themes ?? []), ...(man.contents.rules ?? []), ...(man.contents.keymap ? [man.contents.keymap] : []), ...(man.contents.widgets ?? []).map((w) => `${w}/${WIDGET_MANIFEST}`)];
|
|
171
|
+
for (const r of refs)
|
|
172
|
+
if (!insidePack(dir, r))
|
|
173
|
+
throw new PackError(`pack.json references ${r}, which is missing or not a plain file inside the pack`, 'manifest');
|
|
174
|
+
return man;
|
|
175
|
+
}
|
|
176
|
+
// ------------------------------------------------------------------ signatures (ssh-keygen -Y)
|
|
177
|
+
export const allowedSignersPath = (home) => join(home, 'allowed_signers');
|
|
178
|
+
export const reviewedPacksPath = (home) => join(home, 'reviewed-packs.json');
|
|
179
|
+
/** The reviewed hashes: the shipped list plus the user's `~/.dexterm/reviewed-packs.json` (an array of `sha256:…`). */
|
|
180
|
+
export function reviewedHashes(home) {
|
|
181
|
+
const out = [...REVIEWED_PACKS];
|
|
182
|
+
try {
|
|
183
|
+
const raw = JSON.parse(readFileSync(reviewedPacksPath(home), 'utf8'));
|
|
184
|
+
if (Array.isArray(raw))
|
|
185
|
+
for (const h of raw)
|
|
186
|
+
if (typeof h === 'string' && /^sha256:[0-9a-f]{64}$/.test(h))
|
|
187
|
+
out.push(h);
|
|
188
|
+
}
|
|
189
|
+
catch { /* none */ }
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
const SSH_PUBKEY = /^(ssh-ed25519|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ssh-rsa|sk-ssh-ed25519@openssh\.com|sk-ecdsa-sha2-nistp256@openssh\.com) [A-Za-z0-9+/=]+( [^\s]+)?$/;
|
|
193
|
+
const IDENTITY = /^[a-z0-9][a-z0-9._@-]{0,79}$/;
|
|
194
|
+
/** Pin a publisher key: one `<identity> namespaces="dexterm-pack" <pubkey>` line in ~/.dexterm/allowed_signers (0600). */
|
|
195
|
+
export function pinSigner(home, identity, pubkey) {
|
|
196
|
+
if (!IDENTITY.test(identity))
|
|
197
|
+
throw new PackError('identity: lowercase letters, digits, . _ @ - (max 80)', 'signature');
|
|
198
|
+
const key = pubkey.trim();
|
|
199
|
+
if (!SSH_PUBKEY.test(key))
|
|
200
|
+
throw new PackError('pubkey: an OpenSSH public key line (ssh-ed25519 AAAA… [comment])', 'signature');
|
|
201
|
+
mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
202
|
+
const line = `${identity} namespaces="${PACK_NAMESPACE}" ${key.split(/\s+/).slice(0, 2).join(' ')}\n`;
|
|
203
|
+
const p = allowedSignersPath(home);
|
|
204
|
+
const existing = existsSync(p) ? readFileSync(p, 'utf8') : '';
|
|
205
|
+
if (existing.includes(line))
|
|
206
|
+
return p;
|
|
207
|
+
appendFileSync(p, line, { mode: 0o600 });
|
|
208
|
+
return p;
|
|
209
|
+
}
|
|
210
|
+
export function listSigners(home) {
|
|
211
|
+
try {
|
|
212
|
+
return readFileSync(allowedSignersPath(home), 'utf8').split('\n').filter(Boolean).map((l) => { const [identity, , type, key] = l.split(/\s+/); return { identity, key: `${type} ${key.slice(0, 16)}…` }; });
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
export function signPack(dir, opts) {
|
|
219
|
+
const lockPath = join(dir, PACK_LOCK);
|
|
220
|
+
if (!existsSync(lockPath))
|
|
221
|
+
writeLock(dir);
|
|
222
|
+
const r = spawnSync('ssh-keygen', ['-Y', 'sign', '-f', opts.keyFile, '-n', opts.namespace ?? PACK_NAMESPACE, lockPath], { encoding: 'utf8', timeout: 10_000 });
|
|
223
|
+
if (r.error)
|
|
224
|
+
throw new PackError(`ssh-keygen not available: ${r.error.message}`, 'signature');
|
|
225
|
+
if (r.status !== 0)
|
|
226
|
+
throw new PackError(`ssh-keygen sign failed: ${(r.stderr || '').trim().split('\n')[0]}`, 'signature');
|
|
227
|
+
return join(dir, PACK_SIG);
|
|
228
|
+
}
|
|
229
|
+
/** Verify pack.lock.sig against the pinned publisher `identity` in `allowedSigners`. Never throws. */
|
|
230
|
+
export function verifySignature(dir, opts) {
|
|
231
|
+
const lockPath = join(dir, PACK_LOCK), sigPath = join(dir, PACK_SIG);
|
|
232
|
+
if (!existsSync(sigPath))
|
|
233
|
+
return { ok: false, reason: 'unsigned (no pack.lock.sig)' };
|
|
234
|
+
if (!existsSync(lockPath))
|
|
235
|
+
return { ok: false, reason: 'no pack.lock' };
|
|
236
|
+
if (!existsSync(opts.allowedSigners))
|
|
237
|
+
return { ok: false, reason: `no pinned publisher keys (${basename(opts.allowedSigners)}); dexterm pack trust <identity> "<pubkey>"` };
|
|
238
|
+
if (!IDENTITY.test(opts.identity))
|
|
239
|
+
return { ok: false, reason: 'bad identity' };
|
|
240
|
+
const r = spawnSync('ssh-keygen', ['-Y', 'verify', '-f', opts.allowedSigners, '-I', opts.identity, '-n', opts.namespace ?? PACK_NAMESPACE, '-s', sigPath], { input: readFileSync(lockPath), encoding: 'utf8', timeout: 10_000 });
|
|
241
|
+
if (r.error)
|
|
242
|
+
return { ok: false, reason: `ssh-keygen not available: ${r.error.message}` };
|
|
243
|
+
return r.status === 0 ? { ok: true, identity: opts.identity } : { ok: false, reason: (r.stderr || r.stdout || 'bad signature').trim().split('\n')[0] };
|
|
244
|
+
}
|
|
245
|
+
/** Trust tier for a pack: reviewed (hash in the shipped list) > signed (pinned publisher) > unsigned. */
|
|
246
|
+
export function trustOf(x) {
|
|
247
|
+
if (x.reviewedHashes.includes(x.hash))
|
|
248
|
+
return 'reviewed';
|
|
249
|
+
if (x.signatureOk)
|
|
250
|
+
return 'signed';
|
|
251
|
+
return 'unsigned';
|
|
252
|
+
}
|
|
253
|
+
/** Rule ids and sources are namespaced per pack: `<pack>-<rule>` from `pack:<pack>`. */
|
|
254
|
+
export const packRuleId = (pack, id) => slug(`${pack}-${id}`);
|
|
255
|
+
export const packWorkspaceName = (pack, name) => `${pack}/${name}`;
|
|
256
|
+
export const packThemeName = (pack, name) => slug(`${pack}-${name}`);
|
|
257
|
+
export const packWidgetKey = (pack, widget) => `${pack}/${widget}`;
|
|
258
|
+
export function inspectPack(dir, opts = {}) {
|
|
259
|
+
const manifest = readManifest(dir);
|
|
260
|
+
const integrity = checkIntegrity(dir);
|
|
261
|
+
const identity = opts.identity ?? manifest.publisher;
|
|
262
|
+
const signature = opts.home ? verifySignature(dir, { allowedSigners: allowedSignersPath(opts.home), identity }) : { ok: false, reason: 'no home (signature not checked)' };
|
|
263
|
+
const reviewed = opts.reviewedHashes ?? (opts.home ? reviewedHashes(opts.home) : REVIEWED_PACKS);
|
|
264
|
+
const trust = integrity.ok ? trustOf({ hash: integrity.hash, signatureOk: signature.ok, reviewedHashes: reviewed }) : 'unsigned';
|
|
265
|
+
const warnings = [];
|
|
266
|
+
const out = { dir, manifest, integrity, signature, trust, workspaces: [], themes: [], rules: [], keymap: null, widgets: [], warnings };
|
|
267
|
+
for (const rel of manifest.contents.workspaces ?? []) {
|
|
268
|
+
try {
|
|
269
|
+
const raw = readPackJson(dir, rel);
|
|
270
|
+
const ws = parseWorkspace(JSON.stringify(raw), rel);
|
|
271
|
+
if (ws.name !== basename(rel, '.json'))
|
|
272
|
+
throw new Error(`"name" must be "${basename(rel, '.json')}" (the file name)`);
|
|
273
|
+
out.workspaces.push({ path: rel, workspace: ws });
|
|
274
|
+
}
|
|
275
|
+
catch (e) {
|
|
276
|
+
out.workspaces.push({ path: rel, workspace: null, error: e.message });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
for (const rel of manifest.contents.themes ?? []) {
|
|
280
|
+
const name = basename(rel, '.json');
|
|
281
|
+
try {
|
|
282
|
+
const raw = readPackJson(dir, rel);
|
|
283
|
+
if (raw && typeof raw === 'object' && raw.name !== undefined && raw.name !== name)
|
|
284
|
+
throw new Error(`"name" must be "${name}" (the file name)`);
|
|
285
|
+
const doc = { ...raw, name };
|
|
286
|
+
const errors = validate('userTheme', doc);
|
|
287
|
+
if (errors.length)
|
|
288
|
+
throw new Error(`${errors[0].path || '/'} ${errors[0].msg}`);
|
|
289
|
+
out.themes.push({ path: rel, name, doc });
|
|
290
|
+
}
|
|
291
|
+
catch (e) {
|
|
292
|
+
out.themes.push({ path: rel, name, doc: null, error: e.message });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
for (const rel of manifest.contents.rules ?? []) {
|
|
296
|
+
let text;
|
|
297
|
+
try {
|
|
298
|
+
text = readFileSync(join(dir, rel), 'utf8');
|
|
299
|
+
}
|
|
300
|
+
catch (e) {
|
|
301
|
+
out.rules.push({ path: rel, rules: [], errors: [{ line: 0, msg: e.message }] });
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (rel.endsWith('.json')) {
|
|
305
|
+
try {
|
|
306
|
+
const raw = readPackJson(dir, rel);
|
|
307
|
+
const arr = Array.isArray(raw) ? raw : [raw];
|
|
308
|
+
const file = { path: rel, rules: [], errors: [] };
|
|
309
|
+
arr.forEach((r, i) => {
|
|
310
|
+
const errs = validate('ruleJson', r);
|
|
311
|
+
if (errs.length) {
|
|
312
|
+
file.errors.push({ line: i + 1, msg: `${errs[0].path || '/'} ${errs[0].msg}` });
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const j = r;
|
|
316
|
+
const rule = { ...j, id: packRuleId(manifest.name, j.id), source: `pack:${manifest.name}` };
|
|
317
|
+
// the same refusals a typed rule gets (money-shaped args, prototype keys, caps): the schema alone allows any args
|
|
318
|
+
try {
|
|
319
|
+
new RulesEngine().load(rule);
|
|
320
|
+
}
|
|
321
|
+
catch (e) {
|
|
322
|
+
file.errors.push({ line: i + 1, msg: e.message });
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
file.rules.push(rule);
|
|
326
|
+
});
|
|
327
|
+
out.rules.push(file);
|
|
328
|
+
}
|
|
329
|
+
catch (e) {
|
|
330
|
+
out.rules.push({ path: rel, rules: [], errors: [{ line: 0, msg: e.message }] });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
const parsed = parseRulesFile(text, { source: `pack:${manifest.name}`, idPrefix: manifest.name });
|
|
335
|
+
out.rules.push({ path: rel, rules: parsed.rules, errors: parsed.errors });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
for (const f of out.rules)
|
|
339
|
+
for (const e of f.errors)
|
|
340
|
+
warnings.push(`${f.path}:${e.line} refused: ${e.msg}`);
|
|
341
|
+
if (manifest.contents.keymap) {
|
|
342
|
+
const rel = manifest.contents.keymap;
|
|
343
|
+
try {
|
|
344
|
+
const raw = readPackJson(dir, rel);
|
|
345
|
+
const errors = validate('keymap', raw);
|
|
346
|
+
if (errors.length)
|
|
347
|
+
throw new Error(`${errors[0].path || '/'} ${errors[0].msg}`);
|
|
348
|
+
out.keymap = { path: rel, keymap: raw };
|
|
349
|
+
const n = Object.keys(raw.global ?? {}).length + Object.values(raw.screens ?? {}).reduce((a, t) => a + Object.keys(t).length, 0);
|
|
350
|
+
warnings.push(`keymap: ${n} binding(s) are NOT merged on install — review with "dexterm pack keymap ${manifest.name}" and merge explicitly`);
|
|
351
|
+
}
|
|
352
|
+
catch (e) {
|
|
353
|
+
out.keymap = { path: rel, keymap: null, error: e.message };
|
|
354
|
+
warnings.push(`${rel} refused: ${e.message}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
for (const wdir of manifest.contents.widgets ?? []) {
|
|
358
|
+
const name = basename(wdir);
|
|
359
|
+
const entry = { name, dir: wdir, manifest: null, pages: [] };
|
|
360
|
+
try {
|
|
361
|
+
const raw = readPackJson(dir, `${wdir}/${WIDGET_MANIFEST}`);
|
|
362
|
+
const errors = validate('widgetManifest', raw);
|
|
363
|
+
if (errors.length)
|
|
364
|
+
throw new Error(`${errors[0].path || '/'} ${errors[0].msg}`);
|
|
365
|
+
const wm = raw;
|
|
366
|
+
if (wm.name !== name)
|
|
367
|
+
throw new Error(`"name" must be "${name}" (the directory name)`);
|
|
368
|
+
for (const w of wm.widgets) {
|
|
369
|
+
if (!insidePack(dir, `${wdir}/${w.page}`))
|
|
370
|
+
throw new Error(`page ${w.page} is missing`);
|
|
371
|
+
// r2: a page that would load code or documents from elsewhere is refused HERE, before anything lands (the popup
|
|
372
|
+
// host refuses the same page again at open and at ticket issue; its CSP allow-lists inline blocks by hash only)
|
|
373
|
+
const problems = auditWidgetHtml(readFileSync(join(dir, wdir, w.page), 'utf8'));
|
|
374
|
+
if (problems.length)
|
|
375
|
+
throw new Error(`page ${w.page}: ${problems.join('; ')}`);
|
|
376
|
+
entry.pages.push(`${wdir}/${w.page}`);
|
|
377
|
+
}
|
|
378
|
+
entry.manifest = wm;
|
|
379
|
+
warnings.push(`widget ${name}: ${wm.widgets.length} page(s) of html+js the popup host would serve (inline only, hash-allow-listed; no remote script, style, frame or import) — installed DISABLED; enable with "dexterm pack widget ${manifest.name} ${name} --enable" after reading it`);
|
|
380
|
+
}
|
|
381
|
+
catch (e) {
|
|
382
|
+
entry.error = e.message;
|
|
383
|
+
warnings.push(`widget ${name} refused: ${e.message}`);
|
|
384
|
+
}
|
|
385
|
+
out.widgets.push(entry);
|
|
386
|
+
}
|
|
387
|
+
for (const w of out.workspaces)
|
|
388
|
+
if (w.error)
|
|
389
|
+
warnings.push(`${w.path} refused: ${w.error}`);
|
|
390
|
+
for (const t of out.themes)
|
|
391
|
+
if (t.error)
|
|
392
|
+
warnings.push(`${t.path} refused: ${t.error}`);
|
|
393
|
+
if (!integrity.ok)
|
|
394
|
+
warnings.unshift(`integrity: ${integrity.reason}${'changed' in integrity && integrity.changed?.length ? ` (changed: ${integrity.changed.join(', ')})` : ''}`);
|
|
395
|
+
if (trust === 'unsigned')
|
|
396
|
+
warnings.unshift(`not reviewed or signed (${signature.ok ? '' : signature.reason}) — its rules load DISABLED; do you trust its source?`);
|
|
397
|
+
return out;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Install into <home>/packs/<name>@<version>/ and record it in config.packs. Unsigned packs need an explicit
|
|
401
|
+
* `acceptUnsigned` (the risk card). Widgets are registered DISABLED with no capabilities. The keymap is not merged.
|
|
402
|
+
* Returns the NEW config (the caller persists it); writes only under <home>/packs.
|
|
403
|
+
* r2: installing a NEW VERSION of an installed pack is an upgrade — the old `<name>@<old>` directory is deleted (only
|
|
404
|
+
* under <home>/packs) once the new one is in place, stale widget registrations go, and every widget of the new version
|
|
405
|
+
* starts DISABLED again (its page may have changed; consent is per reviewed page).
|
|
406
|
+
*/
|
|
407
|
+
export function installPack(home, dir, config, opts = {}) {
|
|
408
|
+
const insp = inspectPack(dir, { home, reviewedHashes: opts.reviewedHashes, identity: opts.identity });
|
|
409
|
+
if (!insp.integrity.ok)
|
|
410
|
+
throw new PackError(`integrity: ${insp.integrity.reason}${'changed' in insp.integrity && insp.integrity.changed?.length ? ' (' + insp.integrity.changed.join(', ') + ')' : ''}`, 'integrity');
|
|
411
|
+
if (insp.trust === 'unsigned' && !opts.acceptUnsigned)
|
|
412
|
+
throw new PackError('pack is unsigned and not reviewed; accept the risk card first (--accept-unsigned)', 'trust');
|
|
413
|
+
// anything refused inside the pack blocks the install: a pack is all-or-nothing, like a keymap
|
|
414
|
+
const refused = insp.warnings.filter((w) => / refused: /.test(w));
|
|
415
|
+
if (refused.length)
|
|
416
|
+
throw new PackError(`pack content refused: ${refused[0]}`, 'install');
|
|
417
|
+
const dest = join(packsDir(home), `${insp.manifest.name}@${insp.manifest.version}`);
|
|
418
|
+
if (existsSync(dest))
|
|
419
|
+
throw new PackError(`already installed: ${insp.manifest.name}@${insp.manifest.version} (remove it first)`, 'install');
|
|
420
|
+
mkdirSync(packsDir(home), { recursive: true, mode: 0o700 });
|
|
421
|
+
// copy exactly the locked files (never a symlink, never node_modules: listPackFiles already refused them)
|
|
422
|
+
mkdirSync(dest, { recursive: true, mode: 0o700 });
|
|
423
|
+
for (const f of [...listPackFiles(dir), PACK_LOCK, ...(existsSync(join(dir, PACK_SIG)) ? [PACK_SIG] : [])]) {
|
|
424
|
+
mkdirSync(join(dest, f, '..'), { recursive: true, mode: 0o700 });
|
|
425
|
+
cpSync(join(dir, f), join(dest, f), { dereference: false });
|
|
426
|
+
}
|
|
427
|
+
const cfg = structuredClone(config);
|
|
428
|
+
const prev = (cfg.packs ?? []).find((p) => p.name === insp.manifest.name);
|
|
429
|
+
cfg.packs = (cfg.packs ?? []).filter((p) => p.name !== insp.manifest.name);
|
|
430
|
+
cfg.packs.push({ name: insp.manifest.name, version: insp.manifest.version, source: realpathSync(dir).slice(0, 200), hash: insp.integrity.hash, trust: insp.trust, publisher: insp.manifest.publisher, installedAt: opts.now ?? Date.now() });
|
|
431
|
+
// widgets from packs are registered DISABLED with NO capabilities: consent is a later, explicit step (and an upgrade
|
|
432
|
+
// starts from zero again: the registrations of the previous version — enabled or not — are dropped first)
|
|
433
|
+
for (const k of Object.keys(cfg.plugins ?? {}))
|
|
434
|
+
if (k.startsWith(`${insp.manifest.name}/`))
|
|
435
|
+
delete cfg.plugins[k];
|
|
436
|
+
for (const w of insp.widgets) {
|
|
437
|
+
if (!w.manifest)
|
|
438
|
+
continue;
|
|
439
|
+
cfg.plugins ??= {};
|
|
440
|
+
cfg.plugins[packWidgetKey(insp.manifest.name, w.name)] = { enabled: false, capabilities: [], settings: {} };
|
|
441
|
+
}
|
|
442
|
+
// the previous version's directory goes now that the new one is complete (never anything outside <home>/packs)
|
|
443
|
+
let replaced = null;
|
|
444
|
+
if (prev && prev.version !== insp.manifest.version) {
|
|
445
|
+
const old = join(packsDir(home), `${prev.name}@${prev.version}`);
|
|
446
|
+
const root = resolve(packsDir(home));
|
|
447
|
+
if (resolve(old).startsWith(root + sep) && existsSync(old)) {
|
|
448
|
+
rmSync(old, { recursive: true, force: true });
|
|
449
|
+
replaced = old;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return { config: cfg, dest, trust: insp.trust, hash: insp.integrity.hash, manifest: insp.manifest, inspection: insp, replaced };
|
|
453
|
+
}
|
|
454
|
+
/** Remove an installed pack: its directory (only under <home>/packs) and its config record + widget registrations. */
|
|
455
|
+
export function removePack(home, config, name) {
|
|
456
|
+
const rec = (config.packs ?? []).find((p) => p.name === name);
|
|
457
|
+
const cfg = structuredClone(config);
|
|
458
|
+
cfg.packs = (cfg.packs ?? []).filter((p) => p.name !== name);
|
|
459
|
+
for (const k of Object.keys(cfg.plugins ?? {}))
|
|
460
|
+
if (k.startsWith(`${name}/`))
|
|
461
|
+
delete cfg.plugins[k];
|
|
462
|
+
let removed = null;
|
|
463
|
+
if (rec) {
|
|
464
|
+
const dest = join(packsDir(home), `${rec.name}@${rec.version}`);
|
|
465
|
+
const root = resolve(packsDir(home));
|
|
466
|
+
if (resolve(dest).startsWith(root + sep) && existsSync(dest)) {
|
|
467
|
+
rmSync(dest, { recursive: true, force: true });
|
|
468
|
+
removed = dest;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return { config: cfg, removed };
|
|
472
|
+
}
|
|
473
|
+
/** Flip a pack widget's consent flag (the only way its page can ever be served). Returns the new config or throws. */
|
|
474
|
+
export function setWidgetEnabled(config, pack, widget, enabled) {
|
|
475
|
+
const key = packWidgetKey(pack, widget);
|
|
476
|
+
if (!config.plugins?.[key])
|
|
477
|
+
throw new PackError(`no widget ${key} (installed packs register theirs disabled)`, 'install');
|
|
478
|
+
const cfg = structuredClone(config);
|
|
479
|
+
cfg.plugins[key] = { ...cfg.plugins[key], enabled };
|
|
480
|
+
return cfg;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* The content of every INSTALLED pack (a config.packs record whose directory still hashes to the recorded hash).
|
|
484
|
+
* A directory under ~/.dexterm/packs without a record is ignored and noted (copying files is not an install); a
|
|
485
|
+
* pack whose files changed since install is skipped and reported (re-install to accept the new hash).
|
|
486
|
+
*/
|
|
487
|
+
export function loadPacks(home, config) {
|
|
488
|
+
const out = { workspaces: [], themes: [], rules: [], keymaps: [], widgets: [], errors: [], notes: [] };
|
|
489
|
+
const records = config.packs ?? [];
|
|
490
|
+
for (const rec of records) {
|
|
491
|
+
const dir = join(packsDir(home), `${rec.name}@${rec.version}`);
|
|
492
|
+
if (!existsSync(dir)) {
|
|
493
|
+
out.errors.push(`pack ${rec.name}@${rec.version}: directory missing (dexterm pack remove ${rec.name})`);
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
let insp;
|
|
497
|
+
try {
|
|
498
|
+
insp = inspectPack(dir, { home, reviewedHashes: [] });
|
|
499
|
+
}
|
|
500
|
+
catch (e) {
|
|
501
|
+
out.errors.push(`pack ${rec.name}: ${e.message}`);
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (!insp.integrity.ok || insp.integrity.hash !== rec.hash) {
|
|
505
|
+
out.errors.push(`pack ${rec.name}: files changed since install (${insp.integrity.ok ? 'hash differs' : insp.integrity.reason}); skipped — re-install to accept`);
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
const enabled = rec.trust === 'reviewed' || rec.trust === 'signed';
|
|
509
|
+
for (const w of insp.workspaces) {
|
|
510
|
+
if (w.workspace)
|
|
511
|
+
out.workspaces.push({ pack: rec.name, name: packWorkspaceName(rec.name, w.workspace.name), path: join(dir, w.path), workspace: { ...w.workspace, name: packWorkspaceName(rec.name, w.workspace.name) } });
|
|
512
|
+
else
|
|
513
|
+
out.errors.push(`pack ${rec.name}: ${w.path}: ${w.error}`);
|
|
514
|
+
}
|
|
515
|
+
for (const t of insp.themes) {
|
|
516
|
+
if (t.doc) {
|
|
517
|
+
const name = packThemeName(rec.name, t.name);
|
|
518
|
+
out.themes.push({ pack: rec.name, name, path: join(dir, t.path), doc: { ...t.doc, name } });
|
|
519
|
+
}
|
|
520
|
+
else
|
|
521
|
+
out.errors.push(`pack ${rec.name}: ${t.path}: ${t.error}`);
|
|
522
|
+
}
|
|
523
|
+
for (const f of insp.rules) {
|
|
524
|
+
for (const r of f.rules)
|
|
525
|
+
out.rules.push({ ...r, enabled });
|
|
526
|
+
for (const e of f.errors)
|
|
527
|
+
out.errors.push(`pack ${rec.name}: ${f.path}:${e.line}: ${e.msg}`);
|
|
528
|
+
}
|
|
529
|
+
if (insp.keymap?.keymap)
|
|
530
|
+
out.keymaps.push({ pack: rec.name, keymap: insp.keymap.keymap });
|
|
531
|
+
for (const w of insp.widgets) {
|
|
532
|
+
if (w.manifest)
|
|
533
|
+
out.widgets.push({ pack: rec.name, name: w.name, dir: join(dir, w.dir), manifest: w.manifest, enabled: config.plugins?.[packWidgetKey(rec.name, w.name)]?.enabled === true });
|
|
534
|
+
else
|
|
535
|
+
out.errors.push(`pack ${rec.name}: widget ${w.name}: ${w.error}`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
// rogue directories: present on disk, absent from config
|
|
539
|
+
try {
|
|
540
|
+
for (const d of readdirSync(packsDir(home)).sort()) {
|
|
541
|
+
const m = /^([a-z][a-z0-9-]{0,39})@(\d+\.\d+\.\d+)$/.exec(d);
|
|
542
|
+
if (!m) {
|
|
543
|
+
out.notes.push(`packs/${d}: not a pack directory name; ignored`);
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
if (!records.some((r) => r.name === m[1] && r.version === m[2]))
|
|
547
|
+
out.notes.push(`packs/${d}: not installed (no config record); ignored — dexterm pack add <dir> installs a pack`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
catch { /* no packs dir */ }
|
|
551
|
+
return out;
|
|
552
|
+
}
|
|
553
|
+
// ------------------------------------------------------------------ export (the user's own files → a pack)
|
|
554
|
+
/** Refuse anything that looks like a secret inside pack text: base58 private keys, Helius-style tokens, long hex, high-entropy blobs. */
|
|
555
|
+
export function scanForSecrets(text) {
|
|
556
|
+
const b58 = /[1-9A-HJ-NP-Za-km-z]{64,90}/.exec(text);
|
|
557
|
+
if (b58)
|
|
558
|
+
return `looks like a base58 private key (${b58[0].slice(0, 6)}…)`;
|
|
559
|
+
const hlx = /\bhlx-[A-Za-z0-9_-]{8,}/.exec(text);
|
|
560
|
+
if (hlx)
|
|
561
|
+
return 'looks like a Helius token (hlx-…)';
|
|
562
|
+
const hex = /\b[0-9a-f]{64}\b/.exec(text);
|
|
563
|
+
if (hex && !/sha256:/.test(text.slice(Math.max(0, hex.index - 7), hex.index)))
|
|
564
|
+
return `looks like a 32-byte hex secret (${hex[0].slice(0, 6)}…)`;
|
|
565
|
+
const uuidish = /[A-Za-z0-9_-]{40,}/.exec(text);
|
|
566
|
+
if (uuidish && entropy(uuidish[0]) > 4.6)
|
|
567
|
+
return `a high-entropy token (${uuidish[0].slice(0, 6)}…)`;
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
function entropy(s) {
|
|
571
|
+
const f = new Map();
|
|
572
|
+
for (const ch of s)
|
|
573
|
+
f.set(ch, (f.get(ch) ?? 0) + 1);
|
|
574
|
+
let h = 0;
|
|
575
|
+
for (const n of f.values()) {
|
|
576
|
+
const p = n / s.length;
|
|
577
|
+
h -= p * Math.log2(p);
|
|
578
|
+
}
|
|
579
|
+
return h;
|
|
580
|
+
}
|
|
581
|
+
/** Write a pack directory from the user's own objects (+ pack.lock). Refuses secrets and bad names. Returns the lock. */
|
|
582
|
+
export function exportPack(outDir, spec) {
|
|
583
|
+
if (!/^[a-z][a-z0-9-]{0,39}$/.test(spec.name))
|
|
584
|
+
throw new PackError('pack name: lowercase letters, digits, dashes', 'manifest');
|
|
585
|
+
const version = spec.version ?? '1.0.0';
|
|
586
|
+
if (!/^\d+\.\d+\.\d+$/.test(version))
|
|
587
|
+
throw new PackError('version: x.y.z', 'manifest');
|
|
588
|
+
if (existsSync(outDir) && readdirSync(outDir).length)
|
|
589
|
+
throw new PackError(`${outDir} is not empty`, 'io');
|
|
590
|
+
const manifest = { name: spec.name, version, publisher: spec.publisher, ...(spec.description ? { description: spec.description.slice(0, 300) } : {}), requires: { dexterm: '>=0.1.0', api: 1 }, contents: {} };
|
|
591
|
+
const files = [];
|
|
592
|
+
for (const ws of spec.workspaces ?? []) {
|
|
593
|
+
const base = ws.name.includes('/') ? ws.name.split('/')[1] : ws.name;
|
|
594
|
+
files.push([`workspaces/${base}.json`, JSON.stringify({ ...ws, name: base }, null, 2) + '\n']);
|
|
595
|
+
(manifest.contents.workspaces ??= []).push(`workspaces/${base}.json`);
|
|
596
|
+
}
|
|
597
|
+
for (const t of spec.themes ?? []) {
|
|
598
|
+
const { name, ...body } = t;
|
|
599
|
+
files.push([`themes/${name}.json`, JSON.stringify(body, null, 2) + '\n']);
|
|
600
|
+
(manifest.contents.themes ??= []).push(`themes/${name}.json`);
|
|
601
|
+
}
|
|
602
|
+
if (spec.rules?.length) {
|
|
603
|
+
const lines = ['# rules exported by dexterm — one per line; parsed by the rules grammar, never eval\'d', ...spec.rules.map((r) => ruleTextOf(r))];
|
|
604
|
+
files.push(['rules/rules.rules', lines.join('\n') + '\n']);
|
|
605
|
+
manifest.contents.rules = ['rules/rules.rules'];
|
|
606
|
+
}
|
|
607
|
+
if (spec.keymap && (Object.keys(spec.keymap.global ?? {}).length || Object.keys(spec.keymap.screens ?? {}).length)) {
|
|
608
|
+
files.push(['keymap.json', JSON.stringify(spec.keymap, null, 2) + '\n']);
|
|
609
|
+
manifest.contents.keymap = 'keymap.json';
|
|
610
|
+
}
|
|
611
|
+
const errors = validate('packManifest', manifest);
|
|
612
|
+
if (errors.length)
|
|
613
|
+
throw new PackError(`manifest: ${errors[0].path || '/'} ${errors[0].msg}`, 'manifest');
|
|
614
|
+
files.unshift([PACK_MANIFEST, JSON.stringify(manifest, null, 2) + '\n']);
|
|
615
|
+
for (const [p, text] of files) {
|
|
616
|
+
const s = scanForSecrets(text);
|
|
617
|
+
if (s)
|
|
618
|
+
throw new PackError(`refused to export ${p}: ${s}`, 'secret');
|
|
619
|
+
}
|
|
620
|
+
mkdirSync(outDir, { recursive: true, mode: 0o700 });
|
|
621
|
+
for (const [p, text] of files) {
|
|
622
|
+
mkdirSync(join(outDir, p, '..'), { recursive: true });
|
|
623
|
+
writeFileSync(join(outDir, p), text, { mode: 0o600 });
|
|
624
|
+
}
|
|
625
|
+
return writeLock(outDir);
|
|
626
|
+
}
|
|
627
|
+
/** One exported rule line, named so the importer gets a stable id; re-parsed so the line is exactly what an importer will parse. */
|
|
628
|
+
function ruleTextOf(r) {
|
|
629
|
+
const name = r.name && !r.name.startsWith('when ') ? r.name : r.id;
|
|
630
|
+
const line = ruleToText({ ...r, name });
|
|
631
|
+
ruleToJson(line); // throws if the export would not round-trip through the grammar
|
|
632
|
+
return line;
|
|
633
|
+
}
|