@jinn-network/client 0.1.1 → 0.1.2-canary.d6e72dfd
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/CHANGELOG.md +14 -0
- package/CONTRIBUTING.md +123 -0
- package/README.md +210 -37
- package/deployments/deployment-claim-registry-baseSepolia.json +13 -0
- package/deployments/deployment-jinn-testnet-faucet-baseSepolia-fast.json +15 -0
- package/dist/adapters/claim-registry/abi.d.ts +127 -0
- package/dist/adapters/claim-registry/abi.js +93 -0
- package/dist/adapters/claim-registry/abi.js.map +1 -0
- package/dist/adapters/claim-registry/client.d.ts +89 -0
- package/dist/adapters/claim-registry/client.js +205 -0
- package/dist/adapters/claim-registry/client.js.map +1 -0
- package/dist/adapters/mech/adapter.d.ts +1 -0
- package/dist/adapters/mech/adapter.js +75 -41
- package/dist/adapters/mech/adapter.js.map +1 -1
- package/dist/adapters/mech/contracts.d.ts +2 -0
- package/dist/adapters/mech/contracts.js +57 -7
- package/dist/adapters/mech/contracts.js.map +1 -1
- package/dist/adapters/mech/ipfs.d.ts +8 -0
- package/dist/adapters/mech/ipfs.js +12 -0
- package/dist/adapters/mech/ipfs.js.map +1 -1
- package/dist/adapters/mech/types.d.ts +20 -46
- package/dist/adapters/mech/types.js +16 -35
- package/dist/adapters/mech/types.js.map +1 -1
- package/dist/api/gather-status.d.ts +1 -0
- package/dist/api/gather-status.js +33 -1
- package/dist/api/gather-status.js.map +1 -1
- package/dist/api/portfolio-v0-build.d.ts +81 -0
- package/dist/api/portfolio-v0-build.js +141 -0
- package/dist/api/portfolio-v0-build.js.map +1 -0
- package/dist/api/portfolio-v0-doctor.d.ts +37 -0
- package/dist/api/portfolio-v0-doctor.js +123 -0
- package/dist/api/portfolio-v0-doctor.js.map +1 -0
- package/dist/api/rewards-build.js +1 -1
- package/dist/api/rewards-build.js.map +1 -1
- package/dist/api/status-build.d.ts +7 -0
- package/dist/api/status-build.js +1 -0
- package/dist/api/status-build.js.map +1 -1
- package/dist/bin/jinn-mcp.d.ts +0 -12
- package/dist/bin/jinn-mcp.js +5 -14
- package/dist/bin/jinn-mcp.js.map +1 -1
- package/dist/build-meta.json +1 -1
- package/dist/cli/commands/auth.js +115 -25
- package/dist/cli/commands/auth.js.map +1 -1
- package/dist/cli/commands/bootstrap.js +1 -0
- package/dist/cli/commands/bootstrap.js.map +1 -1
- package/dist/cli/commands/doctor.js +130 -14
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/fleet-scale.js +1 -0
- package/dist/cli/commands/fleet-scale.js.map +1 -1
- package/dist/cli/commands/fund-requirements.js +2 -0
- package/dist/cli/commands/fund-requirements.js.map +1 -1
- package/dist/cli/commands/intents.d.ts +17 -0
- package/dist/cli/commands/intents.js +489 -0
- package/dist/cli/commands/intents.js.map +1 -0
- package/dist/cli/commands/keys-backup.js +13 -11
- package/dist/cli/commands/keys-backup.js.map +1 -1
- package/dist/cli/commands/mcp.d.ts +3 -0
- package/dist/cli/commands/mcp.js +19 -0
- package/dist/cli/commands/mcp.js.map +1 -0
- package/dist/cli/commands/plugin-install.js +8 -4
- package/dist/cli/commands/plugin-install.js.map +1 -1
- package/dist/cli/commands/quickstart.js +60 -4
- package/dist/cli/commands/quickstart.js.map +1 -1
- package/dist/cli/commands/rewards.js +27 -1
- package/dist/cli/commands/rewards.js.map +1 -1
- package/dist/cli/commands/submit-intent.js +108 -5
- package/dist/cli/commands/submit-intent.js.map +1 -1
- package/dist/cli/commands/version.js +1 -0
- package/dist/cli/commands/version.js.map +1 -1
- package/dist/cli/deployment-digest.js +5 -0
- package/dist/cli/deployment-digest.js.map +1 -1
- package/dist/cli/execution-context.js +1 -0
- package/dist/cli/execution-context.js.map +1 -1
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/intent-registry-access.d.ts +64 -0
- package/dist/cli/intent-registry-access.js +187 -0
- package/dist/cli/intent-registry-access.js.map +1 -0
- package/dist/cli/introspection-context.js +1 -0
- package/dist/cli/introspection-context.js.map +1 -1
- package/dist/cli/password.d.ts +21 -9
- package/dist/cli/password.js +45 -24
- package/dist/cli/password.js.map +1 -1
- package/dist/config.d.ts +110 -8
- package/dist/config.js +41 -12
- package/dist/config.js.map +1 -1
- package/dist/daemon/creator.d.ts +7 -1
- package/dist/daemon/creator.js +38 -3
- package/dist/daemon/creator.js.map +1 -1
- package/dist/daemon/daemon.d.ts +43 -0
- package/dist/daemon/daemon.js +87 -2
- package/dist/daemon/daemon.js.map +1 -1
- package/dist/earning/bootstrap.d.ts +2 -1
- package/dist/earning/bootstrap.js +72 -4
- package/dist/earning/bootstrap.js.map +1 -1
- package/dist/earning/contracts.d.ts +10 -0
- package/dist/earning/contracts.js +24 -0
- package/dist/earning/contracts.js.map +1 -1
- package/dist/earning/jinn-rewards.d.ts +9 -0
- package/dist/earning/jinn-rewards.js +7 -0
- package/dist/earning/jinn-rewards.js.map +1 -1
- package/dist/intents/prediction-apy-v0-auto.d.ts +11 -0
- package/dist/intents/prediction-apy-v0-auto.js +46 -0
- package/dist/intents/prediction-apy-v0-auto.js.map +1 -0
- package/dist/intents/prediction-apy-v0-template.d.ts +8 -0
- package/dist/intents/prediction-apy-v0-template.js +22 -0
- package/dist/intents/prediction-apy-v0-template.js.map +1 -0
- package/dist/intents/prediction-v0-auto.d.ts +53 -0
- package/dist/intents/prediction-v0-auto.js +84 -0
- package/dist/intents/prediction-v0-auto.js.map +1 -0
- package/dist/intents/prediction-v0-template.d.ts +65 -0
- package/dist/intents/prediction-v0-template.js +125 -0
- package/dist/intents/prediction-v0-template.js.map +1 -0
- package/dist/main.js +149 -1
- package/dist/main.js.map +1 -1
- package/dist/mcp/operator-server.d.ts +1 -1
- package/dist/mcp/operator-server.js +1 -1
- package/dist/preflight/claude-auth.d.ts +12 -1
- package/dist/preflight/claude-auth.js +21 -3
- package/dist/preflight/claude-auth.js.map +1 -1
- package/dist/restorer/engine/canonical-json.d.ts +18 -0
- package/dist/restorer/engine/canonical-json.js +59 -0
- package/dist/restorer/engine/canonical-json.js.map +1 -0
- package/dist/restorer/engine/claim.d.ts +69 -0
- package/dist/restorer/engine/claim.js +104 -0
- package/dist/restorer/engine/claim.js.map +1 -0
- package/dist/restorer/engine/delivery.d.ts +52 -0
- package/dist/restorer/engine/delivery.js +63 -0
- package/dist/restorer/engine/delivery.js.map +1 -0
- package/dist/restorer/engine/engine.d.ts +203 -0
- package/dist/restorer/engine/engine.js +753 -0
- package/dist/restorer/engine/engine.js.map +1 -0
- package/dist/restorer/engine/manifest-assembly.d.ts +67 -0
- package/dist/restorer/engine/manifest-assembly.js +79 -0
- package/dist/restorer/engine/manifest-assembly.js.map +1 -0
- package/dist/restorer/engine/packaging.d.ts +87 -0
- package/dist/restorer/engine/packaging.js +350 -0
- package/dist/restorer/engine/packaging.js.map +1 -0
- package/dist/restorer/engine/persistence.d.ts +170 -0
- package/dist/restorer/engine/persistence.js +381 -0
- package/dist/restorer/engine/persistence.js.map +1 -0
- package/dist/restorer/engine/recovery.d.ts +22 -0
- package/dist/restorer/engine/recovery.js +24 -0
- package/dist/restorer/engine/recovery.js.map +1 -0
- package/dist/restorer/engine/registry.d.ts +62 -0
- package/dist/restorer/engine/registry.js +73 -0
- package/dist/restorer/engine/registry.js.map +1 -0
- package/dist/restorer/engine/signing.d.ts +30 -0
- package/dist/restorer/engine/signing.js +39 -0
- package/dist/restorer/engine/signing.js.map +1 -0
- package/dist/restorer/engine/state.d.ts +42 -0
- package/dist/restorer/engine/state.js +87 -0
- package/dist/restorer/engine/state.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/api-wallet.d.ts +64 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/api-wallet.js +96 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/api-wallet.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/index.d.ts +101 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/index.js +710 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/index.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/mcp-tools.d.ts +137 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/mcp-tools.js +865 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/mcp-tools.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/safety-rails.d.ts +74 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/safety-rails.js +74 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/safety-rails.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/session-orchestrator.d.ts +97 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/session-orchestrator.js +226 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/session-orchestrator.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-prediction/index.d.ts +43 -0
- package/dist/restorer/impls/claude-mcp-prediction/index.js +230 -0
- package/dist/restorer/impls/claude-mcp-prediction/index.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-prediction/mcp-tools.d.ts +38 -0
- package/dist/restorer/impls/claude-mcp-prediction/mcp-tools.js +135 -0
- package/dist/restorer/impls/claude-mcp-prediction/mcp-tools.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-prediction/prompt.d.ts +8 -0
- package/dist/restorer/impls/claude-mcp-prediction/prompt.js +54 -0
- package/dist/restorer/impls/claude-mcp-prediction/prompt.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-prediction/session-orchestrator.d.ts +36 -0
- package/dist/restorer/impls/claude-mcp-prediction/session-orchestrator.js +137 -0
- package/dist/restorer/impls/claude-mcp-prediction/session-orchestrator.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-prediction/types.d.ts +82 -0
- package/dist/restorer/impls/claude-mcp-prediction/types.js +6 -0
- package/dist/restorer/impls/claude-mcp-prediction/types.js.map +1 -0
- package/dist/restorer/impls/legacy-claude/index.d.ts +45 -0
- package/dist/restorer/impls/legacy-claude/index.js +71 -0
- package/dist/restorer/impls/legacy-claude/index.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/canonical-metrics.d.ts +68 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/canonical-metrics.js +117 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/canonical-metrics.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/availability.d.ts +49 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/availability.js +91 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/availability.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/consistency.d.ts +78 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/consistency.js +274 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/consistency.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/eligibility.d.ts +23 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/eligibility.js +49 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/eligibility.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/integrity.d.ts +25 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/integrity.js +44 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/integrity.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/spec.d.ts +17 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/spec.js +43 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/spec.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/index.d.ts +43 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/index.js +431 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/index.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/score.d.ts +21 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/score.js +32 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/score.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/types.d.ts +32 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/types.js +8 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/types.js.map +1 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/index.d.ts +39 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/index.js +98 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/index.js.map +1 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/strategy.d.ts +2 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/strategy.js +7 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/strategy.js.map +1 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/types.d.ts +4 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/types.js +2 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/types.js.map +1 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/canonical-metrics.d.ts +2 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/canonical-metrics.js +7 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/canonical-metrics.js.map +1 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/index.d.ts +39 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/index.js +186 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/index.js.map +1 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/score.d.ts +9 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/score.js +20 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/score.js.map +1 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/types.d.ts +7 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/types.js +2 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/types.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-baseline/index.d.ts +29 -0
- package/dist/restorer/impls/prediction-v0-baseline/index.js +94 -0
- package/dist/restorer/impls/prediction-v0-baseline/index.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-baseline/strategy.d.ts +8 -0
- package/dist/restorer/impls/prediction-v0-baseline/strategy.js +41 -0
- package/dist/restorer/impls/prediction-v0-baseline/strategy.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-baseline/types.d.ts +7 -0
- package/dist/restorer/impls/prediction-v0-baseline/types.js +2 -0
- package/dist/restorer/impls/prediction-v0-baseline/types.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-evaluator/canonical-metrics.d.ts +20 -0
- package/dist/restorer/impls/prediction-v0-evaluator/canonical-metrics.js +66 -0
- package/dist/restorer/impls/prediction-v0-evaluator/canonical-metrics.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/availability.d.ts +9 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/availability.js +23 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/availability.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/eligibility.d.ts +3 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/eligibility.js +13 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/eligibility.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/integrity.d.ts +7 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/integrity.js +93 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/integrity.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/spec.d.ts +5 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/spec.js +20 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/spec.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-evaluator/index.d.ts +33 -0
- package/dist/restorer/impls/prediction-v0-evaluator/index.js +208 -0
- package/dist/restorer/impls/prediction-v0-evaluator/index.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-evaluator/score.d.ts +8 -0
- package/dist/restorer/impls/prediction-v0-evaluator/score.js +15 -0
- package/dist/restorer/impls/prediction-v0-evaluator/score.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-evaluator/types.d.ts +7 -0
- package/dist/restorer/impls/prediction-v0-evaluator/types.js +2 -0
- package/dist/restorer/impls/prediction-v0-evaluator/types.js.map +1 -0
- package/dist/restorer/types.d.ts +177 -0
- package/dist/restorer/types.js +7 -0
- package/dist/restorer/types.js.map +1 -0
- package/dist/store/store.d.ts +3 -1
- package/dist/store/store.js +3 -0
- package/dist/store/store.js.map +1 -1
- package/dist/types/desired-state.d.ts +53 -0
- package/dist/types/desired-state.js +20 -0
- package/dist/types/desired-state.js.map +1 -1
- package/dist/types/index.d.ts +4 -1
- package/dist/types/index.js +4 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/portfolio.d.ts +1000 -0
- package/dist/types/portfolio.js +168 -0
- package/dist/types/portfolio.js.map +1 -0
- package/dist/types/prediction-apy.d.ts +919 -0
- package/dist/types/prediction-apy.js +121 -0
- package/dist/types/prediction-apy.js.map +1 -0
- package/dist/types/prediction.d.ts +925 -0
- package/dist/types/prediction.js +140 -0
- package/dist/types/prediction.js.map +1 -0
- package/dist/venues/aave-v3/addresses.d.ts +6 -0
- package/dist/venues/aave-v3/addresses.js +19 -0
- package/dist/venues/aave-v3/addresses.js.map +1 -0
- package/dist/venues/aave-v3/client.d.ts +81 -0
- package/dist/venues/aave-v3/client.js +97 -0
- package/dist/venues/aave-v3/client.js.map +1 -0
- package/dist/venues/chainlink/client.d.ts +99 -0
- package/dist/venues/chainlink/client.js +130 -0
- package/dist/venues/chainlink/client.js.map +1 -0
- package/dist/venues/chainlink/feeds.d.ts +8 -0
- package/dist/venues/chainlink/feeds.js +9 -0
- package/dist/venues/chainlink/feeds.js.map +1 -0
- package/dist/venues/hyperliquid/account-value.d.ts +30 -0
- package/dist/venues/hyperliquid/account-value.js +30 -0
- package/dist/venues/hyperliquid/account-value.js.map +1 -0
- package/dist/venues/hyperliquid/client.d.ts +63 -0
- package/dist/venues/hyperliquid/client.js +135 -0
- package/dist/venues/hyperliquid/client.js.map +1 -0
- package/dist/venues/hyperliquid/grid.d.ts +36 -0
- package/dist/venues/hyperliquid/grid.js +61 -0
- package/dist/venues/hyperliquid/grid.js.map +1 -0
- package/dist/venues/hyperliquid/types.d.ts +81 -0
- package/dist/venues/hyperliquid/types.js +8 -0
- package/dist/venues/hyperliquid/types.js.map +1 -0
- package/dist/withdraw/run-withdraw-plan.js +2 -0
- package/dist/withdraw/run-withdraw-plan.js.map +1 -1
- package/docker-compose.yml +44 -0
- package/package.json +12 -1
- package/skills/jinn-operator/SKILL.md +85 -0
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claude-mcp-hyperliquid — Reference RestorerImpl for portfolio.v0 — §8.
|
|
3
|
+
*
|
|
4
|
+
* Spawns Claude Code session(s) with HL-specific MCP tools (§8.2).
|
|
5
|
+
* Safety rails enforced at the tool boundary (§8.3).
|
|
6
|
+
* Sequential session cadence per §8.4.
|
|
7
|
+
* API wallet managed in implStateDir (§8.1).
|
|
8
|
+
*
|
|
9
|
+
* OPERATOR PREREQUISITE (v0):
|
|
10
|
+
* Before this impl can place trades, the operator must:
|
|
11
|
+
* 1. Let the daemon boot once (or call canAttempt()) to generate the API wallet.
|
|
12
|
+
* 2. Approve the generated API wallet address on their HL master account
|
|
13
|
+
* (Settings → API Wallets → Add in the HL UI).
|
|
14
|
+
* 3. Set approved:true in <implStateDir>/api-wallet.json
|
|
15
|
+
*
|
|
16
|
+
* The impl will surface the wallet address and instructions in canAttempt()
|
|
17
|
+
* if the wallet is not yet approved.
|
|
18
|
+
*
|
|
19
|
+
* Programmatic approval is a §8.5 future item.
|
|
20
|
+
*/
|
|
21
|
+
import { writeFileSync, mkdirSync, chmodSync, existsSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { HyperliquidClient, HL_MAINNET_BASE_URL, HL_TESTNET_BASE_URL } from '../../../venues/hyperliquid/client.js';
|
|
24
|
+
import { getUnifiedAccountValue } from '../../../venues/hyperliquid/account-value.js';
|
|
25
|
+
import { PortfolioV0IntentSchema, PortfolioV0EligibilitySchema, } from '../../../types/portfolio.js';
|
|
26
|
+
import { equityCurve, equityReturnPct, maxDrawdownPct, closedTradesCount, tradedNotionalMultiple, } from '../portfolio-v0-evaluator/canonical-metrics.js';
|
|
27
|
+
import { provisionApiWallet, loadApiWalletState, markApiWalletApproved, saveApiWalletState, walletStatePath, } from './api-wallet.js';
|
|
28
|
+
import { buildHlTools } from './mcp-tools.js';
|
|
29
|
+
import { createRateLimitState, DEFAULT_SAFETY_CONFIG as DEFAULT_SAFETY_CONFIG_IMPORTED } from './safety-rails.js';
|
|
30
|
+
import { runSessionLoop } from './session-orchestrator.js';
|
|
31
|
+
const DEFAULT_IMPL_STATE_DIR = '/tmp/jinn-engine-impl-state/claude-mcp-hyperliquid';
|
|
32
|
+
// ── Impl ───────────────────────────────────────────────────────────────────────
|
|
33
|
+
export class ClaudeMcpHyperliquidImpl {
|
|
34
|
+
name = 'claude-mcp-hyperliquid';
|
|
35
|
+
version = '1.0.0';
|
|
36
|
+
config;
|
|
37
|
+
constructor(config = {}) {
|
|
38
|
+
this.config = config;
|
|
39
|
+
}
|
|
40
|
+
supports(ctx) {
|
|
41
|
+
return ctx.kind === 'portfolio.v0' && ctx.type !== 'evaluation';
|
|
42
|
+
}
|
|
43
|
+
async canAttempt(intent) {
|
|
44
|
+
if (!intent.spec || intent.spec['kind'] !== 'portfolio.v0') {
|
|
45
|
+
return { ok: false, reason: 'spec.kind is not portfolio.v0' };
|
|
46
|
+
}
|
|
47
|
+
// Validate the intent shape
|
|
48
|
+
const parseResult = PortfolioV0IntentSchema.safeParse(intent);
|
|
49
|
+
if (!parseResult.success) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
reason: `Invalid portfolio.v0 intent: ${parseResult.error.message}`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// implStateDir is not available in canAttempt — we can only check
|
|
56
|
+
// if the intent is structurally valid. API wallet check is done at run() time.
|
|
57
|
+
return { ok: true };
|
|
58
|
+
}
|
|
59
|
+
/** Resolve the impl state directory used by enable / readiness flows. */
|
|
60
|
+
resolveImplStateDir() {
|
|
61
|
+
return this.config.implStateDir ?? DEFAULT_IMPL_STATE_DIR;
|
|
62
|
+
}
|
|
63
|
+
async isReady() {
|
|
64
|
+
const implStateDir = this.resolveImplStateDir();
|
|
65
|
+
const wallet = loadApiWalletState(implStateDir);
|
|
66
|
+
if (!wallet) {
|
|
67
|
+
return {
|
|
68
|
+
ready: false,
|
|
69
|
+
reason: 'HL api-wallet not provisioned',
|
|
70
|
+
nextStep: {
|
|
71
|
+
description: 'Run `jinn intents enable portfolio.v0 --hl-master <your-HL-master-address>` to generate the api-wallet and complete HL approval.',
|
|
72
|
+
cli: 'jinn intents enable portfolio.v0 --hl-master 0x...',
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (!wallet.approved) {
|
|
77
|
+
return {
|
|
78
|
+
ready: false,
|
|
79
|
+
reason: 'HL api-wallet generated but not approved on HL',
|
|
80
|
+
nextStep: {
|
|
81
|
+
description: `Approve address ${wallet.address} on Hyperliquid (Settings → API Wallets → Add), then re-run \`jinn intents enable portfolio.v0 --confirm-approved\`.`,
|
|
82
|
+
cli: 'jinn intents enable portfolio.v0 --confirm-approved',
|
|
83
|
+
url: 'https://app.hyperliquid-testnet.xyz/API',
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (!wallet.masterAddress) {
|
|
88
|
+
return {
|
|
89
|
+
ready: false,
|
|
90
|
+
reason: 'HL master address not recorded in api-wallet state',
|
|
91
|
+
nextStep: {
|
|
92
|
+
description: 'Re-run enable with the master address to record it: `jinn intents enable portfolio.v0 --hl-master 0x...`.',
|
|
93
|
+
cli: 'jinn intents enable portfolio.v0 --hl-master 0x...',
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return { ready: true };
|
|
98
|
+
}
|
|
99
|
+
enableMetadata() {
|
|
100
|
+
return {
|
|
101
|
+
description: 'portfolio.v0 — 24h managed-trading intent executed against a Hyperliquid master account. Requires you to (1) bring an HL master with USDC, and (2) approve a generated api-wallet as an HL agent.',
|
|
102
|
+
requiredArgs: [
|
|
103
|
+
{ name: 'hl-master', description: 'Your Hyperliquid master address (holds USDC; approves the api-wallet).', required: true },
|
|
104
|
+
],
|
|
105
|
+
externalResources: [
|
|
106
|
+
{ name: 'HL Testnet Settings → API Wallets', url: 'https://app.hyperliquid-testnet.xyz/API' },
|
|
107
|
+
{ name: 'HL Mainnet Settings → API Wallets', url: 'https://app.hyperliquid.xyz/API' },
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
async onEnable(args) {
|
|
112
|
+
const implStateDir = this.resolveImplStateDir();
|
|
113
|
+
const hlMaster = args['hl-master'];
|
|
114
|
+
const confirmApproved = args['confirm-approved'] !== undefined;
|
|
115
|
+
// Ensure the impl state directory exists so we can write the wallet file.
|
|
116
|
+
if (!existsSync(implStateDir)) {
|
|
117
|
+
mkdirSync(implStateDir, { recursive: true, mode: 0o700 });
|
|
118
|
+
}
|
|
119
|
+
const existing = loadApiWalletState(implStateDir);
|
|
120
|
+
// Already enabled: idempotent no-op.
|
|
121
|
+
if (existing && existing.approved && existing.masterAddress) {
|
|
122
|
+
return {
|
|
123
|
+
status: 'ready',
|
|
124
|
+
details: {
|
|
125
|
+
apiWalletAddress: existing.address,
|
|
126
|
+
masterAddress: existing.masterAddress,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// First invocation: need --hl-master to know which account this wallet belongs to.
|
|
131
|
+
if (!existing && !hlMaster) {
|
|
132
|
+
return {
|
|
133
|
+
status: 'missing_args',
|
|
134
|
+
required: [
|
|
135
|
+
{ name: 'hl-master', description: 'Your Hyperliquid master address (holds USDC).', required: true },
|
|
136
|
+
],
|
|
137
|
+
example: { cli: 'jinn intents enable portfolio.v0 --hl-master 0x...' },
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// State transition 1: no wallet yet — generate it and tell the agent to have the operator approve on HL.
|
|
141
|
+
if (!existing) {
|
|
142
|
+
const wallet = provisionApiWallet(implStateDir);
|
|
143
|
+
// Record the master so we don't re-ask on the next invocation.
|
|
144
|
+
saveApiWalletState(implStateDir, { ...wallet, masterAddress: hlMaster });
|
|
145
|
+
return {
|
|
146
|
+
status: 'waiting_for_external_action',
|
|
147
|
+
action: {
|
|
148
|
+
description: `API wallet ${wallet.address} generated. Open the Hyperliquid UI, go to Settings → API Wallets → Add, and approve this address under your master ${hlMaster}. Once done, re-run this command with --confirm-approved.`,
|
|
149
|
+
url: 'https://app.hyperliquid-testnet.xyz/API',
|
|
150
|
+
},
|
|
151
|
+
details: {
|
|
152
|
+
apiWalletAddress: wallet.address,
|
|
153
|
+
masterAddress: hlMaster,
|
|
154
|
+
walletStatePath: walletStatePath(implStateDir),
|
|
155
|
+
},
|
|
156
|
+
nextInvocation: {
|
|
157
|
+
cli: 'jinn intents enable portfolio.v0 --confirm-approved',
|
|
158
|
+
purpose: 'Record the operator-confirmed HL approval and enable portfolio.v0 claims.',
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// State transition 2: wallet exists, not yet confirmed — if --confirm-approved is passed, flip the flag.
|
|
163
|
+
if (!existing.approved && !confirmApproved) {
|
|
164
|
+
return {
|
|
165
|
+
status: 'waiting_for_external_action',
|
|
166
|
+
action: {
|
|
167
|
+
description: `Approve api-wallet address ${existing.address} on Hyperliquid under master ${existing.masterAddress ?? hlMaster ?? '<set via --hl-master>'}, then re-run with --confirm-approved.`,
|
|
168
|
+
url: 'https://app.hyperliquid-testnet.xyz/API',
|
|
169
|
+
},
|
|
170
|
+
details: {
|
|
171
|
+
apiWalletAddress: existing.address,
|
|
172
|
+
masterAddress: existing.masterAddress ?? hlMaster,
|
|
173
|
+
},
|
|
174
|
+
nextInvocation: {
|
|
175
|
+
cli: 'jinn intents enable portfolio.v0 --confirm-approved',
|
|
176
|
+
purpose: 'Record the operator-confirmed HL approval.',
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// State transition 3: --confirm-approved passed — persist approval and optionally update the master.
|
|
181
|
+
const updated = markApiWalletApproved(implStateDir);
|
|
182
|
+
const resolvedMaster = hlMaster ?? updated.masterAddress;
|
|
183
|
+
if (!resolvedMaster) {
|
|
184
|
+
return {
|
|
185
|
+
status: 'missing_args',
|
|
186
|
+
required: [
|
|
187
|
+
{ name: 'hl-master', description: 'Master address was never recorded; pass --hl-master now.', required: true },
|
|
188
|
+
],
|
|
189
|
+
example: { cli: 'jinn intents enable portfolio.v0 --hl-master 0x... --confirm-approved' },
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (updated.masterAddress !== resolvedMaster) {
|
|
193
|
+
saveApiWalletState(implStateDir, { ...updated, masterAddress: resolvedMaster });
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
status: 'ready',
|
|
197
|
+
details: {
|
|
198
|
+
apiWalletAddress: updated.address,
|
|
199
|
+
masterAddress: resolvedMaster,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
async onDisable() {
|
|
204
|
+
// Intentionally no-op on key material: operators may re-enable later
|
|
205
|
+
// and re-generating the wallet would force a fresh HL approval round-trip.
|
|
206
|
+
// The config-level disable (removing the impl from `restorers.disabled`)
|
|
207
|
+
// is handled by the `jinn intents disable` verb, not the impl itself.
|
|
208
|
+
}
|
|
209
|
+
async run(ctx) {
|
|
210
|
+
const { intent, implStateDir, workingDir, log, abort, msUntilEndTs } = ctx;
|
|
211
|
+
const testDeps = this.config._testDeps;
|
|
212
|
+
log({ level: 'info', msg: 'claude-mcp-hyperliquid: starting', data: { implStateDir, workingDir } });
|
|
213
|
+
// ── Parse intent ──────────────────────────────────────────────────────────
|
|
214
|
+
const portfolioIntent = PortfolioV0IntentSchema.parse(intent);
|
|
215
|
+
const { masterAddress, venue } = portfolioIntent.spec.account;
|
|
216
|
+
const eligibility = PortfolioV0EligibilitySchema.parse(portfolioIntent.eligibility ?? {});
|
|
217
|
+
const window = portfolioIntent.window;
|
|
218
|
+
// ── Select HL client ──────────────────────────────────────────────────────
|
|
219
|
+
const hlBaseUrl = venue === 'hyperliquid-mainnet' ? HL_MAINNET_BASE_URL : HL_TESTNET_BASE_URL;
|
|
220
|
+
const hlClient = testDeps?.hlClient ?? new HyperliquidClient(hlBaseUrl);
|
|
221
|
+
// ── Provision API wallet ──────────────────────────────────────────────────
|
|
222
|
+
const walletState = provisionApiWallet(implStateDir);
|
|
223
|
+
if (!walletState.approved && !testDeps) {
|
|
224
|
+
log({
|
|
225
|
+
level: 'warn',
|
|
226
|
+
msg: 'claude-mcp-hyperliquid: API wallet not approved — write tools will fail. Operator action required.',
|
|
227
|
+
data: {
|
|
228
|
+
apiWalletAddress: walletState.address,
|
|
229
|
+
instructions: [
|
|
230
|
+
'1. Go to Hyperliquid Settings → API Wallets → Add',
|
|
231
|
+
`2. Approve address: ${walletState.address}`,
|
|
232
|
+
'3. Set approved:true in <implStateDir>/api-wallet.json',
|
|
233
|
+
],
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
// Continue anyway — read-only sessions still work; write tools will fail at the HL exchange layer
|
|
237
|
+
}
|
|
238
|
+
// Sanity check: the agent key stored in api-wallet.json was approved by a
|
|
239
|
+
// specific master. HL routes trades to THAT master, regardless of what the
|
|
240
|
+
// intent's spec says. If the operator switched masters (new wallet, new
|
|
241
|
+
// approval) without updating the intent, every trade would silently land
|
|
242
|
+
// on the wrong account and pre/post snapshots would read the wrong equity.
|
|
243
|
+
// Fail fast with a clear remedy.
|
|
244
|
+
if (!testDeps
|
|
245
|
+
&& walletState.masterAddress !== undefined
|
|
246
|
+
&& walletState.masterAddress.toLowerCase() !== masterAddress.toLowerCase()) {
|
|
247
|
+
throw new Error(`E_MASTER_MISMATCH: api-wallet.json says the agent ${walletState.address} is approved by ` +
|
|
248
|
+
`${walletState.masterAddress}, but the intent's spec.account.masterAddress is ${masterAddress}. ` +
|
|
249
|
+
`HL would route trades to the agent's approver (${walletState.masterAddress}) while pre/post ` +
|
|
250
|
+
`snapshots would read from the intent's master (${masterAddress}) — silent fund-on-the-wrong-account bug. ` +
|
|
251
|
+
`Remedy: either (a) update ~/.jinn-client/portfolio-v0-intent.json to use masterAddress=${walletState.masterAddress}, ` +
|
|
252
|
+
`or (b) approve the agent on the intent's master (${masterAddress}) via the HL UI and update ${implStateDir}/api-wallet.json.`);
|
|
253
|
+
}
|
|
254
|
+
// ── Take pre-snapshot ─────────────────────────────────────────────────────
|
|
255
|
+
//
|
|
256
|
+
// Pre/post snapshots source equity from HL's unified view: perps
|
|
257
|
+
// `marginSummary.accountValue` + spot USDC balance. This matches the
|
|
258
|
+
// `portfolio` endpoint's `accountValueHistory` (which reports the unified
|
|
259
|
+
// figure too), so restorer-claimed equity and evaluator-rederived grid
|
|
260
|
+
// points compare cleanly — and accounts parked on spot are not ignored.
|
|
261
|
+
log({ level: 'info', msg: 'claude-mcp-hyperliquid: taking pre-snapshot' });
|
|
262
|
+
const preUnified = await getUnifiedAccountValue(hlClient, masterAddress);
|
|
263
|
+
const preSnapshot = {
|
|
264
|
+
capturedAt: Date.now(),
|
|
265
|
+
hlTime: preUnified.clearinghouseState.time,
|
|
266
|
+
payload: buildSnapshotPayload(preUnified),
|
|
267
|
+
};
|
|
268
|
+
log({
|
|
269
|
+
level: 'info',
|
|
270
|
+
msg: 'claude-mcp-hyperliquid: pre-snapshot captured',
|
|
271
|
+
data: {
|
|
272
|
+
accountValue: preUnified.accountValue,
|
|
273
|
+
perpsAccountValue: preUnified.perpsAccountValue,
|
|
274
|
+
spotUsdc: preUnified.spotUsdc,
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
// ── Set up session infrastructure ─────────────────────────────────────────
|
|
278
|
+
const writeOps = [];
|
|
279
|
+
const rateLimitState = createRateLimitState();
|
|
280
|
+
// Build HL tools (shared across sessions via closure)
|
|
281
|
+
const hlTools = buildHlTools({
|
|
282
|
+
hlClient,
|
|
283
|
+
hlBaseUrl,
|
|
284
|
+
apiWalletPrivateKey: walletState.privateKey,
|
|
285
|
+
apiWalletAddress: walletState.address,
|
|
286
|
+
masterAddress,
|
|
287
|
+
safetyConfig: this.config.safetyConfig
|
|
288
|
+
? { ...DEFAULT_SAFETY_CONFIG_IMPORTED, ...this.config.safetyConfig }
|
|
289
|
+
: undefined,
|
|
290
|
+
rateLimitState,
|
|
291
|
+
onWriteOp: (op) => writeOps.push(op),
|
|
292
|
+
});
|
|
293
|
+
// ── MCP config for Claude sessions ────────────────────────────────────────
|
|
294
|
+
// The HL tools are served via an in-process MCP server written to a temp script.
|
|
295
|
+
// We create a per-run MCP config that includes both:
|
|
296
|
+
// 1. The existing jinn-client MCP server (tool: submit_restoration_result etc)
|
|
297
|
+
// 2. A new HL-specific MCP server (tool: hl_*)
|
|
298
|
+
//
|
|
299
|
+
// The HL server is an inline node script that creates a McpServer with the tools.
|
|
300
|
+
// We write it to workingDir/mcp/hl-server.ts and reference it in the config.
|
|
301
|
+
const hlMcpServerPath = join(workingDir, 'mcp', 'hl-server.mjs');
|
|
302
|
+
// Config file path — written by _writeHlMcpServerScript alongside the script
|
|
303
|
+
const hlMcpConfigPath = join(workingDir, 'mcp', 'hl-server-config.json');
|
|
304
|
+
mkdirSync(join(workingDir, 'mcp'), { recursive: true });
|
|
305
|
+
_writeHlMcpServerScript(hlMcpServerPath, {
|
|
306
|
+
hlBaseUrl,
|
|
307
|
+
apiWalletPrivateKey: walletState.privateKey,
|
|
308
|
+
apiWalletAddress: walletState.address,
|
|
309
|
+
masterAddress,
|
|
310
|
+
safetyConfig: this.config.safetyConfig,
|
|
311
|
+
});
|
|
312
|
+
const { command: jinnMcpCommand, args: jinnMcpArgs } = _resolveJinnMcpLauncher();
|
|
313
|
+
const mcpConfigPath = join(workingDir, 'mcp', 'mcp-config.json');
|
|
314
|
+
writeFileSync(mcpConfigPath, JSON.stringify({
|
|
315
|
+
mcpServers: {
|
|
316
|
+
'jinn-client': {
|
|
317
|
+
command: jinnMcpCommand,
|
|
318
|
+
args: jinnMcpArgs,
|
|
319
|
+
env: {
|
|
320
|
+
DESIRED_STATE_ID: intent.id,
|
|
321
|
+
DESIRED_STATE_DESCRIPTION: intent.description,
|
|
322
|
+
DESIRED_STATE_CONTEXT: intent.context ? JSON.stringify(intent.context) : '',
|
|
323
|
+
DESIRED_STATE_TYPE: intent.type ?? '',
|
|
324
|
+
RESTORATION_REQUEST_ID: intent.restorationRequestId ?? '',
|
|
325
|
+
REQUEST_ID: intent.restorationRequestId ?? '',
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
'jinn-hl': {
|
|
329
|
+
command: process.execPath,
|
|
330
|
+
// Pass config file path as argv[2] — private key is in config file, not script body
|
|
331
|
+
args: [hlMcpServerPath, hlMcpConfigPath],
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
}));
|
|
335
|
+
// ── Session loop ──────────────────────────────────────────────────────────
|
|
336
|
+
const rationale = [];
|
|
337
|
+
let allSessions = [];
|
|
338
|
+
if (testDeps?.runSession) {
|
|
339
|
+
// Test mode: use injected runner instead of spawning Claude
|
|
340
|
+
allSessions = await _runTestSessions(testDeps.runSession, intent, portfolioIntent, workingDir, abort, msUntilEndTs, log, this.config);
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
// Production mode: spawn Claude with MCP config
|
|
344
|
+
const preAccountValue = parseFloat(preUnified.accountValue);
|
|
345
|
+
const loopResult = await runSessionLoop((sessionNum, sessionId) => buildSessionPrompt({
|
|
346
|
+
sessionNum,
|
|
347
|
+
sessionId,
|
|
348
|
+
intent: portfolioIntent,
|
|
349
|
+
preAccountValue,
|
|
350
|
+
msUntilEndTs: msUntilEndTs(),
|
|
351
|
+
}), {
|
|
352
|
+
claudePath: this.config.claudePath ?? 'claude',
|
|
353
|
+
claudeModel: this.config.claudeModel,
|
|
354
|
+
mcpConfigPath,
|
|
355
|
+
workingDir,
|
|
356
|
+
abort,
|
|
357
|
+
msUntilEndTs,
|
|
358
|
+
log,
|
|
359
|
+
getMids: () => hlClient.allMids(),
|
|
360
|
+
// Write a per-session outcome.json as soon as each Claude subprocess
|
|
361
|
+
// exits. Consumed by gather-status.ts / portfolio-v0-build.ts to
|
|
362
|
+
// surface `recentClaudeOutcomes` in /v1/status — operators can see
|
|
363
|
+
// trading activity before the 24h window closes.
|
|
364
|
+
//
|
|
365
|
+
// NOTE: the daemon-side `writeOps` closure was initially used for
|
|
366
|
+
// tool-call counts, but the actual MCP tool invocations happen in
|
|
367
|
+
// Claude's hl-server.mjs SUBPROCESS — the daemon's callback is never
|
|
368
|
+
// wired to that server. So the authoritative counter for real trading
|
|
369
|
+
// is HL's own userFillsByTime: ask HL what filled between the
|
|
370
|
+
// session's start/end (±5s padding for clock skew) and report that.
|
|
371
|
+
onSessionEnd: async (sr) => {
|
|
372
|
+
try {
|
|
373
|
+
const outcomePath = join(workingDir, 'sessions', sr.sessionId, 'outcome.json');
|
|
374
|
+
let fillsInWindow = [];
|
|
375
|
+
let fillsQueryError = null;
|
|
376
|
+
try {
|
|
377
|
+
// Give HL a moment to propagate fills before we query.
|
|
378
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
379
|
+
const { fills } = await hlClient.userFillsByTime(masterAddress, sr.startedAt - 5_000, sr.endedAt + 5_000);
|
|
380
|
+
fillsInWindow = fills.map(f => ({ coin: f.coin, dir: f.dir, oid: f.oid }));
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
fillsQueryError = err instanceof Error ? err.message : String(err);
|
|
384
|
+
}
|
|
385
|
+
const coinCounts = {};
|
|
386
|
+
for (const f of fillsInWindow) {
|
|
387
|
+
coinCounts[f.coin] = (coinCounts[f.coin] ?? 0) + 1;
|
|
388
|
+
}
|
|
389
|
+
const outcome = {
|
|
390
|
+
schemaVersion: 2,
|
|
391
|
+
requestId: ctx.intent.restorationRequestId ?? ctx.intent.id,
|
|
392
|
+
sessionId: sr.sessionId,
|
|
393
|
+
startedAt: sr.startedAt,
|
|
394
|
+
endedAt: sr.endedAt,
|
|
395
|
+
durationMs: sr.endedAt - sr.startedAt,
|
|
396
|
+
aborted: sr.aborted,
|
|
397
|
+
fillsInSessionWindow: fillsInWindow.length,
|
|
398
|
+
coinCounts,
|
|
399
|
+
...(fillsQueryError !== null ? { fillsQueryError } : {}),
|
|
400
|
+
};
|
|
401
|
+
writeFileSync(outcomePath, JSON.stringify(outcome, null, 2), { encoding: 'utf-8' });
|
|
402
|
+
}
|
|
403
|
+
catch (err) {
|
|
404
|
+
log({
|
|
405
|
+
level: 'warn',
|
|
406
|
+
msg: 'claude-mcp-hyperliquid: failed to write session outcome.json',
|
|
407
|
+
data: { sessionId: sr.sessionId, err: err instanceof Error ? err.message : String(err) },
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
config: {
|
|
412
|
+
cadenceMs: this.config.cadenceMs,
|
|
413
|
+
sessionMaxMs: this.config.sessionMaxMs,
|
|
414
|
+
trackedCoins: [], // could be populated from intent context
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
allSessions = loopResult.sessions;
|
|
418
|
+
}
|
|
419
|
+
// ── Post-session: match fills to sessions ─────────────────────────────────
|
|
420
|
+
log({ level: 'info', msg: 'claude-mcp-hyperliquid: matching fills to sessions' });
|
|
421
|
+
let windowFills = [];
|
|
422
|
+
try {
|
|
423
|
+
const fillsResult = await hlClient.userFillsByTime(masterAddress, window.startTs, window.endTs);
|
|
424
|
+
windowFills = fillsResult.fills;
|
|
425
|
+
}
|
|
426
|
+
catch (e) {
|
|
427
|
+
log({ level: 'warn', msg: 'claude-mcp-hyperliquid: failed to fetch window fills', data: { err: e instanceof Error ? e.message : String(e) } });
|
|
428
|
+
}
|
|
429
|
+
// Match fills to sessions by timestamp
|
|
430
|
+
for (const session of allSessions) {
|
|
431
|
+
const sessionFillTids = windowFills
|
|
432
|
+
.filter((f) => f.time >= session.startedAt - 5000 && f.time <= session.endedAt + 5000)
|
|
433
|
+
.map((f) => f.tid);
|
|
434
|
+
session.initiatedFillTids = sessionFillTids;
|
|
435
|
+
if (sessionFillTids.length > 0) {
|
|
436
|
+
rationale.push({
|
|
437
|
+
ts: session.startedAt,
|
|
438
|
+
sessionId: session.sessionId,
|
|
439
|
+
note: `Session completed with ${sessionFillTids.length} fills`,
|
|
440
|
+
relatedFillTids: sessionFillTids,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
// ── Take post-snapshot ────────────────────────────────────────────────────
|
|
445
|
+
log({ level: 'info', msg: 'claude-mcp-hyperliquid: taking post-snapshot' });
|
|
446
|
+
let postUnified;
|
|
447
|
+
try {
|
|
448
|
+
postUnified = await getUnifiedAccountValue(hlClient, masterAddress);
|
|
449
|
+
}
|
|
450
|
+
catch (e) {
|
|
451
|
+
log({ level: 'error', msg: 'claude-mcp-hyperliquid: failed to take post-snapshot', data: { err: e instanceof Error ? e.message : String(e) } });
|
|
452
|
+
// Use pre-snapshot as fallback
|
|
453
|
+
postUnified = preUnified;
|
|
454
|
+
}
|
|
455
|
+
const postSnapshot = {
|
|
456
|
+
capturedAt: Date.now(),
|
|
457
|
+
hlTime: postUnified.clearinghouseState.time,
|
|
458
|
+
payload: buildSnapshotPayload(postUnified),
|
|
459
|
+
};
|
|
460
|
+
// ── Compute canonical metrics (import from evaluator — §8 task discipline) ─
|
|
461
|
+
const preValue = parseFloat(preUnified.accountValue);
|
|
462
|
+
const postValue = parseFloat(postUnified.accountValue);
|
|
463
|
+
const fillTimes = windowFills.map((f) => f.time);
|
|
464
|
+
const curve = equityCurve(preSnapshot.capturedAt, preValue, postSnapshot.capturedAt, postValue, fillTimes);
|
|
465
|
+
const equityReturn = equityReturnPct(preValue, postValue);
|
|
466
|
+
const drawdown = maxDrawdownPct(curve);
|
|
467
|
+
const closedTrades = closedTradesCount(windowFills);
|
|
468
|
+
const notional = tradedNotionalMultiple(windowFills, preValue);
|
|
469
|
+
log({
|
|
470
|
+
level: 'info',
|
|
471
|
+
msg: 'claude-mcp-hyperliquid: metrics computed',
|
|
472
|
+
data: { equityReturn, drawdown, closedTrades, notional },
|
|
473
|
+
});
|
|
474
|
+
// ── Build artifacts list (transcripts) ────────────────────────────────────
|
|
475
|
+
const artifacts = allSessions.map((session) => ({
|
|
476
|
+
path: `sessions/${session.sessionId}/transcript.txt`,
|
|
477
|
+
role: 'session_transcript',
|
|
478
|
+
metadata: {
|
|
479
|
+
sessionId: session.sessionId,
|
|
480
|
+
startedAt: session.startedAt,
|
|
481
|
+
endedAt: session.endedAt,
|
|
482
|
+
modelId: this.config.claudeModel ?? 'unknown',
|
|
483
|
+
initiatedFillTids: session.initiatedFillTids,
|
|
484
|
+
},
|
|
485
|
+
tags: ['transcript', 'session'],
|
|
486
|
+
access: { kind: 'open' },
|
|
487
|
+
}));
|
|
488
|
+
// ── Return RestorationOutput ──────────────────────────────────────────────
|
|
489
|
+
return {
|
|
490
|
+
venueRef: { name: venue === 'hyperliquid-mainnet' ? 'hyperliquid-mainnet' : 'hyperliquid-testnet' },
|
|
491
|
+
preSnapshot,
|
|
492
|
+
postSnapshot,
|
|
493
|
+
fills: windowFills,
|
|
494
|
+
gating: {
|
|
495
|
+
equityReturnPct: String(equityReturn),
|
|
496
|
+
maxDrawdownPct: String(drawdown),
|
|
497
|
+
closedTradesCount: closedTrades,
|
|
498
|
+
tradedNotionalMultiple: String(notional),
|
|
499
|
+
},
|
|
500
|
+
informational: {
|
|
501
|
+
sessionCount: allSessions.length,
|
|
502
|
+
totalWriteOps: writeOps.length,
|
|
503
|
+
preAccountValue: String(preValue),
|
|
504
|
+
postAccountValue: String(postValue),
|
|
505
|
+
windowFillCount: windowFills.length,
|
|
506
|
+
minReturnPct: portfolioIntent.spec.target.minReturnPct,
|
|
507
|
+
maxDrawdownConstraint: portfolioIntent.spec.constraint.maxDrawdownPct,
|
|
508
|
+
minClosedTrades: eligibility.minClosedTrades,
|
|
509
|
+
minTradedNotionalMultiple: eligibility.minTradedNotionalMultiple,
|
|
510
|
+
},
|
|
511
|
+
artifacts,
|
|
512
|
+
rationale,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// ── Snapshot payload ──────────────────────────────────────────────────────────
|
|
517
|
+
/**
|
|
518
|
+
* Build the on-manifest snapshot payload from a unified account-value result.
|
|
519
|
+
*
|
|
520
|
+
* The top-level `accountValue` is the canonical figure used for metrics and
|
|
521
|
+
* consistency checks (matches HL's `portfolio` endpoint unified equity).
|
|
522
|
+
* The raw perps and spot payloads are included verbatim for auditability.
|
|
523
|
+
*/
|
|
524
|
+
function buildSnapshotPayload(unified) {
|
|
525
|
+
return {
|
|
526
|
+
accountValue: unified.accountValue,
|
|
527
|
+
perpsAccountValue: unified.perpsAccountValue,
|
|
528
|
+
spotUsdc: unified.spotUsdc,
|
|
529
|
+
clearinghouseState: unified.clearinghouseState,
|
|
530
|
+
spotClearinghouseState: unified.spotClearinghouseState,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function buildSessionPrompt(params) {
|
|
534
|
+
const { sessionNum, sessionId, intent, preAccountValue, msUntilEndTs } = params;
|
|
535
|
+
const minsRemaining = Math.round(msUntilEndTs / 60_000);
|
|
536
|
+
const { minReturnPct } = intent.spec.target;
|
|
537
|
+
const { maxDrawdownPct } = intent.spec.constraint;
|
|
538
|
+
const { masterAddress } = intent.spec.account;
|
|
539
|
+
return `EXECUTE IMMEDIATELY. DO NOT WRITE TEXT BEFORE YOUR FIRST TOOL CALL. DO NOT ASK QUESTIONS.
|
|
540
|
+
|
|
541
|
+
Your first action this turn MUST be a tool call to \`hl_account_unified\`. Begin now.
|
|
542
|
+
|
|
543
|
+
You are an autonomous trader managing a Hyperliquid perpetuals portfolio. Every turn, you call tools, observe results, and decide the next action. You never defer to a human — there is no human in the loop. The only way to satisfy the intent is to open positions, close them, and repeat enough times to hit the targets below.
|
|
544
|
+
|
|
545
|
+
## Verdict criteria (ALL four must pass for a PASS verdict)
|
|
546
|
+
- Equity return ≥ ${minReturnPct}% over the 24h window
|
|
547
|
+
- Max drawdown ≤ ${maxDrawdownPct}% peak-to-trough
|
|
548
|
+
- At least 20 closed trades
|
|
549
|
+
- Traded notional ≥ 5× starting equity (~$${(preAccountValue * 5).toFixed(2)})
|
|
550
|
+
|
|
551
|
+
Missing any one → REJECTED → zero rewards. Inaction = REJECTED.
|
|
552
|
+
|
|
553
|
+
## Context
|
|
554
|
+
- Intent: ${intent.description}
|
|
555
|
+
- Master account: ${masterAddress}
|
|
556
|
+
- Starting equity: $${preAccountValue.toFixed(2)}
|
|
557
|
+
- Session ${sessionNum} (${sessionId})
|
|
558
|
+
- Time remaining in window: ${minsRemaining} min
|
|
559
|
+
- You run again in ~30 minutes, or sooner on ≥2% mid-move. Use this session fully.
|
|
560
|
+
|
|
561
|
+
## Tools you will call (all are MCP tools — call them by name)
|
|
562
|
+
Read (prefer these):
|
|
563
|
+
- \`hl_account_unified\` — PREFERRED single-call read: unified equity (perps+spot), positions, open orders
|
|
564
|
+
- \`hl_all_mids\` — current mid prices for all assets
|
|
565
|
+
- \`hl_user_fills\` — recent fills (check turnover + trade count)
|
|
566
|
+
- \`hl_meta\` — asset metadata (max leverage, size decimals)
|
|
567
|
+
Read (niche):
|
|
568
|
+
- \`hl_clearinghouse_state\` — perps-only snapshot; DO NOT use for decisions (hides spot USDC)
|
|
569
|
+
- \`hl_portfolio\` — historical equity curve
|
|
570
|
+
Write: \`hl_open_position\`, \`hl_close_position\`, \`hl_modify_position\`, \`hl_cancel_orders\`
|
|
571
|
+
|
|
572
|
+
## Risk rails (tool-enforced — not suggestions)
|
|
573
|
+
- Max 25% of account value per position
|
|
574
|
+
- Max 10× leverage
|
|
575
|
+
- Max 50 bps slippage
|
|
576
|
+
- **Every \`hl_open_position\` MUST include both \`tp\` (take-profit) and \`sl\` (stop-loss)**. The tool rejects bare opens with \`TPSL_REQUIRED\` unless you pass \`bypassRiskRails=true\`. Only bypass for a scalp you will close THIS turn. Between turns you're unprotected — ~30 min of tail risk is enough to break the ${maxDrawdownPct}% drawdown on a 5–10× move.
|
|
577
|
+
- Sensible starting ranges: sl at 1–2% adverse from mid; tp at 1–3% favorable. Example long at mid $2300: \`sl=2265\` (−1.5%), \`tp=2345\` (+2%).
|
|
578
|
+
- The tool validates tp/sl are on the correct sides of mid. A long with sl≥mid or tp≤mid returns \`SL_INVALID\`/\`TP_INVALID\`.
|
|
579
|
+
|
|
580
|
+
## Your turn (do this sequence in tool calls, not prose)
|
|
581
|
+
1. \`hl_account_unified\` — one call, gives you unified equity + positions + open orders
|
|
582
|
+
2. \`hl_all_mids\` — current prices for majors (BTC, ETH, SOL, at minimum)
|
|
583
|
+
3. \`hl_user_fills\` — recent fills so you know your turnover + trade count so far
|
|
584
|
+
4. Based on the read tool outputs, open/adjust/close positions with the write tools. Aim for at least 2–3 position actions in this session. You have 20+ trades to hit across the window — aggressive, disciplined turnover is required, not careful inaction.
|
|
585
|
+
5. Emit a ONE-PARAGRAPH rationale as your final text message after the tool calls. Not before.
|
|
586
|
+
|
|
587
|
+
Begin with \`hl_account_unified\` now. No preamble.`;
|
|
588
|
+
}
|
|
589
|
+
// ── Test session runner ────────────────────────────────────────────────────────
|
|
590
|
+
async function _runTestSessions(runSession, intent, portfolioIntent, workingDir, abort, msUntilEndTs, log, config) {
|
|
591
|
+
const sessions = [];
|
|
592
|
+
let sessionNum = 0;
|
|
593
|
+
// Run a single test session (tests don't need full cadence)
|
|
594
|
+
if (!abort.aborted && msUntilEndTs() > 0) {
|
|
595
|
+
sessionNum++;
|
|
596
|
+
const sessionId = `test-session-${sessionNum}`;
|
|
597
|
+
const startedAt = Date.now();
|
|
598
|
+
const sessionDir = join(workingDir, 'sessions', sessionId);
|
|
599
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
600
|
+
const transcriptPath = join(sessionDir, 'transcript.txt');
|
|
601
|
+
let stdout = '';
|
|
602
|
+
try {
|
|
603
|
+
const result = await runSession(sessionId, `Session ${sessionNum} for ${intent.id}`);
|
|
604
|
+
stdout = result.stdout;
|
|
605
|
+
}
|
|
606
|
+
catch (e) {
|
|
607
|
+
log({ level: 'warn', msg: 'claude-mcp-hyperliquid: test session threw', data: { err: e instanceof Error ? e.message : String(e) } });
|
|
608
|
+
}
|
|
609
|
+
writeFileSync(transcriptPath, stdout || '(test session — no output)');
|
|
610
|
+
sessions.push({
|
|
611
|
+
sessionId,
|
|
612
|
+
startedAt,
|
|
613
|
+
endedAt: Date.now(),
|
|
614
|
+
transcriptPath,
|
|
615
|
+
aborted: abort.aborted,
|
|
616
|
+
stdout,
|
|
617
|
+
initiatedFillTids: [],
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
return sessions;
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Write a thin ESM wrapper script to disk that delegates to the compiled
|
|
624
|
+
* startMcpServer() from this package. This is spawned as a subprocess by
|
|
625
|
+
* Claude's --mcp-config.
|
|
626
|
+
*
|
|
627
|
+
* Security: the config (including apiWalletPrivateKey) is written to a
|
|
628
|
+
* SEPARATE file (hl-server-config.json) with mode 0o600. The script body
|
|
629
|
+
* receives only the config file path via process.argv[2] — no private key
|
|
630
|
+
* or secret data appears in the script source.
|
|
631
|
+
*
|
|
632
|
+
* The import path is resolved at write-time from __dirname (this module's
|
|
633
|
+
* location) so it always points to the compiled mcp-tools.js alongside this
|
|
634
|
+
* file. The daemon MUST run from a compiled build (dist/) — `yarn build`
|
|
635
|
+
* produces `dist/restorer/impls/claude-mcp-hyperliquid/mcp-tools.js`, which
|
|
636
|
+
* this wrapper imports by absolute path from plain Node. Running from tsx
|
|
637
|
+
* against src/ would leave mcp-tools.js non-existent and cause Claude to
|
|
638
|
+
* silently spawn with no HL tools loaded — hence the explicit existence
|
|
639
|
+
* check and `E_DAEMON_MUST_RUN_FROM_DIST` error below.
|
|
640
|
+
*/
|
|
641
|
+
export function _writeHlMcpServerScript(outPath, hlConfig) {
|
|
642
|
+
// Absolute path to the compiled mcp-tools module, alongside this file.
|
|
643
|
+
const mcpToolsPath = join(__dirname, 'mcp-tools.js');
|
|
644
|
+
// Production guardrail: the generated wrapper is loaded by plain Node (the
|
|
645
|
+
// Claude subprocess's --mcp-config launches `node hl-server.mjs`), which
|
|
646
|
+
// can only import `.js`. Running the daemon via tsx against src/ leaves
|
|
647
|
+
// mcp-tools.js non-existent and Claude spawns with zero HL tools — a
|
|
648
|
+
// silent failure that looks like "Claude refused to trade." Skipped under
|
|
649
|
+
// NODE_ENV=test so vitest runs don't need a preceding `yarn build`.
|
|
650
|
+
if (process.env['NODE_ENV'] !== 'test' && !existsSync(mcpToolsPath)) {
|
|
651
|
+
throw new Error(`E_DAEMON_MUST_RUN_FROM_DIST: ${mcpToolsPath} does not exist. ` +
|
|
652
|
+
`The Jinn daemon must run from a compiled build (e.g. \`node dist/bin/jinn.js run\`), ` +
|
|
653
|
+
`not directly via tsx/ts-node against src/. ` +
|
|
654
|
+
`If you're developing, run \`yarn dev\` (build + run alias) instead of \`yarn jinn run\`. ` +
|
|
655
|
+
`If you installed via npm, the compiled artifact ships in the package — reinstall with ` +
|
|
656
|
+
`\`npm install -g @jinn-network/client@latest\` and use the \`jinn\` binary directly.`);
|
|
657
|
+
}
|
|
658
|
+
// Write config (including private key) to a separate file alongside the script,
|
|
659
|
+
// with restricted permissions — private key must not be world-readable.
|
|
660
|
+
const scriptDir = join(outPath, '..');
|
|
661
|
+
const configPath = join(scriptDir, 'hl-server-config.json');
|
|
662
|
+
// Write config with restricted permissions — private key must not be world-readable
|
|
663
|
+
writeFileSync(configPath, JSON.stringify(hlConfig), { encoding: 'utf-8', mode: 0o600 });
|
|
664
|
+
// Explicitly chmod in case the umask relaxed the mode
|
|
665
|
+
chmodSync(configPath, 0o600);
|
|
666
|
+
const script = `#!/usr/bin/env node
|
|
667
|
+
// Auto-generated HL MCP server wrapper — do not edit.
|
|
668
|
+
// Delegates to the compiled mcp-tools module so the real EIP-712 signing path
|
|
669
|
+
// is always active. See: ${mcpToolsPath}
|
|
670
|
+
//
|
|
671
|
+
// The private key is NOT embedded here — it is read from the config file
|
|
672
|
+
// passed as process.argv[2]. The config file is created at mode 0o600.
|
|
673
|
+
//
|
|
674
|
+
// We deliberately do NOT unlink the config file on exit. Each Claude session
|
|
675
|
+
// spawns a fresh hl-server.mjs child (via --mcp-config); deleting the config
|
|
676
|
+
// when session N exits means session N+1 starts with no HL tools and hangs
|
|
677
|
+
// until sessionMaxMs. The working directory is ephemeral — it gets cleared
|
|
678
|
+
// on next attempt — so leaving the mode-0o600 config in place is safe.
|
|
679
|
+
import { readFileSync } from 'node:fs';
|
|
680
|
+
import { startMcpServer } from ${JSON.stringify(mcpToolsPath)};
|
|
681
|
+
|
|
682
|
+
const configPath = process.argv[2];
|
|
683
|
+
if (!configPath) {
|
|
684
|
+
process.stderr.write('Usage: hl-server.mjs <config-file-path>\\n');
|
|
685
|
+
process.exit(1);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
689
|
+
await startMcpServer(config);
|
|
690
|
+
`.trim();
|
|
691
|
+
writeFileSync(outPath, script, { encoding: 'utf-8', mode: 0o600 });
|
|
692
|
+
}
|
|
693
|
+
// ── Jinn MCP launcher resolution ──────────────────────────────────────────────
|
|
694
|
+
import { dirname } from 'node:path';
|
|
695
|
+
import { fileURLToPath } from 'node:url';
|
|
696
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
697
|
+
function _resolveJinnMcpLauncher() {
|
|
698
|
+
const mcpDir = join(__dirname, '..', '..', '..', 'mcp');
|
|
699
|
+
const js = join(mcpDir, 'server.js');
|
|
700
|
+
const ts = join(mcpDir, 'server.ts');
|
|
701
|
+
if (existsSync(js)) {
|
|
702
|
+
return { command: process.execPath, args: [js] };
|
|
703
|
+
}
|
|
704
|
+
if (existsSync(ts)) {
|
|
705
|
+
return { command: process.execPath, args: ['--import', 'tsx', ts] };
|
|
706
|
+
}
|
|
707
|
+
return { command: process.execPath, args: [js] };
|
|
708
|
+
}
|
|
709
|
+
export default ClaudeMcpHyperliquidImpl;
|
|
710
|
+
//# sourceMappingURL=index.js.map
|