dexterm-cli 0.1.6 → 0.2.0
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/dist/packages/agents/src/schema.js +22 -1
- package/dist/packages/agents/src/spectator.js +3 -1
- package/dist/packages/agents/src/story.js +11 -2
- package/dist/packages/agents/src/tiers.js +36 -2
- package/dist/packages/agents/src/tools.js +35 -2
- package/dist/packages/autotrade/assets/exit-profile.schema.json +492 -0
- package/dist/packages/autotrade/assets/limits.schema.json +569 -0
- package/dist/packages/autotrade/assets/profiles/moonbag.json +46 -0
- package/dist/packages/autotrade/assets/profiles/runner.json +88 -0
- package/dist/packages/autotrade/assets/profiles/scalp.json +88 -0
- package/dist/packages/autotrade/assets/profiles/swing.json +88 -0
- package/dist/packages/autotrade/assets/strategies/moonbag.json +101 -0
- package/dist/packages/autotrade/assets/strategies/runner.json +101 -0
- package/dist/packages/autotrade/assets/strategies/sample-dip-ladder.json +188 -0
- package/dist/packages/autotrade/assets/strategies/scalp.json +98 -0
- package/dist/packages/autotrade/assets/strategies/swing.json +97 -0
- package/dist/packages/autotrade/assets/strategies/yolo.json +157 -0
- package/dist/packages/autotrade/assets/tapes/yolo-rehearsal.json +1 -0
- package/dist/packages/autotrade/examples/executor/README.md +70 -0
- package/dist/packages/autotrade/examples/executor/jsonl-tail-executor.mjs +57 -0
- package/dist/packages/autotrade/examples/executor/kit.mjs +144 -0
- package/dist/packages/autotrade/examples/executor/signal-cmd-executor.mjs +25 -0
- package/dist/packages/autotrade/examples/executor/telegram-bot-recipe.md +47 -0
- package/dist/packages/autotrade/examples/executor/webhook-executor.mjs +47 -0
- package/dist/packages/autotrade/src/account.js +74 -0
- package/dist/packages/autotrade/src/backtest.js +358 -0
- package/dist/packages/autotrade/src/basis-text.js +32 -0
- package/dist/packages/autotrade/src/chat/approvals.js +484 -0
- package/dist/packages/autotrade/src/chat/audit.js +140 -0
- package/dist/packages/autotrade/src/chat/config.js +95 -0
- package/dist/packages/autotrade/src/chat/connector.js +24 -0
- package/dist/packages/autotrade/src/chat/control.js +104 -0
- package/dist/packages/autotrade/src/chat/fake.js +100 -0
- package/dist/packages/autotrade/src/chat/format.js +71 -0
- package/dist/packages/autotrade/src/chat/hub.js +187 -0
- package/dist/packages/autotrade/src/chat/index.js +18 -0
- package/dist/packages/autotrade/src/chat/notify.js +302 -0
- package/dist/packages/autotrade/src/chat/pairing.js +46 -0
- package/dist/packages/autotrade/src/chat/redact.js +48 -0
- package/dist/packages/autotrade/src/chat/secrets.js +106 -0
- package/dist/packages/autotrade/src/chat/service.js +251 -0
- package/dist/packages/autotrade/src/chat/telegram.js +290 -0
- package/dist/packages/autotrade/src/desk-executor.js +188 -0
- package/dist/packages/autotrade/src/engine.js +401 -0
- package/dist/packages/autotrade/src/execution-port.js +97 -0
- package/dist/packages/autotrade/src/executor-health.js +217 -0
- package/dist/packages/autotrade/src/fleet-config.js +169 -0
- package/dist/packages/autotrade/src/fleet-control.js +276 -0
- package/dist/packages/autotrade/src/fleet.js +642 -0
- package/dist/packages/autotrade/src/forward.js +355 -0
- package/dist/packages/autotrade/src/index.js +72 -0
- package/dist/packages/autotrade/src/journey.js +477 -0
- package/dist/packages/autotrade/src/ledger.js +108 -0
- package/dist/packages/autotrade/src/limits.js +682 -0
- package/dist/packages/autotrade/src/live-source.js +182 -0
- package/dist/packages/autotrade/src/monitor-daemon.js +715 -0
- package/dist/packages/autotrade/src/monitor.js +190 -0
- package/dist/packages/autotrade/src/ollama-sessions.js +137 -0
- package/dist/packages/autotrade/src/outbox.js +394 -0
- package/dist/packages/autotrade/src/paper-accounts.js +72 -0
- package/dist/packages/autotrade/src/paper-limits.js +87 -0
- package/dist/packages/autotrade/src/real-mode.js +688 -0
- package/dist/packages/autotrade/src/receipts.js +330 -0
- package/dist/packages/autotrade/src/replay.js +198 -0
- package/dist/packages/autotrade/src/report.js +168 -0
- package/dist/packages/autotrade/src/runner-config.js +99 -0
- package/dist/packages/autotrade/src/runner.js +923 -0
- package/dist/packages/autotrade/src/safety.js +243 -0
- package/dist/packages/autotrade/src/service-guard.js +125 -0
- package/dist/packages/autotrade/src/service-install.js +213 -0
- package/dist/packages/autotrade/src/service-recover.js +232 -0
- package/dist/packages/autotrade/src/service.js +289 -0
- package/dist/packages/autotrade/src/session.js +269 -0
- package/dist/packages/autotrade/src/sessions.js +503 -0
- package/dist/packages/autotrade/src/signal-channels.js +312 -0
- package/dist/packages/autotrade/src/signal.js +196 -0
- package/dist/packages/autotrade/src/store.js +250 -0
- package/dist/packages/autotrade/src/strategy.js +258 -0
- package/dist/packages/autotrade/src/trade-ledger.js +241 -0
- package/dist/packages/autotrade/src/yolo.js +140 -0
- package/dist/packages/cli/assets/claude/analyst-system.md +4 -4
- package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +1 -1
- package/dist/packages/cli/src/ai-cmd.js +412 -0
- package/dist/packages/cli/src/analyst/backend.js +83 -15
- package/dist/packages/cli/src/analyst/codex.js +15 -9
- package/dist/packages/cli/src/analyst/ollama.js +263 -0
- package/dist/packages/cli/src/analyst/session.js +5 -2
- package/dist/packages/cli/src/app-help-gen.js +56 -8
- package/dist/packages/cli/src/autotrade-cmd/audit.js +74 -0
- package/dist/packages/cli/src/autotrade-cmd/backtest.js +216 -0
- package/dist/packages/cli/src/autotrade-cmd/executor.js +91 -0
- package/dist/packages/cli/src/autotrade-cmd/flatten.js +64 -0
- package/dist/packages/cli/src/autotrade-cmd/fleet.js +401 -0
- package/dist/packages/cli/src/autotrade-cmd/forward.js +283 -0
- package/dist/packages/cli/src/autotrade-cmd/journey.js +208 -0
- package/dist/packages/cli/src/autotrade-cmd/limits.js +393 -0
- package/dist/packages/cli/src/autotrade-cmd/receipt.js +63 -0
- package/dist/packages/cli/src/autotrade-cmd/report.js +79 -0
- package/dist/packages/cli/src/autotrade-cmd/requests.js +79 -0
- package/dist/packages/cli/src/autotrade-cmd/run.js +657 -0
- package/dist/packages/cli/src/autotrade-cmd/service.js +873 -0
- package/dist/packages/cli/src/autotrade-cmd/signal.js +824 -0
- package/dist/packages/cli/src/autotrade-cmd/stop.js +76 -0
- package/dist/packages/cli/src/autotrade-cmd/strategies.js +198 -0
- package/dist/packages/cli/src/autotrade-cmd/strategy-control.js +174 -0
- package/dist/packages/cli/src/autotrade-cmd/telegram-host.js +101 -0
- package/dist/packages/cli/src/autotrade-cmd.js +214 -0
- package/dist/packages/cli/src/display.js +156 -17
- package/dist/packages/cli/src/doctor.js +62 -3
- package/dist/packages/cli/src/export.js +10 -2
- package/dist/packages/cli/src/index.js +1 -1
- package/dist/packages/cli/src/ledger-cmd.js +290 -0
- package/dist/packages/cli/src/live/chart-fills.js +46 -0
- package/dist/packages/cli/src/live/index.js +23 -89
- package/dist/packages/cli/src/live/screens/token-live.js +9 -6
- package/dist/packages/cli/src/live/trade.js +12 -457
- package/dist/packages/cli/src/live/watchlist.js +1 -1
- package/dist/packages/cli/src/main.js +147 -190
- package/dist/packages/cli/src/mcp-cmd.js +7 -1
- package/dist/packages/cli/src/plan-cmd.js +1 -1
- package/dist/packages/cli/src/pnl-app.js +31 -21
- package/dist/packages/cli/src/pnl-card.js +162 -21
- package/dist/packages/cli/src/popup.js +22 -6
- package/dist/packages/cli/src/record-cmd.js +141 -20
- package/dist/packages/cli/src/screens/agent-hud.js +34 -2
- package/dist/packages/cli/src/screens/analyst-session.js +7 -4
- package/dist/packages/cli/src/screens/analyst.js +3 -2
- package/dist/packages/cli/src/screens/autotrade-journey.js +134 -0
- package/dist/packages/cli/src/screens/autotrade-keys.js +182 -0
- package/dist/packages/cli/src/screens/autotrade-proposals.js +123 -0
- package/dist/packages/cli/src/screens/autotrade-screen.js +776 -0
- package/dist/packages/cli/src/screens/autotrade-ui.js +650 -0
- package/dist/packages/cli/src/screens/autotrade-worker.js +91 -0
- package/dist/packages/cli/src/screens/brain.js +37 -11
- package/dist/packages/cli/src/screens/overlay.js +5 -2
- package/dist/packages/cli/src/screens/plans.js +5 -17
- package/dist/packages/cli/src/screens/pnl-chip.js +9 -8
- package/dist/packages/cli/src/screens/positions.js +6 -28
- package/dist/packages/cli/src/screens/ticket.js +38 -205
- package/dist/packages/cli/src/screens/token.js +3 -3
- package/dist/packages/cli/src/state.js +25 -19
- package/dist/packages/cli/src/telegram-cmd.js +454 -0
- package/dist/packages/cli/src/trade.js +3 -128
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/cli/src/wallet-cmd.js +39 -340
- package/dist/packages/cli/src/wallet-command.js +8 -11
- package/dist/packages/cli/src/widgets.js +0 -12
- package/dist/packages/custom/src/config.js +20 -2
- package/dist/packages/custom/src/schema.js +35 -10
- package/dist/packages/custom/src/workspace.js +3 -3
- package/dist/packages/data/src/basis.js +191 -0
- package/dist/packages/data/src/detail.js +53 -2
- package/dist/packages/data/src/index.js +0 -14
- package/dist/packages/data/src/store.js +14 -5
- package/dist/packages/data/src/trade/agent/paper-desk.js +228 -16
- package/dist/packages/data/src/trade/agent/rendezvous.js +3 -2
- package/dist/packages/data/src/trade/bridge.js +4 -102
- package/dist/packages/data/src/trade/ledger/export.js +101 -0
- package/dist/packages/data/src/trade/ledger/file.js +135 -0
- package/dist/packages/data/src/trade/ledger/fold.js +191 -0
- package/dist/packages/data/src/trade/ledger/index.js +9 -0
- package/dist/packages/data/src/trade/ledger/schema.js +83 -0
- package/dist/packages/data/src/trade/ledger/sources.js +307 -0
- package/dist/packages/data/src/trade/ledger/stats.js +123 -0
- package/dist/packages/data/src/trade/ledger/tags.js +116 -0
- package/dist/packages/data/src/trade/ledger/view.js +51 -0
- package/dist/packages/data/src/trade/plans/index.js +0 -1
- package/dist/packages/data/src/trade/plans/service.js +3 -27
- package/dist/packages/mcp/assets/kb/app-help/autotrade-chat.md +48 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-executor-kit.md +44 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-journey.md +46 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-limits.md +48 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-parallel.md +42 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-real-signal.md +45 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-runner.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-stop.md +37 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-strategies.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-telegram.md +40 -0
- package/dist/packages/mcp/assets/kb/app-help/chart-trade-markers.md +42 -0
- package/dist/packages/mcp/assets/kb/app-help/link-a-wallet.md +4 -10
- package/dist/packages/mcp/assets/kb/app-help/live-trading.md +38 -0
- package/dist/packages/mcp/assets/kb/app-help/live-yolo.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/order-plans.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/price-or-marketcap.md +5 -2
- package/dist/packages/mcp/assets/kb/app-help/screen-analyst.md +3 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-autotrade.md +229 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-kol.md +3 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-newpairs.md +3 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +3 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-screener.md +3 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-token.md +3 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-trending.md +3 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-watchlist.md +3 -0
- package/dist/packages/mcp/assets/kb/app-help/standing-mandates.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/trade-ledger.md +54 -0
- package/dist/packages/mcp/assets/kb/app-help/use-local-model.md +38 -0
- package/dist/packages/mcp/assets/kb/docs/docs-agents-agent-paper-trading.md +4 -1
- package/dist/packages/mcp/assets/kb/docs/docs-agents-reading-your-trade-ledger.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-agents-untrusted-by-default.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-if-claude-is-missing.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-use-a-local-model-instead.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-what-it-sees.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-backtests-and-their-limits.md +56 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-change-a-strategy-by-chat.md +51 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-checking-the-record-of-a-run.md +28 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-comparing-them.md +41 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-exits-and-wakes.md +76 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-how-a-stop-reaches-the-runner.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-on-the-q-screen.md +38 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-one-strategy-or-all-of-them.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-paper-accounts.md +50 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-presets.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-running-it-all-day.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-sessions-and-their-limits.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-several-strategies-at-once.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-stopping-it.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-strategies-and-presets.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-always-on-service.md +57 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-autotrade-json-file.md +48 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-heartbeat.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-run-file-and-the-heartbeat.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-runner-and-candidate-sessions.md +45 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-each-session-shows-first.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-costs-claude-usage-per-session.md +58 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-is-paper-self-directed.md +50 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-will-not-do.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-which-account-day-caps-and-the-usage-guard.md +44 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-your-strategies.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +22 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-beginners.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-1-ask-for-a-strategy.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-2-backtest-it-held-out-split.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-3-forward-test-it-on-live-data.md +44 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-4-promote-it-to-always-on-paper.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-5-real-only-if-you-choose.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-ladder.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-report.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-what-the-analyst-knows.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat.md +24 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-markers.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-y-axis.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-environment-variables.md +4 -1
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-files-on-disk.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-price-display-mode.md +8 -6
- package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-2-the-one-key-mirror-ticket-a-proposal.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-3-capped-auto-mirror-default-off-less.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-what-is-proven-what-is-not.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-data-sources-sources.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-first-run.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-executor-health.md +38 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-paper-vs-real.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-digest.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-executor-kit.md +48 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-whole-path.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-what-dexterm-does-and-never-does.md +44 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real-your-limits.md +77 -0
- package/dist/packages/mcp/assets/kb/docs/docs-going-real.md +24 -0
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-changed-in-0-1-7.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-real-mode-your-strategy-s-trades-to-your.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-stopping.md +28 -0
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-the-short-version.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-using-a-trading-bot-with-dexterm.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does-not-do.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading.md +23 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-another-machine.md +28 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-autotrade-on-a-local-model.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-check-your-model-dexterm-ai-probe.md +38 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-how-we-checked.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-models-without-tool-calling.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-pick-the-engine-globally-or-per-surface.md +43 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-restriction-profiles.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-set-it-up-in-two-minutes.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-troubleshooting.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-what-runs-on-a-local-model.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-what-stays-outside-the-model.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-which-model.md +38 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-your-own-agent-on-the-local-model.md +28 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama.md +28 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-letting-an-agent-paper-trade-experimental.md +3 -4
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-link-a-wallet-read-only.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-order-plans-and-standing-mandates.md +3 -4
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-panic.md +4 -7
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-paper-only-mandates.md +6 -7
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-pnl-cards.md +7 -7
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-safety-model.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-ticket.md +3 -4
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-trade-ledger.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-wallet.md +4 -5
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-trade-moments.md +4 -5
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +4 -5
- package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-buy-and-sell-markers.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-what-it-shows.md +2 -1
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-receipts-what-your-executor-did.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-safety.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-set-it-up.md +47 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-channels.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-signal.md +48 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-is.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-will-not-do.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-where-you-see-it.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-autotrade-paper.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-order-plans.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-screens-token-screen.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-brain-view-and-the-pnl-chip.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-approve-one-tap-entries.md +50 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-control-the-commands.md +43 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-if-something-goes-wrong.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-notify-what-reaches-your-phone.md +41 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-set-it-up.md +47 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-the-record.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-will-not-do.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram.md +23 -0
- package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-common-questions.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-doctor-lines.md +4 -0
- package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-voice.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-yolo-1-replay-it-on-your-own-history.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-yolo-2-run-it-on-paper.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-yolo-3-watch-it-and-stop-it.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-yolo-the-rules.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-yolo-what-it-is.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-yolo-what-it-will-not-do.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-yolo.md +26 -0
- package/dist/packages/mcp/assets/kb/entries/autotrade.md +40 -0
- package/dist/packages/mcp/src/app-help.js +1 -1
- package/dist/packages/mcp/src/autotrade-tools.js +1107 -0
- package/dist/packages/mcp/src/backtest-requests.js +71 -0
- package/dist/packages/mcp/src/engine/config.js +159 -0
- package/dist/packages/mcp/src/engine/index.js +10 -0
- package/dist/packages/mcp/src/engine/loop.js +101 -0
- package/dist/packages/mcp/src/engine/mcp-client.js +123 -0
- package/dist/packages/mcp/src/engine/ollama.js +258 -0
- package/dist/packages/mcp/src/engine/probe.js +110 -0
- package/dist/packages/mcp/src/engine/reason.js +35 -0
- package/dist/packages/mcp/src/engine/restrictions.js +111 -0
- package/dist/packages/mcp/src/index.js +7 -1
- package/dist/packages/mcp/src/ledger-tools.js +494 -0
- package/dist/packages/mcp/src/paper-agent.js +2 -0
- package/dist/packages/mcp/src/server.js +138 -19
- package/dist/packages/packs/src/backtest.js +3 -1
- package/dist/packages/popup/src/pages/chart-lib.js +134 -21
- package/dist/packages/popup/src/pages/chart.css +9 -0
- package/dist/packages/popup/src/pages/chart.html +3 -1
- package/dist/packages/popup/src/pages/chart.js +104 -7
- package/dist/packages/popup/src/protocol.js +2 -0
- package/dist/packages/popup/src/server.js +4 -0
- package/dist/packages/recorder/src/index.js +2 -0
- package/dist/packages/recorder/src/recorder.js +63 -6
- package/dist/packages/recorder/src/retention.js +100 -0
- package/dist/packages/recorder/src/schema.js +34 -1
- package/dist/packages/recorder/src/ticks.js +237 -0
- package/dist/packages/tui/src/index.js +1 -0
- package/dist/packages/tui/src/keymap.js +28 -1
- package/dist/packages/tui/src/markers.js +219 -0
- package/dist/packages/tui/src/widgets.js +149 -2
- package/package.json +2 -1
- package/dist/packages/data/src/trade/caps.js +0 -323
- package/dist/packages/data/src/trade/executor.js +0 -933
- package/dist/packages/data/src/trade/plans/executor-live.js +0 -220
- package/dist/packages/data/src/trade/rpc-exec.js +0 -229
- package/dist/packages/data/src/trade/signer/client.js +0 -197
- package/dist/packages/data/src/trade/signer/fast-wallet.js +0 -154
- package/dist/packages/data/src/trade/signer/host.js +0 -97
- package/dist/packages/data/src/trade/signer/main.js +0 -121
- package/dist/packages/data/src/trade/signer/probe-builder.js +0 -85
- package/dist/packages/data/src/trade/signer/protocol.js +0 -92
- package/dist/packages/data/src/trade/signer/service.js +0 -400
- package/dist/packages/data/src/trade/signer/sol-usd-feed.js +0 -98
- package/dist/packages/data/src/trade/sol-usd.js +0 -86
- package/dist/packages/data/src/trade/swap-build.js +0 -108
- package/dist/packages/data/src/trade/token-delta.js +0 -201
- package/dist/packages/data/vendor/solana-web3/LICENSE +0 -20
- package/dist/packages/data/vendor/solana-web3/NOTICE.md +0 -13
- package/dist/packages/data/vendor/solana-web3/index.iife.min.js +0 -20
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-a-live-ticket.md +0 -42
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-before-you-press-size-vs-caps.md +0 -34
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-devnet-signer-mainnet-token.md +0 -35
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-jupiter-rate-limits.md +0 -33
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-link-a-wallet.md +0 -32
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-live-trading-devnet-first.md +0 -30
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-mainnet.md +0 -31
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-live-fire-path-devnet-the-fast-wallet.md +0 -41
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-signer-process.md +0 -34
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-what-the-live-path-keeps.md +0 -37
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-your-first-live-trade.md +0 -41
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// dexterm autotrade — safety: stop, flatten, pause, the kill switch every step, the dead-man, the run audit and the
|
|
2
|
+
// session isolation proof (task-1509 AT-7, AUTOTRADE-SPEC §6.3, §6.4). PAPER ONLY.
|
|
3
|
+
//
|
|
4
|
+
// Control file: $DEXTERM_HOME/autotrade/control.json (`dexterm-autotrade-control/1`) carries ONE request for ONE run
|
|
5
|
+
// (its runId): stop (soft: no new entries, every session closed, orders dropped, mechanical exits keep running),
|
|
6
|
+
// flatten (a stop, then every position sold at its next tick), pause / resume-entries (new entries only). The CLI
|
|
7
|
+
// (`dexterm autotrade stop|flatten`) and the Q screen (`!`, `f` + y, `p`) write it; the runner re-reads it on every
|
|
8
|
+
// step (a stat, then a parse when it changed). A request for another run never touches this one. A file that cannot
|
|
9
|
+
// be read counts as a stop (fail closed), like the panic file. A stop or flatten is final for the run: a later
|
|
10
|
+
// request can escalate a stop to a flatten but never lift it, and nothing here lifts the panic file.
|
|
11
|
+
//
|
|
12
|
+
// RunnerSafety.step() is what the runner calls every step. It folds in:
|
|
13
|
+
// - the panic file (`dexterm wallet panic`, KillSwitch), re-read every step; the halt is sticky for the run, and
|
|
14
|
+
// a new run refuses to start while it is engaged (resume is `dexterm wallet resume`, a keypress on a real TTY);
|
|
15
|
+
// - the control file;
|
|
16
|
+
// - the dead-man for entries: no new entries while the runner's own heartbeat cannot be written (the TUI and the
|
|
17
|
+
// monitor would see it as dead), or while the position monitor is down when it runs the exits (desk executor).
|
|
18
|
+
// Those lift by themselves; mechanical exits never wait on any of them.
|
|
19
|
+
//
|
|
20
|
+
// Audit: auditRun() recomputes the hash chain of runs/<runId>.jsonl (verifyChain) and checks it against the run's
|
|
21
|
+
// summary (written on every heartbeat with the head at that moment), so lines cut from the END of a ledger, which a
|
|
22
|
+
// chain alone cannot see, show up as missing events.
|
|
23
|
+
//
|
|
24
|
+
// Isolation: sessionIsolation() reads a session's stream-json init event: tools and MCP servers must be 0 (the
|
|
25
|
+
// session is ended otherwise, sessions.ts); plugins, agents, skills and slash commands are counted for the ledger.
|
|
26
|
+
// The user's plugins and agents still list as loaded with these flags; with no tools they cannot act (§4).
|
|
27
|
+
//
|
|
28
|
+
// Nothing here holds a key, touches a wallet, the signer or the network, and no model can reach any of it: sessions
|
|
29
|
+
// have no tools, and only a keypress or a command the user runs writes the control file.
|
|
30
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
31
|
+
import { join } from 'node:path';
|
|
32
|
+
import { GENESIS, RUN_SUMMARY_SCHEMA, readLedgerFile, runFiles, runsDir, verifyChain, writeJsonAtomic } from "./ledger.js";
|
|
33
|
+
import { runLockPath, runnerRendezvousPath } from "./runner.js";
|
|
34
|
+
export const CONTROL_SCHEMA = 'dexterm-autotrade-control/1';
|
|
35
|
+
const ACTIONS = ['stop', 'flatten', 'pause', 'resume-entries'];
|
|
36
|
+
export const controlPath = (home) => join(home, 'autotrade', 'control.json');
|
|
37
|
+
/** The request in the control file, `unreadable` when it exists but cannot be parsed, or null when there is none. */
|
|
38
|
+
export function readControl(home) {
|
|
39
|
+
const p = controlPath(home);
|
|
40
|
+
if (!existsSync(p))
|
|
41
|
+
return null;
|
|
42
|
+
try {
|
|
43
|
+
const d = JSON.parse(readFileSync(p, 'utf8'));
|
|
44
|
+
if (d.schema !== CONTROL_SCHEMA || typeof d.runId !== 'string' || !ACTIONS.includes(d.action) || typeof d.at !== 'number')
|
|
45
|
+
return 'unreadable';
|
|
46
|
+
return { schema: CONTROL_SCHEMA, runId: d.runId, action: d.action, at: d.at, by: d.by === 'tui' || d.by === 'chat' ? d.by : 'cli' };
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return 'unreadable';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const FINAL = ['stop', 'flatten'];
|
|
53
|
+
/**
|
|
54
|
+
* Write a request for a run. A stop or flatten already on file for the same run is never lifted: pause and
|
|
55
|
+
* resume-entries are refused, a second stop is a no-op, and only stop → flatten escalates.
|
|
56
|
+
*/
|
|
57
|
+
export function requestControl(home, runId, action, by, now = Date.now()) {
|
|
58
|
+
const cur = readControl(home);
|
|
59
|
+
if (cur && cur !== 'unreadable' && cur.runId === runId && FINAL.includes(cur.action)) {
|
|
60
|
+
if (cur.action === action || (cur.action === 'flatten' && action === 'stop'))
|
|
61
|
+
return { ok: true, request: cur, already: true };
|
|
62
|
+
if (action !== 'flatten')
|
|
63
|
+
return { ok: false, why: `the runner is already ${cur.action === 'stop' ? 'stopping' : 'flattening'}; only a flatten can follow a stop`, current: cur };
|
|
64
|
+
}
|
|
65
|
+
const request = { schema: CONTROL_SCHEMA, runId, action, at: now, by };
|
|
66
|
+
mkdirSync(join(home, 'autotrade'), { recursive: true, mode: 0o700 });
|
|
67
|
+
writeJsonAtomic(controlPath(home), request);
|
|
68
|
+
return { ok: true, request, already: false };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* At a runner's start: a request left for an older run is removed (it can never apply to this one). Returns false
|
|
72
|
+
* when the file cannot be read: the new run must refuse to start until the user looks at it (fail closed).
|
|
73
|
+
*/
|
|
74
|
+
export function clearStaleControl(home, runId) {
|
|
75
|
+
const cur = readControl(home);
|
|
76
|
+
if (cur === 'unreadable')
|
|
77
|
+
return false;
|
|
78
|
+
if (cur && cur.runId !== runId)
|
|
79
|
+
rmSync(controlPath(home), { force: true });
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
/** At a runner's end: remove its own request (a later run never sees it). */
|
|
83
|
+
export function clearControl(home, runId) {
|
|
84
|
+
const cur = readControl(home);
|
|
85
|
+
if (cur && cur !== 'unreadable' && cur.runId === runId)
|
|
86
|
+
rmSync(controlPath(home), { force: true });
|
|
87
|
+
}
|
|
88
|
+
export const HEARTBEAT_STALE_MS = 30_000;
|
|
89
|
+
export class RunnerSafety {
|
|
90
|
+
haltWhy = null;
|
|
91
|
+
haltDetail = '';
|
|
92
|
+
flattening = false;
|
|
93
|
+
/** A stop or flatten was asked for (also after a panic halt): the run ends once nothing is held. */
|
|
94
|
+
stopAsked = false;
|
|
95
|
+
sig = '';
|
|
96
|
+
ctl = null;
|
|
97
|
+
o;
|
|
98
|
+
constructor(o) { this.o = o; }
|
|
99
|
+
/** The control request, re-parsed only when the file changed (a stat per step). */
|
|
100
|
+
control() {
|
|
101
|
+
const p = controlPath(this.o.home);
|
|
102
|
+
let sig = '';
|
|
103
|
+
try {
|
|
104
|
+
const st = statSync(p);
|
|
105
|
+
sig = `${st.mtimeMs}:${st.size}:${st.ino}`;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
sig = '';
|
|
109
|
+
}
|
|
110
|
+
if (sig !== this.sig) {
|
|
111
|
+
this.sig = sig;
|
|
112
|
+
this.ctl = sig ? readControl(this.o.home) : null;
|
|
113
|
+
}
|
|
114
|
+
return this.ctl;
|
|
115
|
+
}
|
|
116
|
+
step() {
|
|
117
|
+
const halt = (why, detail) => { if (!this.haltWhy) {
|
|
118
|
+
this.haltWhy = why;
|
|
119
|
+
this.haltDetail = detail;
|
|
120
|
+
} };
|
|
121
|
+
const panic = this.o.killSwitch?.engaged() ?? null;
|
|
122
|
+
if (panic)
|
|
123
|
+
halt('kill-switch', `panic file: ${panic.reason}`);
|
|
124
|
+
const c = this.control();
|
|
125
|
+
let pause = null;
|
|
126
|
+
if (c === 'unreadable') {
|
|
127
|
+
halt('control-unreadable', 'the control file cannot be read (treated as a stop)');
|
|
128
|
+
this.stopAsked = true;
|
|
129
|
+
}
|
|
130
|
+
else if (c && c.runId === this.o.runId) {
|
|
131
|
+
if (c.action === 'stop' || c.action === 'flatten') {
|
|
132
|
+
halt(c.action, `requested from the ${c.by === 'tui' ? 'Q screen' : c.by === 'chat' ? 'paired chat' : 'command line'}`);
|
|
133
|
+
this.stopAsked = true;
|
|
134
|
+
if (c.action === 'flatten')
|
|
135
|
+
this.flattening = true;
|
|
136
|
+
}
|
|
137
|
+
else if (c.action === 'pause')
|
|
138
|
+
pause = 'paused';
|
|
139
|
+
}
|
|
140
|
+
if (!pause && (this.o.heartbeatAgeMs?.() ?? 0) > (this.o.staleMs ?? HEARTBEAT_STALE_MS))
|
|
141
|
+
pause = 'heartbeat-stale';
|
|
142
|
+
if (!pause && this.o.monitorAlive && !this.o.monitorAlive())
|
|
143
|
+
pause = 'monitor-heartbeat-stale';
|
|
144
|
+
return { halt: this.haltWhy, ...(this.haltWhy ? { detail: this.haltDetail } : {}), flatten: this.flattening, pauseEntries: pause, endWhenFlat: this.stopAsked };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// ───────────────────────────── the active runner ─────────────────────────────
|
|
148
|
+
const alive = (pid) => { try {
|
|
149
|
+
process.kill(pid, 0);
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
return e.code === 'EPERM';
|
|
154
|
+
} };
|
|
155
|
+
/** The runner holding run.lock with a live pid (and what its rendezvous says), or null. */
|
|
156
|
+
export function activeRunner(home) {
|
|
157
|
+
let lock;
|
|
158
|
+
try {
|
|
159
|
+
lock = JSON.parse(readFileSync(runLockPath(home), 'utf8'));
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
if (typeof lock.pid !== 'number' || typeof lock.runId !== 'string' || !alive(lock.pid))
|
|
165
|
+
return null;
|
|
166
|
+
const r = readRendezvousLoose(home);
|
|
167
|
+
const same = r && r.runId === lock.runId;
|
|
168
|
+
return {
|
|
169
|
+
runId: lock.runId, pid: lock.pid, startedAt: lock.startedAt,
|
|
170
|
+
state: same && typeof r.state === 'string' ? r.state : null,
|
|
171
|
+
haltReason: same && typeof r.haltReason === 'string' ? r.haltReason : null,
|
|
172
|
+
heartbeatAt: same && typeof r.heartbeatAt === 'number' ? r.heartbeatAt : null,
|
|
173
|
+
positions: same && Array.isArray(r.positions) ? r.positions.length : 0,
|
|
174
|
+
sessions: same && Array.isArray(r.sessions) ? r.sessions.length : 0,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function readRendezvousLoose(home) {
|
|
178
|
+
try {
|
|
179
|
+
return JSON.parse(readFileSync(runnerRendezvousPath(home), 'utf8'));
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/** Run ids with a ledger under runs/, oldest first (ids sort by start time). */
|
|
186
|
+
export function listRunIds(home) {
|
|
187
|
+
const dir = runsDir(home);
|
|
188
|
+
if (!existsSync(dir))
|
|
189
|
+
return [];
|
|
190
|
+
return readdirSync(dir).filter((f) => f.endsWith('.jsonl') && !f.endsWith('.stream.jsonl')).map((f) => f.slice(0, -'.jsonl'.length)).filter((id) => { try {
|
|
191
|
+
runFiles(home, id);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return false;
|
|
196
|
+
} }).sort();
|
|
197
|
+
}
|
|
198
|
+
/** Recompute one run's chain and check it against its summary. */
|
|
199
|
+
export function auditRun(home, runId) {
|
|
200
|
+
const files = runFiles(home, runId);
|
|
201
|
+
const active = activeRunner(home);
|
|
202
|
+
const running = active?.runId === runId;
|
|
203
|
+
const base = { runId, ledger: files.ledger, running };
|
|
204
|
+
let events;
|
|
205
|
+
try {
|
|
206
|
+
events = readLedgerFile(files.ledger);
|
|
207
|
+
}
|
|
208
|
+
catch (e) {
|
|
209
|
+
return { ...base, events: 0, ok: false, head: null, brokenAt: 0, error: `cannot read the ledger: ${e.message.slice(0, 160)}`, ended: false, summary: null, strategy: null };
|
|
210
|
+
}
|
|
211
|
+
const chain = verifyChain(events);
|
|
212
|
+
const start = events.find((e) => e.kind === 'run_start');
|
|
213
|
+
const strategy = start && typeof start.strategy === 'string' ? { id: start.strategy, version: String(start.version ?? ''), hash: String(start.strategyHash ?? '') } : null;
|
|
214
|
+
// A parallel run's own ledger (task-1529) closes with fleet_end; each strategy's ledger with run_end.
|
|
215
|
+
const ended = events.length > 0 && (events[events.length - 1].kind === 'run_end' || events[events.length - 1].kind === 'fleet_end');
|
|
216
|
+
let summary = null;
|
|
217
|
+
try {
|
|
218
|
+
const s = JSON.parse(readFileSync(files.summary, 'utf8'));
|
|
219
|
+
if (s.schema === RUN_SUMMARY_SCHEMA && typeof s.events === 'number' && typeof s.ledgerHead === 'string') {
|
|
220
|
+
const at = s.events === 0 ? GENESIS : events[s.events - 1]?.hash;
|
|
221
|
+
summary = { events: s.events, head: s.ledgerHead, matches: at === s.ledgerHead };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
summary = null;
|
|
226
|
+
}
|
|
227
|
+
if (!chain.ok)
|
|
228
|
+
return { ...base, events: events.length, ok: false, head: null, brokenAt: chain.at, error: chain.error, ended, summary, strategy };
|
|
229
|
+
if (summary && summary.events > events.length) {
|
|
230
|
+
return { ...base, events: events.length, ok: false, head: chain.head, brokenAt: events.length + 1, error: `the summary counts ${summary.events} events but the ledger has ${events.length} (lines were cut from the end)`, ended, summary, strategy };
|
|
231
|
+
}
|
|
232
|
+
if (summary && !summary.matches) {
|
|
233
|
+
return { ...base, events: events.length, ok: false, head: chain.head, brokenAt: summary.events, error: `the summary's head for event ${summary.events} does not match the ledger (the ledger was rewritten)`, ended, summary, strategy };
|
|
234
|
+
}
|
|
235
|
+
return { ...base, events: events.length, ok: true, head: chain.head, ended, summary, strategy };
|
|
236
|
+
}
|
|
237
|
+
/** Counts from a stream-json init event (-1 when a list is missing). Tools and MCP servers must both be 0. */
|
|
238
|
+
export function sessionIsolation(init) {
|
|
239
|
+
const n = (k) => (Array.isArray(init[k]) ? init[k].length : -1);
|
|
240
|
+
const tools = n('tools');
|
|
241
|
+
const mcpServers = n('mcp_servers');
|
|
242
|
+
return { tools, mcpServers, plugins: n('plugins'), agents: n('agents'), skills: n('skills'), slashCommands: n('slash_commands'), ok: tools === 0 && mcpServers === 0 };
|
|
243
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// dexterm autotrade — the always-on service's guard aggregator (0.2.0 card B, AUTOTRADE-SPEC §7.3 / §7.4).
|
|
2
|
+
//
|
|
3
|
+
// Every 10 s (and on demand) the service folds what it can see into named guard states. Each state has ONE effect:
|
|
4
|
+
// stop the service stops the fleet (a soft stop: no new entries, sessions closed, exits keep running)
|
|
5
|
+
// pause-entries no new entries while it holds; lifts by itself (hysteresis where a value can flap)
|
|
6
|
+
// notice shown in `service status` and sent once as a notification; nothing pauses
|
|
7
|
+
// Open positions are never paused by any state: their exits keep running (the monitor is never paused, §7.4).
|
|
8
|
+
//
|
|
9
|
+
// state id effect from
|
|
10
|
+
// stop-file stop the service STOP marker (`service stop`, a plain `touch`)
|
|
11
|
+
// panic stop the panic file (`dexterm wallet panic`); the fleet halts itself too
|
|
12
|
+
// disk-low pause-entries free space under runner.json disk.minFreeGb (default 5 GB); +1 GB to lift
|
|
13
|
+
// load-high pause-entries 1-min load above load.pauseAboveLoad1m for load.sustainSec; lifts below resumeBelowLoad1m
|
|
14
|
+
// busy-process pause-entries a runner.json load.busyProcs pattern matches a running process (not ours)
|
|
15
|
+
// data-stale pause-entries no strategy has seen a market tick for dataStaleSec (default 300 s)
|
|
16
|
+
// runner-stale notice the fleet's heartbeat (runner-status.json) is older than 30 s
|
|
17
|
+
// allowance-high notice the model usage windows are above runner.json allowanceGuard (the fleet pauses sessions itself)
|
|
18
|
+
// day-cap notice the fleet's global day caps are used up (sessions resume when the day rolls)
|
|
19
|
+
// recorder-down notice the market recorder is installed but has written nothing for recorderStaleSec
|
|
20
|
+
// (default 15 min); not installed → notice with `dexterm record install`
|
|
21
|
+
// strategy-halted notice a strategy halted itself (daily loss / drawdown caps); one per strategy
|
|
22
|
+
//
|
|
23
|
+
// The guard is pure over its inputs (the service collects them); transitions come back so the caller can log one
|
|
24
|
+
// ledger line and send one notification per change, never one per tick.
|
|
25
|
+
export const DEFAULT_GUARD_CONFIG = {
|
|
26
|
+
minFreeGb: 5, pauseAboveLoad1m: 25, resumeBelowLoad1m: 20, sustainSec: 120, busyProcs: [],
|
|
27
|
+
dataStaleSec: 300, recorderStaleSec: 900, fiveHourMax: 0.6, sevenDayMax: 0.8,
|
|
28
|
+
};
|
|
29
|
+
/** runner.json → the guard's knobs (the stricter of the two never matters here: these only pause). */
|
|
30
|
+
export function guardConfigFrom(rc, over = {}) {
|
|
31
|
+
return {
|
|
32
|
+
...DEFAULT_GUARD_CONFIG,
|
|
33
|
+
...(rc?.load ? { pauseAboveLoad1m: rc.load.pauseAboveLoad1m, resumeBelowLoad1m: rc.load.resumeBelowLoad1m, sustainSec: rc.load.sustainSec, busyProcs: [...rc.load.busyProcs] } : {}),
|
|
34
|
+
...(rc?.disk ? { minFreeGb: rc.disk.minFreeGb } : {}),
|
|
35
|
+
...(rc?.allowanceGuard ? { fiveHourMax: rc.allowanceGuard.fiveHourMax, sevenDayMax: rc.allowanceGuard.sevenDayMax } : {}),
|
|
36
|
+
...over,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const gb = (n) => `${n.toFixed(1)} GB`;
|
|
40
|
+
export class ServiceGuard {
|
|
41
|
+
cfg;
|
|
42
|
+
active = new Map();
|
|
43
|
+
loadAboveSince = null;
|
|
44
|
+
loadPaused = false;
|
|
45
|
+
diskLow = false;
|
|
46
|
+
constructor(cfg = DEFAULT_GUARD_CONFIG) { this.cfg = cfg; }
|
|
47
|
+
evaluate(i) {
|
|
48
|
+
const c = this.cfg;
|
|
49
|
+
const want = new Map();
|
|
50
|
+
const on = (id, effect, detail) => { want.set(id, { effect, detail }); };
|
|
51
|
+
if (i.stopFile)
|
|
52
|
+
on('stop-file', 'stop', i.stopFile);
|
|
53
|
+
if (i.panic)
|
|
54
|
+
on('panic', 'stop', `the panic switch is engaged (${i.panic}); \`dexterm wallet resume\` lifts it`);
|
|
55
|
+
if (i.freeGb !== null) {
|
|
56
|
+
if (!this.diskLow && i.freeGb < c.minFreeGb)
|
|
57
|
+
this.diskLow = true;
|
|
58
|
+
else if (this.diskLow && i.freeGb >= c.minFreeGb + 1)
|
|
59
|
+
this.diskLow = false;
|
|
60
|
+
if (this.diskLow)
|
|
61
|
+
on('disk-low', 'pause-entries', `${gb(i.freeGb)} free, under the ${gb(c.minFreeGb)} floor (runner.json disk.minFreeGb); lifts at ${gb(c.minFreeGb + 1)}`);
|
|
62
|
+
}
|
|
63
|
+
if (i.load1m !== null) {
|
|
64
|
+
if (i.load1m > c.pauseAboveLoad1m) {
|
|
65
|
+
this.loadAboveSince ??= i.now;
|
|
66
|
+
if (i.now - this.loadAboveSince >= c.sustainSec * 1000)
|
|
67
|
+
this.loadPaused = true;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
this.loadAboveSince = null;
|
|
71
|
+
if (i.load1m < c.resumeBelowLoad1m)
|
|
72
|
+
this.loadPaused = false;
|
|
73
|
+
}
|
|
74
|
+
if (this.loadPaused)
|
|
75
|
+
on('load-high', 'pause-entries', `1-min load ${i.load1m.toFixed(1)} (pauses above ${c.pauseAboveLoad1m} for ${c.sustainSec} s, resumes below ${c.resumeBelowLoad1m})`);
|
|
76
|
+
}
|
|
77
|
+
if (i.busy.length)
|
|
78
|
+
on('busy-process', 'pause-entries', `a busy process is running (${i.busy.slice(0, 3).join(', ')})`);
|
|
79
|
+
if (i.upMs >= c.dataStaleSec * 1000 && (i.dataAgeMs === null || i.dataAgeMs > c.dataStaleSec * 1000)) {
|
|
80
|
+
on('data-stale', 'pause-entries', i.dataAgeMs === null ? `no market tick since the start (${Math.round(i.upMs / 1000)} s)` : `the newest market tick is ${Math.round(i.dataAgeMs / 1000)} s old (limit ${c.dataStaleSec} s)`);
|
|
81
|
+
}
|
|
82
|
+
if (i.runnerAgeMs !== null && i.runnerAgeMs > 30_000)
|
|
83
|
+
on('runner-stale', 'notice', `the strategies' heartbeat is ${Math.round(i.runnerAgeMs / 1000)} s old`);
|
|
84
|
+
const a = i.allowance;
|
|
85
|
+
if (a && ((a.fiveHour ?? 0) > c.fiveHourMax || (a.sevenDay ?? 0) > c.sevenDayMax)) {
|
|
86
|
+
on('allowance-high', 'notice', `model usage 5 h ${pctOf(a.fiveHour)} / 7 d ${pctOf(a.sevenDay)} (limits ${pctOf(c.fiveHourMax)} / ${pctOf(c.sevenDayMax)}): no new sessions until it falls`);
|
|
87
|
+
}
|
|
88
|
+
if (i.dayCap)
|
|
89
|
+
on('day-cap', 'notice', `${i.dayCap}: no new sessions until the day rolls`);
|
|
90
|
+
if (i.recorder) {
|
|
91
|
+
if (!i.recorder.installed)
|
|
92
|
+
on('recorder-down', 'notice', 'the market recorder is not installed: backtests and tuning see no new history (`dexterm record install`)');
|
|
93
|
+
else if (i.recorder.newestAgeMs === null || i.recorder.newestAgeMs > c.recorderStaleSec * 1000)
|
|
94
|
+
on('recorder-down', 'notice', `the market recorder has written nothing for ${i.recorder.newestAgeMs === null ? 'ever' : `${Math.round(i.recorder.newestAgeMs / 60_000)} min`} (\`dexterm record status\`)`);
|
|
95
|
+
}
|
|
96
|
+
for (const [id, why] of Object.entries(i.halted))
|
|
97
|
+
on(`strategy-halted:${id}`, 'notice', `strategy ${id} halted itself (${why}); its open positions keep their exits`);
|
|
98
|
+
const transitions = [];
|
|
99
|
+
for (const [id, w] of want) {
|
|
100
|
+
const cur = this.active.get(id);
|
|
101
|
+
if (!cur) {
|
|
102
|
+
this.active.set(id, { id, effect: w.effect, detail: w.detail, since: i.now });
|
|
103
|
+
transitions.push({ id, on: true, effect: w.effect, detail: w.detail });
|
|
104
|
+
}
|
|
105
|
+
else
|
|
106
|
+
cur.detail = w.detail;
|
|
107
|
+
}
|
|
108
|
+
for (const [id, cur] of [...this.active]) {
|
|
109
|
+
if (want.has(id))
|
|
110
|
+
continue;
|
|
111
|
+
this.active.delete(id);
|
|
112
|
+
transitions.push({ id, on: false, effect: cur.effect, detail: cur.detail });
|
|
113
|
+
}
|
|
114
|
+
const states = [...this.active.values()].map((s) => ({ ...s }));
|
|
115
|
+
const stop = states.find((s) => s.effect === 'stop')?.id ?? null;
|
|
116
|
+
const pause = states.find((s) => s.effect === 'pause-entries')?.id ?? null;
|
|
117
|
+
return { states, stop, pauseEntries: pause ? `guard:${pause}` : null, transitions };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const pctOf = (v) => (v === null ? '?' : `${Math.round(v * 100)}%`);
|
|
121
|
+
/** A one-line notification text for a guard transition (plain text, no paths, no secrets). */
|
|
122
|
+
export function guardNoticeText(t) {
|
|
123
|
+
const what = t.effect === 'stop' ? 'STOP' : t.effect === 'pause-entries' ? 'ENTRIES PAUSED' : 'NOTICE';
|
|
124
|
+
return t.on ? `autotrade service · ${what} · ${t.id}: ${t.detail}` : `autotrade service · cleared · ${t.id}${t.effect === 'pause-entries' ? ': entries resume' : ''}`;
|
|
125
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// dexterm autotrade — the always-on service's OS installers (0.2.0 card B, AUTOTRADE-SPEC §7.4).
|
|
2
|
+
//
|
|
3
|
+
// Every platform runs the same thing: `dexterm autotrade service ensure` once a minute and at login. `ensure` exits at
|
|
4
|
+
// once while the service's heartbeat is fresh and otherwise starts it detached, so a crash costs at most one tick and
|
|
5
|
+
// a clean stop stays stopped. The generators are pure (golden-file tested); the CLI shows the user every file and
|
|
6
|
+
// command before anything is written, and records what it wrote (install manifest) so uninstall removes exactly that.
|
|
7
|
+
//
|
|
8
|
+
// macOS ~/Library/LaunchAgents/<label>.plist: StartInterval 60, RunAtLoad, ProcessType Background, Nice 10,
|
|
9
|
+
// LowPriorityIO, AbandonProcessGroup (the started service outlives the tick). NO KeepAlive and never
|
|
10
|
+
// `launchctl submit` (a KeepAlive job relaunches forever). Loaded with `launchctl bootout` + `bootstrap`:
|
|
11
|
+
// `kickstart -k` does NOT re-read the plist's arguments.
|
|
12
|
+
// Linux ~/.config/systemd/user/<unit>.service (oneshot ensure, Nice 10, idle IO, KillMode=process so the started
|
|
13
|
+
// service survives the tick) + <unit>.timer (30 s after login, then every 60 s). Without
|
|
14
|
+
// `loginctl enable-linger` a user unit runs only while the user is logged in.
|
|
15
|
+
// Windows a scheduled task (XML for `schtasks /Create /XML`): at logon + every minute, ignore-new on overlap,
|
|
16
|
+
// below-normal priority, least privilege, 5-minute limit for the tick itself.
|
|
17
|
+
//
|
|
18
|
+
// One label per DEXTERM_HOME: the default home gets `xyz.dexterm.autotrade`; any other home gets a suffix from a hash
|
|
19
|
+
// of its path, so a test home never replaces (or removes) the real one. A non-default home is passed as `--home`.
|
|
20
|
+
// Only PATH is copied into the job's environment (the service needs to find `claude`); nothing else, no secrets.
|
|
21
|
+
import { createHash } from 'node:crypto';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
export const SERVICE_LABEL = 'xyz.dexterm.autotrade';
|
|
24
|
+
export const INSTALL_MANIFEST_SCHEMA = 'dexterm-autotrade-service-install/1';
|
|
25
|
+
/** The label for a DEXTERM_HOME: the default one, or one suffixed by a hash of the path. */
|
|
26
|
+
export function serviceLabel(dexHome, defaultHome) {
|
|
27
|
+
if (dexHome === defaultHome)
|
|
28
|
+
return SERVICE_LABEL;
|
|
29
|
+
return `${SERVICE_LABEL}.h${createHash('sha256').update(dexHome).digest('hex').slice(0, 8)}`;
|
|
30
|
+
}
|
|
31
|
+
/** The tick's command line (`--quiet`: a tick that changes nothing prints nothing, so the tick log stays small). */
|
|
32
|
+
export const ensureArgs = (s) => [s.nodePath, s.binPath, 'autotrade', 'service', 'ensure', '--quiet', ...(s.dexHome ? ['--home', s.dexHome] : [])];
|
|
33
|
+
// ───────────────────────────── macOS ─────────────────────────────
|
|
34
|
+
const xml = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
35
|
+
export const launchAgentPath = (s) => join(s.userHome, 'Library', 'LaunchAgents', `${s.label}.plist`);
|
|
36
|
+
export function renderLaunchAgent(s) {
|
|
37
|
+
const args = ensureArgs(s).map((a) => ` <string>${xml(a)}</string>`).join('\n');
|
|
38
|
+
const env = s.path ? ` <key>EnvironmentVariables</key>\n <dict>\n <key>PATH</key>\n <string>${xml(s.path)}</string>\n </dict>\n` : '';
|
|
39
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
40
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
41
|
+
<plist version="1.0">
|
|
42
|
+
<dict>
|
|
43
|
+
<key>Label</key>
|
|
44
|
+
<string>${xml(s.label)}</string>
|
|
45
|
+
<key>ProgramArguments</key>
|
|
46
|
+
<array>
|
|
47
|
+
${args}
|
|
48
|
+
</array>
|
|
49
|
+
<key>WorkingDirectory</key>
|
|
50
|
+
<string>${xml(s.workingDir)}</string>
|
|
51
|
+
${env} <key>StartInterval</key>
|
|
52
|
+
<integer>60</integer>
|
|
53
|
+
<key>RunAtLoad</key>
|
|
54
|
+
<true/>
|
|
55
|
+
<key>ProcessType</key>
|
|
56
|
+
<string>Background</string>
|
|
57
|
+
<key>Nice</key>
|
|
58
|
+
<integer>10</integer>
|
|
59
|
+
<key>LowPriorityIO</key>
|
|
60
|
+
<true/>
|
|
61
|
+
<key>AbandonProcessGroup</key>
|
|
62
|
+
<true/>
|
|
63
|
+
<key>StandardOutPath</key>
|
|
64
|
+
<string>${xml(s.tickLog)}</string>
|
|
65
|
+
<key>StandardErrorPath</key>
|
|
66
|
+
<string>${xml(s.tickLog)}</string>
|
|
67
|
+
</dict>
|
|
68
|
+
</plist>
|
|
69
|
+
`;
|
|
70
|
+
}
|
|
71
|
+
// ───────────────────────────── Linux (systemd user) ─────────────────────────────
|
|
72
|
+
export const systemdUnit = (s) => s.label.replace(/^xyz\./, '').replace(/\./g, '-');
|
|
73
|
+
export const systemdDir = (s) => join(s.userHome, '.config', 'systemd', 'user');
|
|
74
|
+
/** systemd quoting: double quotes, backslash and quote escaped, `%` doubled (specifier expansion). */
|
|
75
|
+
const sd = (a) => `"${a.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/%/g, '%%')}"`;
|
|
76
|
+
export function renderSystemdService(s) {
|
|
77
|
+
return `[Unit]
|
|
78
|
+
Description=dexterm autotrade service: start it when it is not running (one tick)
|
|
79
|
+
|
|
80
|
+
[Service]
|
|
81
|
+
Type=oneshot
|
|
82
|
+
ExecStart=${ensureArgs(s).map(sd).join(' ')}
|
|
83
|
+
WorkingDirectory=${s.workingDir.replace(/%/g, '%%')}
|
|
84
|
+
${s.path ? `Environment=${sd(`PATH=${s.path}`)}\n` : ''}Nice=10
|
|
85
|
+
IOSchedulingClass=idle
|
|
86
|
+
KillMode=process
|
|
87
|
+
StandardOutput=append:${s.tickLog.replace(/%/g, '%%')}
|
|
88
|
+
StandardError=append:${s.tickLog.replace(/%/g, '%%')}
|
|
89
|
+
`;
|
|
90
|
+
}
|
|
91
|
+
export function renderSystemdTimer(s) {
|
|
92
|
+
return `[Unit]
|
|
93
|
+
Description=dexterm autotrade service tick (every minute)
|
|
94
|
+
|
|
95
|
+
[Timer]
|
|
96
|
+
OnStartupSec=30
|
|
97
|
+
OnUnitActiveSec=60
|
|
98
|
+
AccuracySec=10
|
|
99
|
+
Unit=${systemdUnit(s)}.service
|
|
100
|
+
|
|
101
|
+
[Install]
|
|
102
|
+
WantedBy=timers.target
|
|
103
|
+
`;
|
|
104
|
+
}
|
|
105
|
+
// ───────────────────────────── Windows (scheduled task) ─────────────────────────────
|
|
106
|
+
export const windowsTaskName = (s) => `\\dexterm\\${s.label.replace(/^xyz\.dexterm\./, '')}`;
|
|
107
|
+
export const windowsTaskFile = (dexHome, s) => join(dexHome, 'autotrade', 'service', `${s.label}.task.xml`);
|
|
108
|
+
/** Windows command-line quoting for one argument (CommandLineToArgvW rules). */
|
|
109
|
+
export function winArg(a) {
|
|
110
|
+
if (a && !/[\s"]/.test(a))
|
|
111
|
+
return a;
|
|
112
|
+
let out = '"';
|
|
113
|
+
let bs = 0;
|
|
114
|
+
for (const ch of a) {
|
|
115
|
+
if (ch === '\\') {
|
|
116
|
+
bs++;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (ch === '"') {
|
|
120
|
+
out += '\\'.repeat(bs * 2 + 1) + '"';
|
|
121
|
+
bs = 0;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
out += '\\'.repeat(bs) + ch;
|
|
125
|
+
bs = 0;
|
|
126
|
+
}
|
|
127
|
+
return `${out}${'\\'.repeat(bs * 2)}"`;
|
|
128
|
+
}
|
|
129
|
+
export function renderWindowsTask(s) {
|
|
130
|
+
const [cmd, ...rest] = ensureArgs(s);
|
|
131
|
+
return `<?xml version="1.0" encoding="UTF-16"?>
|
|
132
|
+
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
133
|
+
<RegistrationInfo>
|
|
134
|
+
<Description>dexterm autotrade service: start it when it is not running (one tick a minute)</Description>
|
|
135
|
+
</RegistrationInfo>
|
|
136
|
+
<Triggers>
|
|
137
|
+
<LogonTrigger>
|
|
138
|
+
<Enabled>true</Enabled>
|
|
139
|
+
</LogonTrigger>
|
|
140
|
+
<TimeTrigger>
|
|
141
|
+
<Repetition>
|
|
142
|
+
<Interval>PT1M</Interval>
|
|
143
|
+
<StopAtDurationEnd>false</StopAtDurationEnd>
|
|
144
|
+
</Repetition>
|
|
145
|
+
<StartBoundary>2026-01-01T00:00:00</StartBoundary>
|
|
146
|
+
<Enabled>true</Enabled>
|
|
147
|
+
</TimeTrigger>
|
|
148
|
+
</Triggers>
|
|
149
|
+
<Principals>
|
|
150
|
+
<Principal id="Author">
|
|
151
|
+
<LogonType>InteractiveToken</LogonType>
|
|
152
|
+
<RunLevel>LeastPrivilege</RunLevel>
|
|
153
|
+
</Principal>
|
|
154
|
+
</Principals>
|
|
155
|
+
<Settings>
|
|
156
|
+
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
157
|
+
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
158
|
+
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
159
|
+
<StartWhenAvailable>true</StartWhenAvailable>
|
|
160
|
+
<ExecutionTimeLimit>PT5M</ExecutionTimeLimit>
|
|
161
|
+
<Priority>7</Priority>
|
|
162
|
+
<Enabled>true</Enabled>
|
|
163
|
+
</Settings>
|
|
164
|
+
<Actions Context="Author">
|
|
165
|
+
<Exec>
|
|
166
|
+
<Command>${xml(cmd)}</Command>
|
|
167
|
+
<Arguments>${xml(rest.map(winArg).join(' '))}</Arguments>
|
|
168
|
+
<WorkingDirectory>${xml(s.workingDir)}</WorkingDirectory>
|
|
169
|
+
</Exec>
|
|
170
|
+
</Actions>
|
|
171
|
+
</Task>
|
|
172
|
+
`;
|
|
173
|
+
}
|
|
174
|
+
// ───────────────────────────── the plan ─────────────────────────────
|
|
175
|
+
export function installPlan(s, dexHome) {
|
|
176
|
+
if (s.platform === 'darwin') {
|
|
177
|
+
const path = launchAgentPath(s);
|
|
178
|
+
const domain = `gui/${s.uid}`;
|
|
179
|
+
return {
|
|
180
|
+
platform: s.platform, label: s.label, name: s.label,
|
|
181
|
+
files: [{ path, content: renderLaunchAgent(s) }],
|
|
182
|
+
activate: [['launchctl', 'bootout', `${domain}/${s.label}`], ['launchctl', 'bootstrap', domain, path]],
|
|
183
|
+
deactivate: [['launchctl', 'bootout', `${domain}/${s.label}`]],
|
|
184
|
+
notes: ['A LaunchAgent starts at login: the service runs while you are logged in on this Mac.', 'To change it later: edit nothing by hand; run `dexterm autotrade service install --load` again (bootout + bootstrap; `kickstart -k` would keep the old arguments).'],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (s.platform === 'linux') {
|
|
188
|
+
const unit = systemdUnit(s);
|
|
189
|
+
const dir = systemdDir(s);
|
|
190
|
+
return {
|
|
191
|
+
platform: s.platform, label: s.label, name: `${unit}.timer`,
|
|
192
|
+
files: [{ path: join(dir, `${unit}.service`), content: renderSystemdService(s) }, { path: join(dir, `${unit}.timer`), content: renderSystemdTimer(s) }],
|
|
193
|
+
activate: [['systemctl', '--user', 'daemon-reload'], ['systemctl', '--user', 'enable', '--now', `${unit}.timer`]],
|
|
194
|
+
deactivate: [['systemctl', '--user', 'disable', '--now', `${unit}.timer`]],
|
|
195
|
+
notes: ['A systemd user timer runs while you are logged in; `loginctl enable-linger` (your choice) keeps it running after logout and from boot.'],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const file = windowsTaskFile(dexHome, s);
|
|
199
|
+
const name = windowsTaskName(s);
|
|
200
|
+
return {
|
|
201
|
+
platform: s.platform, label: s.label, name,
|
|
202
|
+
files: [{ path: file, content: renderWindowsTask(s), encoding: 'utf16le' }],
|
|
203
|
+
activate: [['schtasks', '/Create', '/TN', name, '/XML', file, '/F']],
|
|
204
|
+
deactivate: [['schtasks', '/Delete', '/TN', name, '/F']],
|
|
205
|
+
notes: ['The task runs at logon and every minute while you are logged in.'],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
export const fileSha = (content, encoding = 'utf8') => createHash('sha256').update(encode(content, encoding)).digest('hex');
|
|
209
|
+
/** The bytes written for a file (UTF-16 LE with a BOM for the Windows task XML). */
|
|
210
|
+
export function encode(content, encoding = 'utf8') {
|
|
211
|
+
return encoding === 'utf16le' ? Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(content, 'utf16le')]) : Buffer.from(content, 'utf8');
|
|
212
|
+
}
|
|
213
|
+
export const installManifestPath = (dexHome) => join(dexHome, 'autotrade', 'service', 'install.json');
|