From b8dd52b0b169e4a5f6c20a134763121279724cd3 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 12:28:12 -0700 Subject: [PATCH 01/43] feat: scaffold strict TypeScript CLI and CI (#1) --- .github/workflows/ci.yml | 19 + .gitignore | 5 + .prettierrc.json | 1 + README.md | 7 + docs/PROGRESS.md | 5 + eslint.config.js | 2 + package-lock.json | 3217 ++++++++++++++++++++++++++++++++++++++ package.json | 31 + src/cli.ts | 3 + test/smoke.test.ts | 2 + tsconfig.json | 1 + 11 files changed, 3293 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .prettierrc.json create mode 100644 README.md create mode 100644 docs/PROGRESS.md create mode 100644 eslint.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/cli.ts create mode 100644 test/smoke.test.ts create mode 100644 tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d709e69 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,19 @@ +name: CI +on: + push: + branches: [main] + pull_request: +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - run: npm ci + - run: npm run build + - run: npm run lint + - run: npm run typecheck + - run: npm test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..242599d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +coverage/ +.env +*.tgz diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..fe00293 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1 @@ +{"singleQuote":true,"trailingComma":"all"} diff --git a/README.md b/README.md new file mode 100644 index 0000000..d180633 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# agent-cli + +Agent-native Hyperliquid trading CLI. Requires Node 20+; Bun supported. + +Development: `npm ci`, then `npm run build`, `npm run lint`, `npm run typecheck`, `npm test`. + +Local state lives in `~/.bloxwap`; configuration overrides use `BLOXWAP_`. diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md new file mode 100644 index 0000000..1b3e86b --- /dev/null +++ b/docs/PROGRESS.md @@ -0,0 +1,5 @@ +# Implementation progress + +| Issue | State | Notes | +| --- | --- | --- | +| #1 | done | Strict TypeScript ESM scaffold, bundled CLI, Vitest smoke test and CI checks. | diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..6b2de08 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,2 @@ +import tseslint from 'typescript-eslint'; +export default tseslint.config({ignores:['dist/**','node_modules/**']},...tseslint.configs.recommended); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e3ae0f4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3217 @@ +{ + "name": "@bloxwap/agent-cli", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@bloxwap/agent-cli", + "version": "0.1.0", + "bin": { + "agent-cli": "dist/cli.js" + }, + "devDependencies": { + "@types/node": "^26.6.5", + "eslint": "^9.39.5", + "prettier": "^3.9.10", + "tsup": "^8.5.1", + "typescript": "^6.0.3", + "typescript-eslint": "^8.71.2", + "vitest": "^3.2.7" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", + "dev": true, + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.6.tgz", + "integrity": "sha512-FkO19Fowv/OQeJZPVfdetJaI3/VpbZmBsnmAlR/rQ+b3QwfiGQiOpVegpSRH331kelvHTVw9hXYG3DwjAaRzyQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.6.tgz", + "integrity": "sha512-si5/Etm6MphPc2xYHnk455Shls/hoMjTy3mhXge4OPmvCw7TwC4KP4fKokAlKLV4XaQvLvgSHfafPcblLMYYdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.6.tgz", + "integrity": "sha512-PQCl3q7b0BgvVt72i2xrWM6I5KliwwVe4GpWW5QDGyUQawDS9aPZss/aGyFqBmxdRxlE8wDc9CaxiQ5DHRSp/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.6.tgz", + "integrity": "sha512-l9DFl1Itb1YYdSrP1K3U8ih44AOHa/p+Q4WMMDfGvH5qVWp+ajfgS1DlYo4yCRh8wJ2ZJv0Fqccf+ULAmMFjmg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.6.tgz", + "integrity": "sha512-bR4GR3205N9+od0+l5Dd9/iZTDpJkIe6d6Ydp2qbcQZJLmKBdYQQRp8gbWpbMt92twKJY66o1vK4NkoKQODIrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.6.tgz", + "integrity": "sha512-XTVNaDzcFl2TeBglvxdGgsCv9JGtAGRAoJENmGBR/XvDH2mA2K0IXc2aMfP0dR8mexSXSKifJ85FixoXX1wfTw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.6.tgz", + "integrity": "sha512-qWc6qbc+25ugG9vqPR9P422KJBbVfZ05mjcMggoq8tsfWP+eRfC+/tiDdIlRF5VL2pDVZWN9zAhWsmu7R1+7Dw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.6.tgz", + "integrity": "sha512-V4Nwkp8844X/Xnww3PI1LYS0YPei8XQEVG70R+5MPZGO7AhYoun4hCN7Svs4U3wbQLZbwiz+9xRFhUZZdRl8Ww==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.6.tgz", + "integrity": "sha512-xb72A1aPaaIsp+a88VtRae/ovIEXislbUMyp7b9AQQU7ZkP2m/+ik6DW9TQcJyw5R8/RIailF9zExYwkjR++UA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.6.tgz", + "integrity": "sha512-pbcs1BVBwC43hZkjhjlF8Rx8m5G0KnhLZs9Azy/gqccP6UsNSzJr0hKGIAVPqzAH1O/NdPxxC9GVGb4msQg/aw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.6.tgz", + "integrity": "sha512-EKOC8os6Vsg487ZLKUqzLF99Jsmnq6tu7O7mAELakNpcA+8KadktFf/JYdRuHe/DzCUXmN38pUKu4XBcKlqGbQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.6.tgz", + "integrity": "sha512-v/SQ68GnSlr/HyjB79sNXesGDwkOPu9JjbHfmWaymCiM+nZsQ9TstG1KfA4MuEvTr2d3PoY8PhFxj9bt0e32JA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.6.tgz", + "integrity": "sha512-G3pmliMQ7r15zFFNwBlaT/xTdSLi4lfgI7CUrQueyKYheNVhrd8JrWCE6Ll8xf4AA1RZFqq+mNJYntiElsVOyA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.6.tgz", + "integrity": "sha512-XQTB/IPoB/wG6x/wu9uomAt8YH61IAr5iZTubMiad/lu88sotq2ocz9hJd556+Jz2kIkF7ZQZIsUuWE3OWqtcA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.6.tgz", + "integrity": "sha512-O5mpokth6+ydMuwiGtY8fIjvOBfH7u6SzmDNmZ3k6GK5C6iEAC2q2ZN/T5L9jYILx0IcCT0KnfMDihQ5yQFSBw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.6.tgz", + "integrity": "sha512-aIFxbAD9v3TvIaEDsKmukjcDg1JS4/tFXw8SHyQQgzOwzWCQ4cyDK7Ju1pQKqUXDFkEu/BZCFDCDATsKhQcB/Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.6.tgz", + "integrity": "sha512-fuNo1UwBc6BxnHDOxxguQL/InhheMN3RJcJMb0b4xP8r5Q3CNCrJbVg0eCIFuJ4e/zAfjR2gYbOfbzO/G7CIyw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.6.tgz", + "integrity": "sha512-05v6wFKDKJPZs9xlytVrrRu6IbRuoSvlH9xNwEOafXsKyjBFZPvAcZz2VIAB/M+lblYeKa9iOYC4RDVdfcAAaw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.6.tgz", + "integrity": "sha512-wJXKcdMjvkc/FaavZXNP1yW4i0pfOmhZAIWRSLG2yJ0PB0TWi7dkLErhX0u0vptDcXKKO/m1Y/YTeuyvb2MXwg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.6.tgz", + "integrity": "sha512-cwg7/h0ThvJwy8CBkmACdiHAxRWpbsCmIsjOt+Zzx0hTnIcSUmKYEV03bGg+hD827OnwUsbKh1ZZDW754xWyBA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.6.tgz", + "integrity": "sha512-T0RkYIJbdYXJEQFw+tMBO09KDFdgmNxkHsof3wWAABrvnx15Yh8RytmrgYy9+q9cBeBNZF66MMQZC+5nzEz6vg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.6.tgz", + "integrity": "sha512-KlfpmF1AAIdAWxt8QQNClbpLFlAUfYOzqIuJ7IXEStMHpFRuH78RG6FjqPb3Rqhh0K89F5npvG+/i3Fjl6IveQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.6.tgz", + "integrity": "sha512-MrTLjd7Jlt9/ZavJZYG61KQv9Iy9bzbY+yXy73Xvwoa2uP66wvDShkyBr5Chz/yrE6lbD/zHMuTmKQVNQqBApg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.6.tgz", + "integrity": "sha512-8fWDH1CkPI8GdBKAxzHDza7UmgficsKo2+MVbvUgOGhB0629w5sHMg3Qu5y7TZsrYKHHWpz8fla4KvoT7WsO3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.6.tgz", + "integrity": "sha512-9/c9PWdr5GCIHkkxiHM1HjNBQMVdftP4O8QTcoUAaRza2FSqJ4axbf8QAnBuE6BIGQDl71SaRPeDdDYid5ecTw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "26.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", + "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", + "dev": true, + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.2.tgz", + "integrity": "sha512-K72/pvT7qpm8q3k8G5u93NCSN1RcLxX5h7Gw0bQpbMXviBHeY6Nx1WEwmk9wP+eKTSQ8ecxvcgt/ZX/rB938xQ==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/type-utils": "8.71.2", + "@typescript-eslint/utils": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.71.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.12.tgz", + "integrity": "sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.2.tgz", + "integrity": "sha512-aTtmZjp/x3gsGHkDs04/1I8kNogBJkpQaeGaN0PfiG43NzDEmW2n+OP5im9wfsvyX0yP1UZWBrAGtDBfH9jEVw==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.2.tgz", + "integrity": "sha512-Pd68GsLVh5ZXlcEccFPdhSOvtHZ/2WU0XsA8zmXOS3F158lVlcTLhfQ6qvtL1RzbMie0Kp8xtbVwAg4nejKPHw==", + "dev": true, + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.71.2", + "@typescript-eslint/types": "^8.71.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.2.tgz", + "integrity": "sha512-YF4wgg5iUX5Obywoac27B56FhozMNFhdJSbh11H5GBAwmBtfyRhmgTQqW9tRNNmHDvsZ2CX6UGzoz+YfYn2Leg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.2.tgz", + "integrity": "sha512-jz49TAWokm/7yWD9/o837nYax9DyALxIPOImdImvVzo6canBCZYzin8zJyqNR5k1j9K1DE0bVvr0XYB+99uAdg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.2.tgz", + "integrity": "sha512-cC8UaalCViJkb3rH/xLIvidEwoFRtMZMUqDjjJMGEKaTFwYRHYBcoczlEGHuvkHEO8svRDrgZ0yQgOArFmwang==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/utils": "8.71.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.2.tgz", + "integrity": "sha512-y5Ukl3oVp7e/VGxqIoEXoeb/Roj71PAJ4WG/N/RGj+eeDvKRLM15kGr9MijLbN0i03S52zxu31Z98E5gWChJKw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.2.tgz", + "integrity": "sha512-DgNOEsl4pdYzwSL+N/Cl33tX1Hlw3FvId73LnKXfq7jnrF2viOEn7X+P7RBkkSWCVm92gnWd28GIC5vJuLREeA==", + "dev": true, + "dependencies": { + "@typescript-eslint/project-service": "8.71.2", + "@typescript-eslint/tsconfig-utils": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.2.tgz", + "integrity": "sha512-RyKa1G1M9rKHDhrk+HJtiF1MvLD5Ux2JW3TSkzw+02pRALdpJb+QzG1Y5em+LvCgzGwgg8H7mBRFKljNz7ImPw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.2.tgz", + "integrity": "sha512-zozlN0UA32bSG2zkKAgQyRYSbEISVoyU6vvkKYo2aSny74D8Jh4e+POkJ6Lk0dlezZi5x7TtD+88Xb06IB8nhg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.71.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.30", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz", + "integrity": "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz", + "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.6.tgz", + "integrity": "sha512-fX1Vy+UNKdXn+xKDnu/HBXbHVr1AY8cSdQj9BU4kLv5aFCMqGpvrVbyUMmjN3DdzWhSAuTbcX8PCNEVaumthYA==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.64.6", + "@rollup/rollup-android-arm64": "4.64.6", + "@rollup/rollup-darwin-arm64": "4.64.6", + "@rollup/rollup-darwin-x64": "4.64.6", + "@rollup/rollup-freebsd-arm64": "4.64.6", + "@rollup/rollup-freebsd-x64": "4.64.6", + "@rollup/rollup-linux-arm-gnueabihf": "4.64.6", + "@rollup/rollup-linux-arm-musleabihf": "4.64.6", + "@rollup/rollup-linux-arm64-gnu": "4.64.6", + "@rollup/rollup-linux-arm64-musl": "4.64.6", + "@rollup/rollup-linux-loong64-gnu": "4.64.6", + "@rollup/rollup-linux-loong64-musl": "4.64.6", + "@rollup/rollup-linux-ppc64-gnu": "4.64.6", + "@rollup/rollup-linux-ppc64-musl": "4.64.6", + "@rollup/rollup-linux-riscv64-gnu": "4.64.6", + "@rollup/rollup-linux-riscv64-musl": "4.64.6", + "@rollup/rollup-linux-s390x-gnu": "4.64.6", + "@rollup/rollup-linux-x64-gnu": "4.64.6", + "@rollup/rollup-linux-x64-musl": "4.64.6", + "@rollup/rollup-openbsd-x64": "4.64.6", + "@rollup/rollup-openharmony-arm64": "4.64.6", + "@rollup/rollup-win32-arm64-msvc": "4.64.6", + "@rollup/rollup-win32-ia32-msvc": "4.64.6", + "@rollup/rollup-win32-x64-gnu": "4.64.6", + "@rollup/rollup-win32-x64-msvc": "4.64.6", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", + "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/tsup/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.71.2.tgz", + "integrity": "sha512-Rz4effbMy2g9wnvOJZG6SbVavX313Lap1vNEbk1RUochzickvzP8P4HB+bkNg5e50eVz1hQGjh9K1omI1xB4Bw==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.71.2", + "@typescript-eslint/parser": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/utils": "8.71.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "dev": true + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.7.tgz", + "integrity": "sha512-AHEzTORMbi7/7aXp7w7KZ5+DCUdSeLsJz0grY8xHpc9DHdYb0vQu6DqJfm2tOw3gDExNgPgnJTXxM6syYFHaCA==", + "dev": true, + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..cd32f5b --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "@bloxwap/agent-cli", + "version": "0.1.0", + "type": "module", + "bin": { + "agent-cli": "dist/cli.js" + }, + "engines": { + "node": ">=20" + }, + "files": [ + "dist", + "SKILL.md", + "policies" + ], + "scripts": { + "build": "tsup src/cli.ts --format esm --clean", + "lint": "eslint .", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "devDependencies": { + "@types/node": "^26.6.5", + "eslint": "^9.39.5", + "prettier": "^3.9.10", + "tsup": "^8.5.1", + "typescript": "^6.0.3", + "typescript-eslint": "^8.71.2", + "vitest": "^3.2.7" + } +} diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..ef9aab8 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,3 @@ +#!/usr/bin/env node +export const version = '0.1.0'; +console.log(JSON.stringify({ ok: true, data: { version } })); diff --git a/test/smoke.test.ts b/test/smoke.test.ts new file mode 100644 index 0000000..abad2b5 --- /dev/null +++ b/test/smoke.test.ts @@ -0,0 +1,2 @@ +import { expect, test } from 'vitest'; +test('strict ESM runtime', () => expect(import.meta.url).toMatch(/^file:/)); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0827a67 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1 @@ +{"compilerOptions":{"target":"ES2022","module":"NodeNext","moduleResolution":"NodeNext","strict":true,"esModuleInterop":true,"skipLibCheck":true,"resolveJsonModule":true,"outDir":"dist"},"include":["src","test"]} From f9ac4c468b751fb6461739df5166b7191e18c263 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 12:28:30 -0700 Subject: [PATCH 02/43] feat: define private runner interface ADR (#2) --- docs/PROGRESS.md | 2 ++ docs/adr/0001-runner-interface.md | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 docs/adr/0001-runner-interface.md diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 1b3e86b..29cf3cf 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -3,3 +3,5 @@ | Issue | State | Notes | | --- | --- | --- | | #1 | done | Strict TypeScript ESM scaffold, bundled CLI, Vitest smoke test and CI checks. | + +| #2 | done | ADR selects private newline-framed JSON-RPC socket; defines methods and Windows strategy. | diff --git a/docs/adr/0001-runner-interface.md b/docs/adr/0001-runner-interface.md new file mode 100644 index 0000000..3352337 --- /dev/null +++ b/docs/adr/0001-runner-interface.md @@ -0,0 +1,23 @@ +# ADR 0001: local JSON-RPC runner interface + +Status: accepted. + +Use newline-delimited JSON-RPC 2.0 over a Unix socket at `~/.bloxwap/run.sock`. +Each request/response occupies one UTF-8 line, with a bounded frame size. +Methods use resource.action names: `order.place`, `order.cancel`, `order.modify`, +`leverage.set`, `status.get`, `balance.get`, `positions.get`, `orders.get`, +`config.get`, `runner.shutdown`. Short CLI command names may be aliases. + +A socket supports multiple CLI clients and keeps signing and nonce ownership in +one process. The state directory is 0700 and the socket 0600; access is authorized +by the OS user. Never expose this protocol on a network address. Same-user malware +is outside this permission boundary. Validate every request independently. + +Stdin JSON-RPC is simple for one parent process but cannot support independent CLI +clients. Localhost HTTP supports clients and Windows but adds port discovery, +network exposure and authentication requirements. Neither is the default. + +On Windows use a named pipe with a name derived from the user's home directory, +with access restricted to the current user. Unix permission bits do not secure +Windows pipes: deployment must enforce the current-user ACL. Direct mode remains +available if the platform cannot establish a private local transport. From a1d91048d30fd23e090d79350ca0689816910a98 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 12:28:57 -0700 Subject: [PATCH 03/43] feat: normalize errors and document stable codes (#3) --- docs/PROGRESS.md | 1 + docs/errors.md | 25 +++++++++++++++++++++++++ scripts/check-commit.sh | 8 ++++++++ src/errors/index.ts | 20 ++++++++++++++++++++ test/errors.test.ts | 8 ++++++++ 5 files changed, 62 insertions(+) create mode 100644 docs/errors.md create mode 100644 scripts/check-commit.sh create mode 100644 src/errors/index.ts create mode 100644 test/errors.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 29cf3cf..e113170 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -5,3 +5,4 @@ | #1 | done | Strict TypeScript ESM scaffold, bundled CLI, Vitest smoke test and CI checks. | | #2 | done | ADR selects private newline-framed JSON-RPC socket; defines methods and Windows strategy. | +| #3 | done | Stable error catalog, serialization, exit codes and append-only guard. | diff --git a/docs/errors.md b/docs/errors.md new file mode 100644 index 0000000..cdc84fc --- /dev/null +++ b/docs/errors.md @@ -0,0 +1,25 @@ +# Stable errors + +Failures always have `{ok:false,code,message,retryable,details}`; successes have +`{ok:true,data}`. Codes are append-only. Retry only when retryable is true, using +bounded backoff. Exit 2 means invalid configuration/input, 75 means temporary +failure, 1 means a permanent failure. + +| Code | Meaning | Retryable | Agent action | +| --- | --- | --- | --- | +| INSUFFICIENT_MARGIN | Account margin too low | false | Reduce size or fund | +| POLICY_REJECTED | Local or custody policy denied | false | Adjust intent | +| AGENT_EXPIRED | Agent expired | false | Rotate | +| AGENT_NOT_APPROVED | Agent or builder unapproved | false | Complete init | +| DEPOSIT_PENDING | Deposit unsettled | true | Poll status | +| NONCE_COLLISION | Nonce reused | true | Generate next nonce | +| RATE_LIMIT | Upstream throttled | true | Back off | +| INVALID_SIZE | Invalid amount | false | Correct size | +| INVALID_PRICE | Invalid price | false | Correct price | +| UNKNOWN_MARKET | Market absent | false | Refresh metadata | +| ORDER_NOT_FOUND | Order absent | false | Refresh orders | +| RUNNER_NOT_RUNNING | Runner unavailable | true | Start runner | +| CONFIG_INVALID | Invalid input/config | false | Correct configuration | +| NETWORK_ERROR | Transport failure | true | Back off | +| UPSTREAM_ERROR | Unrecognized service failure | false | Inspect redacted details | +| INTERNAL | Unexpected local failure | false | Inspect logs | diff --git a/scripts/check-commit.sh b/scripts/check-commit.sh new file mode 100644 index 0000000..6180041 --- /dev/null +++ b/scripts/check-commit.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +npm run build +npm run lint +npm run typecheck +npm test +git add . +git commit -m "$1" diff --git a/src/errors/index.ts b/src/errors/index.ts new file mode 100644 index 0000000..aa0de68 --- /dev/null +++ b/src/errors/index.ts @@ -0,0 +1,20 @@ +export const errorDefaults = { + INSUFFICIENT_MARGIN: false, POLICY_REJECTED: false, AGENT_EXPIRED: false, + AGENT_NOT_APPROVED: false, DEPOSIT_PENDING: true, NONCE_COLLISION: true, + RATE_LIMIT: true, INVALID_SIZE: false, INVALID_PRICE: false, + UNKNOWN_MARKET: false, ORDER_NOT_FOUND: false, RUNNER_NOT_RUNNING: true, + CONFIG_INVALID: false, NETWORK_ERROR: true, UPSTREAM_ERROR: false, INTERNAL: false, +} as const; +export type ErrorCode = keyof typeof errorDefaults; +export class AgentCliError extends Error { + readonly retryable: boolean; + constructor(readonly code: ErrorCode, message: string, readonly details: unknown = null, retryable = errorDefaults[code] as boolean) { + super(message); this.name = 'AgentCliError'; this.retryable = retryable; + } + toJSON() { return { ok: false as const, code: this.code, message: this.message, retryable: this.retryable, details: this.details }; } + get exitCode() { return this.code === 'CONFIG_INVALID' ? 2 : this.retryable ? 75 : 1; } +} +export function normalizeError(error: unknown): AgentCliError { + return error instanceof AgentCliError ? error : new AgentCliError('INTERNAL', error instanceof Error ? error.message : String(error)); +} +export const success = (data: T) => ({ ok: true as const, data }); diff --git a/test/errors.test.ts b/test/errors.test.ts new file mode 100644 index 0000000..6903f70 --- /dev/null +++ b/test/errors.test.ts @@ -0,0 +1,8 @@ +import { expect, test } from 'vitest'; +import { AgentCliError, errorDefaults, success } from '../src/errors/index.js'; +const original = ['INSUFFICIENT_MARGIN','POLICY_REJECTED','AGENT_EXPIRED','AGENT_NOT_APPROVED','DEPOSIT_PENDING','NONCE_COLLISION','RATE_LIMIT','INVALID_SIZE','INVALID_PRICE','UNKNOWN_MARKET','ORDER_NOT_FOUND','RUNNER_NOT_RUNNING','CONFIG_INVALID','NETWORK_ERROR','UPSTREAM_ERROR','INTERNAL']; +test('catalog is append-only', () => original.forEach(code => expect(errorDefaults).toHaveProperty(code))); +test('stable wire shapes', () => { + expect(new AgentCliError('RATE_LIMIT','wait').toJSON()).toEqual({ok:false,code:'RATE_LIMIT',message:'wait',retryable:true,details:null}); + expect(success('yes')).toEqual({ok:true,data:'yes'}); +}); From b31617feb298698cf2e01da708cf41aa493c178c Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 12:29:40 -0700 Subject: [PATCH 04/43] feat: add validated JSON command framework (#4) --- docs/PROGRESS.md | 1 + package-lock.json | 502 +++++++++++++++++++++++++++++++++++++++++++- package.json | 7 + src/cli.ts | 8 +- src/core/router.ts | 45 ++++ test/router.test.ts | 15 ++ 6 files changed, 570 insertions(+), 8 deletions(-) create mode 100644 src/core/router.ts create mode 100644 test/router.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index e113170..1a1288d 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -6,3 +6,4 @@ | #2 | done | ADR selects private newline-framed JSON-RPC socket; defines methods and Windows strategy. | | #3 | done | Stable error catalog, serialization, exit codes and append-only guard. | +| #4 | done | Validated JSON/flags/stdin router, JSON schemas, envelopes and exit codes. | diff --git a/package-lock.json b/package-lock.json index e3ae0f4..6fdedb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,11 +7,18 @@ "": { "name": "@bloxwap/agent-cli", "version": "0.1.0", + "dependencies": { + "@bloxwap/hyperliquid": "^0.6.0", + "@privy-io/node": "^0.35.0", + "qrcode": "^1.5.4", + "zod": "^4.6.7" + }, "bin": { "agent-cli": "dist/cli.js" }, "devDependencies": { "@types/node": "^26.6.5", + "@types/qrcode": "^1.5.6", "eslint": "^9.39.5", "prettier": "^3.9.10", "tsup": "^8.5.1", @@ -23,6 +30,22 @@ "node": ">=20" } }, + "node_modules/@bloxwap/hyperliquid": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@bloxwap/hyperliquid/-/hyperliquid-0.6.0.tgz", + "integrity": "sha512-ryQya36sx5tD0xg9USFg+6krpMvPlHvc55p42FXJlCN6RTWO9obA6QknZpM1RRTPAl5w04Jj16kQxLcesEWl0Q==", + "dependencies": { + "@noble/hashes": "2.2.0", + "valibot": "1.4.2" + }, + "engines": { + "node": ">=22.12.0" + }, + "optionalDependencies": { + "hash-wasm": "4.12.0", + "tiny-secp256k1": "2.2.4" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", @@ -573,6 +596,36 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hpke/chacha20poly1305": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@hpke/chacha20poly1305/-/chacha20poly1305-1.8.0.tgz", + "integrity": "sha512-FcBfAQ+Y99vMNJP2yrZ9wpL8V0GOwp1+zMyzvc6alasrBygfFjFm1yeUtyADJCu/27C3Lm5mJzx6u7pwg+cX5w==", + "dependencies": { + "@hpke/common": "^1.10.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@hpke/common": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@hpke/common/-/common-1.10.1.tgz", + "integrity": "sha512-moJwhmtLtuxiUzzNp1jpfBfx8yefKoO9D/RCR9dmwrnc7qjJqId1rEtQz+lSlU5cabX8daToMSx/7HayXOiaFw==", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@hpke/core": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@hpke/core/-/core-1.9.0.tgz", + "integrity": "sha512-pFxWl1nNJeQCSUFs7+GAblHvXBCjn9EPN65vdKlYQil2aURaRxfGMO6vBKGqm1YHTKwiAxJQNEI70PbSowMP9Q==", + "dependencies": { + "@hpke/common": "^1.10.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -685,6 +738,93 @@ "node": "^22.20 || ^24.12 || >=25" } }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@privy-io/node": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@privy-io/node/-/node-0.35.0.tgz", + "integrity": "sha512-NhttAOmjgwtQzBoTmRCWzY5ZTTJV1WqPUmasyEbi9XfFd1LccpjHiNB+AUCeRXpY0FC5Bt/3Is7xJEmCX3LKNA==", + "dependencies": { + "@hpke/chacha20poly1305": "^1.7.1", + "@hpke/core": "^1.7.5", + "@noble/curves": "^1.9.7", + "@noble/hashes": "^1.8.0", + "@scure/base": "^1.2.5", + "canonicalize": "^2.1.0", + "jose": "^6.1.0", + "lru-cache": "^11.1.0", + "svix": "^1.92.2" + }, + "peerDependencies": { + "@solana/kit": "^5.1.0", + "@x402/evm": "^2.3.0", + "@x402/fetch": "^2.3.0", + "@x402/svm": "^2.3.0", + "viem": "^2.44.2" + }, + "peerDependenciesMeta": { + "@solana/kit": { + "optional": true + }, + "@x402/evm": { + "optional": true + }, + "@x402/fetch": { + "optional": true + }, + "@x402/svm": { + "optional": true + }, + "viem": { + "optional": true + } + } + }, + "node_modules/@privy-io/node/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.64.6", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.6.tgz", @@ -1010,6 +1150,19 @@ "win32" ] }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==" + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -1047,6 +1200,15 @@ "undici-types": "~8.9.0" } }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.71.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.2.tgz", @@ -1459,11 +1621,18 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -1544,6 +1713,22 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/canonicalize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/canonicalize/-/canonicalize-2.1.0.tgz", + "integrity": "sha512-F705O3xrsUtgt98j7leetNhTWPe+5S72rlL5O4jA1pKqBVQ/dT1O1D6PFxmSXvc0SUOinWS57DKx0I3CHrXJHQ==", + "bin": { + "canonicalize": "bin/canonicalize.js" + } + }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", @@ -1600,11 +1785,20 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -1615,8 +1809,7 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/commander": { "version": "4.1.1", @@ -1679,6 +1872,14 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -1694,6 +1895,16 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -1936,6 +2147,11 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2025,6 +2241,14 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2058,6 +2282,12 @@ "node": ">=8" } }, + "node_modules/hash-wasm": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.12.0.tgz", + "integrity": "sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ==", + "optional": true + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2101,6 +2331,14 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2119,6 +2357,14 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/jose": { + "version": "6.2.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", @@ -2250,6 +2496,14 @@ "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", "dev": true }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -2380,6 +2634,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -2396,7 +2658,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "engines": { "node": ">=8" } @@ -2463,6 +2724,14 @@ "pathe": "^2.0.1" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/postcss": { "version": "8.5.30", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz", @@ -2566,6 +2835,22 @@ "node": ">=6" } }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -2579,6 +2864,19 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2645,6 +2943,11 @@ "node": ">=10" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2696,12 +2999,45 @@ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, "node_modules/std-env": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "dev": true }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -2760,6 +3096,14 @@ "node": ">=8" } }, + "node_modules/svix": { + "version": "1.99.1", + "resolved": "https://registry.npmjs.org/svix/-/svix-1.99.1.tgz", + "integrity": "sha512-JfpvbAg5iT5NagDAfOq3e6Ci6NbOMbMm6C3DnfDBB60m2JDK+Z2hXPE9XNAmutb53DCdPoih1V70IbklZnX09A==", + "dependencies": { + "standardwebhooks": "1.0.0" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -2781,6 +3125,18 @@ "node": ">=0.8" } }, + "node_modules/tiny-secp256k1": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.4.tgz", + "integrity": "sha512-FoDTcToPqZE454Q04hH9o2EhxWsm7pOSpicyHkgTwKhdKWdsTUuqfP5MLq3g+VjAtl2vSx6JpXGdwA2qpYkI0Q==", + "optional": true, + "dependencies": { + "uint8array-tools": "0.0.7" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -2940,7 +3296,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2978,6 +3334,15 @@ "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", "dev": true }, + "node_modules/uint8array-tools": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.7.tgz", + "integrity": "sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ==", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/undici-types": { "version": "8.9.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", @@ -2993,6 +3358,19 @@ "punycode": "^2.1.0" } }, + "node_modules/valibot": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.2.tgz", + "integrity": "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/vite": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.7.tgz", @@ -3176,6 +3554,11 @@ "node": ">= 8" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -3201,6 +3584,105 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -3212,6 +3694,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index cd32f5b..290e94b 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,18 @@ }, "devDependencies": { "@types/node": "^26.6.5", + "@types/qrcode": "^1.5.6", "eslint": "^9.39.5", "prettier": "^3.9.10", "tsup": "^8.5.1", "typescript": "^6.0.3", "typescript-eslint": "^8.71.2", "vitest": "^3.2.7" + }, + "dependencies": { + "@bloxwap/hyperliquid": "^0.6.0", + "@privy-io/node": "^0.35.0", + "qrcode": "^1.5.4", + "zod": "^4.6.7" } } diff --git a/src/cli.ts b/src/cli.ts index ef9aab8..d5734f3 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,3 +1,7 @@ #!/usr/bin/env node -export const version = '0.1.0'; -console.log(JSON.stringify({ ok: true, data: { version } })); +import { dispatch } from './core/router.js'; +let stdin=''; +if (!process.stdin.isTTY) for await (const chunk of process.stdin) stdin += String(chunk); +const result=await dispatch(process.argv.slice(2),stdin); +process.stdout.write(JSON.stringify(result.body,null,result.context.pretty ? 2 : undefined)+'\n'); +process.exitCode=result.exitCode; diff --git a/src/core/router.ts b/src/core/router.ts new file mode 100644 index 0000000..a6e8b12 --- /dev/null +++ b/src/core/router.ts @@ -0,0 +1,45 @@ +import { z } from 'zod'; +import { AgentCliError, normalizeError, success } from '../errors/index.js'; +export interface Context { configPath?: string; network?: 'mainnet' | 'testnet'; pretty: boolean; } +export interface Command { description: string; schema: z.ZodType; execute: (input: Record, context: Context) => Promise; } +export const commands = new Map(); +export function register(name: string, command: Command) { commands.set(name, command); } +export function schemas(name: string) { + const command = commands.get(name); + if (!command) throw new AgentCliError('CONFIG_INVALID', `Unknown command: ${name}`); + return { input: z.toJSONSchema(command.schema), output: {oneOf:[{type:'object',required:['ok','data'],properties:{ok:{const:true},data:{}}},{type:'object',required:['ok','code','message','retryable','details'],properties:{ok:{const:false},code:{type:'string'},message:{type:'string'},retryable:{type:'boolean'},details:{}}}]} }; +} +function parseValue(value: string): unknown { try { return JSON.parse(value); } catch { return value; } } +export async function dispatch(argv: string[], stdin = '') { + const input: Record = {}; const positional: string[] = []; + const context: Context = { pretty: false }; + try { + for (let i=0;ic.toUpperCase())]=false; continue; } + if (arg.startsWith('--')) { + const [flag,inline] = arg.slice(2).split('=',2); const key=flag.replace(/-([a-z])/g,(_,c: string)=>c.toUpperCase()); + input[key]=inline !== undefined ? parseValue(inline) : argv[i+1] && !argv[i+1].startsWith('--') ? parseValue(argv[++i]) : true; continue; + } + positional.push(arg); + } + if (stdin.trim()) Object.assign(input,z.record(z.string(),z.unknown()).parse(JSON.parse(stdin))); + const [name='help', ...args]=positional; + if (name === 'help') return {body:success({commands:[...commands].map(([name,c])=>({name,description:c.description})),globalFlags:['--json','--config','--network','--pretty']}),exitCode:0,context}; + if (name === 'schema') return {body:success(schemas(args[0])),exitCode:0,context}; + const command=commands.get(name); if (!command) throw new AgentCliError('CONFIG_INVALID',`Unknown command: ${name}`); + if (args.length) input.args=args; + const parsed=command.schema.safeParse(input); + if (!parsed.success) throw new AgentCliError('CONFIG_INVALID','Invalid command input',parsed.error.flatten()); + const data=await command.execute(parsed.data as Record,context); + return {body:success(data),exitCode:0,context}; + } catch (error) { + const normalized=error instanceof z.ZodError || error instanceof SyntaxError ? new AgentCliError('CONFIG_INVALID','Invalid JSON or flags') : normalizeError(error); + return {body:normalized.toJSON(),exitCode:normalized.exitCode,context}; + } +} diff --git a/test/router.test.ts b/test/router.test.ts new file mode 100644 index 0000000..8032a9f --- /dev/null +++ b/test/router.test.ts @@ -0,0 +1,15 @@ +import { expect, test } from 'vitest'; +import { z } from 'zod'; +import { dispatch, register } from '../src/core/router.js'; +register('echo',{description:'test',schema:z.object({value:z.string()}),execute:async input=>input}); +register('crash',{description:'test',schema:z.object({}),execute:async()=>{throw new Error('unexpected');}}); +test('flags and stdin give one success envelope',async()=>{ + expect((await dispatch(['echo','--value','hello'])).body).toEqual({ok:true,data:{value:'hello'}}); + expect((await dispatch(['echo'],'{"value":"stdin"}')).exitCode).toBe(0); + expect((await dispatch(['echo','--json','{"value":"json"}'])).exitCode).toBe(0); +}); +test('validation and unexpected failures normalize',async()=>{ + expect((await dispatch(['echo'])).body).toMatchObject({ok:false,code:'CONFIG_INVALID'}); + expect((await dispatch(['crash'])).body).toMatchObject({ok:false,code:'INTERNAL'}); + expect((await dispatch(['schema','echo'])).exitCode).toBe(0); +}); From 86b1176e6dc6ea8265ed24d66773b8bf06fc05c6 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 12:30:25 -0700 Subject: [PATCH 05/43] feat: add private versioned configuration store (#5) --- docs/PROGRESS.md | 1 + docs/config.md | 11 ++++++++++ src/cli.ts | 1 + src/commands/config.ts | 15 ++++++++++++++ src/core/config.ts | 47 ++++++++++++++++++++++++++++++++++++++++++ test/config.test.ts | 13 ++++++++++++ 6 files changed, 88 insertions(+) create mode 100644 docs/config.md create mode 100644 src/commands/config.ts create mode 100644 src/core/config.ts create mode 100644 test/config.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 1a1288d..8f68798 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -7,3 +7,4 @@ | #2 | done | ADR selects private newline-framed JSON-RPC socket; defines methods and Windows strategy. | | #3 | done | Stable error catalog, serialization, exit codes and append-only guard. | | #4 | done | Validated JSON/flags/stdin router, JSON schemas, envelopes and exit codes. | +| #5 | done | Versioned validated config, atomic private writes, migration, commands and env overrides. | diff --git a/docs/config.md b/docs/config.md new file mode 100644 index 0000000..1d76805 --- /dev/null +++ b/docs/config.md @@ -0,0 +1,11 @@ +# Configuration + +Default path: `~/.bloxwap/config.json`. `--config` selects another file. +Version 1 is validated on read and write; versionless config migrates to version 1. +Unknown versions fail closed. Writes use a private temporary file and atomic rename. + +`BLOXWAP_NETWORK`, `BLOXWAP_LOG_LEVEL`, `BLOXWAP_PRIVY_APP_ID` and +`BLOXWAP_MASTER_ADDRESS` override stored values in commands. `config get` shows +persisted values; `config validate` validates them without overrides. +Secrets use `env:BLOXWAP_PRIVY_APP_SECRET` and `env:BLOXWAP_AUTHORIZATION_KEY` +references; authorization keys may also use `file:/absolute/private/path`. diff --git a/src/cli.ts b/src/cli.ts index d5734f3..d3d6ae2 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,5 +1,6 @@ #!/usr/bin/env node import { dispatch } from './core/router.js'; +import './commands/config.js'; let stdin=''; if (!process.stdin.isTTY) for await (const chunk of process.stdin) stdin += String(chunk); const result=await dispatch(process.argv.slice(2),stdin); diff --git a/src/commands/config.ts b/src/commands/config.ts new file mode 100644 index 0000000..069fc19 --- /dev/null +++ b/src/commands/config.ts @@ -0,0 +1,15 @@ +import { z } from 'zod'; +import { register } from '../core/router.js'; +import { loadConfig, saveConfig, migrate } from '../core/config.js'; +import { AgentCliError } from '../errors/index.js'; +register('config',{description:'Get, set or validate local configuration',schema:z.object({args:z.array(z.string()).default([])}),execute:async({args},context)=>{ + const [action='get',key,value]=args as string[]; const config=await loadConfig(context.configPath,{}); + if(action==='validate') return {valid:true,version:config.version}; + if(action==='get') return key ? key.split('.').reduce((v,k)=>(v as Record)?.[k],config) ?? null : config; + if(action!=='set' || !key || value===undefined) throw new AgentCliError('CONFIG_INVALID','Use config set key value'); + let parsed:unknown; try {parsed=JSON.parse(value);} catch {parsed=value;} + const updated=structuredClone(config) as Record; const parts=key.split('.'); let cursor=updated; + for(const part of parts.slice(0,-1)) {if (!Object.hasOwn(cursor,part) || !cursor[part] || typeof cursor[part]!=='object') throw new AgentCliError('CONFIG_INVALID','Unknown config key');cursor=cursor[part] as Record;} + if(['__proto__','constructor','prototype'].some(p=>parts.includes(p))) throw new AgentCliError('CONFIG_INVALID','Invalid key'); + cursor[parts.at(-1)!]=parsed; const validated=migrate(updated);await saveConfig(validated,context.configPath);return validated; +}}); diff --git a/src/core/config.ts b/src/core/config.ts new file mode 100644 index 0000000..ba85f00 --- /dev/null +++ b/src/core/config.ts @@ -0,0 +1,47 @@ +import { homedir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { mkdir, readFile, rename, writeFile, chmod } from 'node:fs/promises'; +import { randomUUID } from 'node:crypto'; +import { z } from 'zod'; +import { AgentCliError } from '../errors/index.js'; +export const stateDir = () => join(homedir(), '.bloxwap'); +const address=z.string().regex(/^0x[0-9a-fA-F]{40}$/); +const positive=z.number().positive().finite(); +export const configSchema=z.object({ + version:z.literal(1).default(1),network:z.enum(['mainnet','testnet']).default('testnet'), + masterAddress:address.optional(),agentAddress:address.optional(),agentExpiry:z.number().int().positive().optional(), + privyAppId:z.string().optional(),privyWalletId:z.string().optional(),policyId:z.string().optional(),policyVersion:z.number().int().optional(), + authorizationKeyRef:z.string().regex(/^env:[A-Z_][A-Z0-9_]*$|^file:.+/).optional(), + privyAppSecretRef:z.string().regex(/^env:[A-Z_][A-Z0-9_]*$/).optional(), + revoked:z.boolean().default(false),approved:z.boolean().default(false), + limits:z.object({maxNotional:positive.optional(),maxPositionNotional:positive.optional(),maxLeverage:positive.optional(),maxOrderSize:positive.optional(),dailyLoss:positive.optional(),maxOpenOrders:z.number().int().nonnegative().optional()}).default({}), + allowedMarkets:z.array(z.string()).default([]), + builder:z.object({address:address.optional(),fee:z.number().int().min(0).max(100).default(0)}).default({fee:0}), + logLevel:z.enum(['debug','info','warn','error']).default('info'), + runnerFallback:z.boolean().default(true),expiryWarningDays:z.array(positive).default([14,3,1]), +}).strict(); +export type Config=z.infer; +export const configPath=()=>join(stateDir(),'config.json'); +export async function atomicWrite(path: string,data: string) { + await mkdir(dirname(path),{recursive:true,mode:0o700}); await chmod(dirname(path),0o700); + const temporary=`${path}.${randomUUID()}.tmp`; + await writeFile(temporary,data,{mode:0o600,flag:'wx'}); await rename(temporary,path); await chmod(path,0o600); +} +export function migrate(raw: unknown): Config { + if (raw && typeof raw==='object' && !('version' in raw)) raw={...raw,version:1}; + const result=configSchema.safeParse(raw); + if (!result.success) throw new AgentCliError('CONFIG_INVALID','Invalid config',result.error.flatten()); + return result.data; +} +export async function loadConfig(path=configPath(),env: NodeJS.ProcessEnv=process.env): Promise { + let raw: unknown={}; + try {raw=JSON.parse(await readFile(path,'utf8'));} catch(error) { + if ((error as NodeJS.ErrnoException).code!=='ENOENT') throw new AgentCliError('CONFIG_INVALID','Cannot read config'); + } + const config=migrate(raw); + for (const [name,key] of Object.entries({BLOXWAP_NETWORK:'network',BLOXWAP_LOG_LEVEL:'logLevel',BLOXWAP_PRIVY_APP_ID:'privyAppId',BLOXWAP_MASTER_ADDRESS:'masterAddress'})) { + if (env[name]) Object.assign(config,{[key]:env[name]}); + } + return migrate(config); +} +export async function saveConfig(config: unknown,path=configPath()) {await atomicWrite(path,JSON.stringify(migrate(config),null,2)+'\n');} diff --git a/test/config.test.ts b/test/config.test.ts new file mode 100644 index 0000000..79a782a --- /dev/null +++ b/test/config.test.ts @@ -0,0 +1,13 @@ +import { mkdtemp, stat, writeFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { expect, test } from 'vitest'; +import { loadConfig, saveConfig } from '../src/core/config.js'; +test('atomic private store, migration and corrupt input',async()=>{ + const dir=await mkdtemp(join(tmpdir(),'bloxwap-')); const path=join(dir,'state','config.json'); + try { + await saveConfig({network:'testnet'},path);expect((await stat(path)).mode & 0o777).toBe(0o600);expect((await stat(join(dir,'state'))).mode & 0o777).toBe(0o700); + expect((await loadConfig(path,{BLOXWAP_NETWORK:'mainnet'})).network).toBe('mainnet'); + await writeFile(path,'broken');await expect(loadConfig(path,{})).rejects.toMatchObject({code:'CONFIG_INVALID'}); + } finally {await rm(dir,{recursive:true,force:true});} +}); From 8aa3d88e1493dc8f11b16c61f7eb8f9ccd8fbc25 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 12:30:56 -0700 Subject: [PATCH 06/43] feat: add redacted rotating structured logs (#6) --- docs/PROGRESS.md | 1 + src/core/logger.ts | 30 ++++++++++++++++++++++++++++++ test/logger.test.ts | 15 +++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 src/core/logger.ts create mode 100644 test/logger.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 8f68798..a85ec76 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -8,3 +8,4 @@ | #3 | done | Stable error catalog, serialization, exit codes and append-only guard. | | #4 | done | Validated JSON/flags/stdin router, JSON schemas, envelopes and exit codes. | | #5 | done | Versioned validated config, atomic private writes, migration, commands and env overrides. | +| #6 | done | JSON logging, recursive redaction, correlation IDs and size/day rotation. | diff --git a/src/core/logger.ts b/src/core/logger.ts new file mode 100644 index 0000000..a1316f5 --- /dev/null +++ b/src/core/logger.ts @@ -0,0 +1,30 @@ +import { randomUUID } from 'node:crypto'; +import { appendFile, mkdir, stat, rename, chmod } from 'node:fs/promises'; +import { join } from 'node:path'; +import { stateDir } from './config.js'; +export const levels={debug:10,info:20,warn:30,error:40}; +export type Level=keyof typeof levels; +export function redact(value: unknown): unknown { + if (typeof value==='string') return value.replace(/(?:0x)?[a-fA-F0-9]{64,}/g,'[REDACTED]').replace(/Bearer\s+\S+/gi,'Bearer [REDACTED]'); + if (Array.isArray(value)) return value.map(redact); + if (value && typeof value==='object') return Object.fromEntries(Object.entries(value).map(([key,v])=>[key,/secret|signature|token|authorization|private.?key|seed|mnemonic|password|app.?key/i.test(key) ? '[REDACTED]' : redact(v)])); + return value; +} +export class Logger { + private queue:Promise=Promise.resolve(); + private day=new Date().toISOString().slice(0,10); + readonly requestId: string; + constructor(private options:{level?:Level;directory?:string;maxBytes?:number;requestId?:string;intentId?:string;stderr?:(line:string)=>void}={}) {this.requestId=options.requestId??randomUUID();} + log(level:Level,message:string,fields:Record={}) { + if(levels[level]process.stderr.write(line)))(line); + this.queue=this.queue.then(async()=>{ + const dir=this.options.directory??join(stateDir(),'logs');await mkdir(dir,{recursive:true,mode:0o700});await chmod(dir,0o700); + const path=join(dir,'runner.jsonl');const day=new Date().toISOString().slice(0,10); + try {const info=await stat(path);if(info.size+Buffer.byteLength(line)>(this.options.maxBytes??5_000_000)||day!==this.day) await rename(path,join(dir,`runner.${Date.now()}.${randomUUID()}.jsonl`));} catch(error) {if((error as NodeJS.ErrnoException).code!=='ENOENT') throw error;} + this.day=day;await appendFile(path,line,{mode:0o600}); + }); + } + async flush(){await this.queue;} +} diff --git a/test/logger.test.ts b/test/logger.test.ts new file mode 100644 index 0000000..a21bc61 --- /dev/null +++ b/test/logger.test.ts @@ -0,0 +1,15 @@ +import { expect,test } from 'vitest'; +import { mkdtemp,readdir,readFile,rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { Logger } from '../src/core/logger.js'; +test('nested secrets are redacted and rotated lines retain correlation',async()=>{ + const dir=await mkdtemp(join(tmpdir(),'bloxwap-logs-'));const lines:string[]=[]; + try { + const logger=new Logger({directory:dir,maxBytes:100,requestId:'request',stderr:line=>lines.push(line)}); + logger.log('info','safe',{nested:{authorizationKey:'secret'},raw:'0x'+'ab'.repeat(32),token:'sensitive'});logger.log('warn','next');await logger.flush(); + const output=lines.join('');expect(output).not.toContain('secret');expect(output).not.toContain('sensitive');expect(output).not.toContain('ab'.repeat(32)); + expect(lines.every(line=>JSON.parse(line).requestId==='request')).toBe(true); + const files=await readdir(dir);expect(files.length).toBe(2);expect(await readFile(join(dir,files[0]),'utf8')).toContain('request'); + }finally{await rm(dir,{recursive:true,force:true});} +}); From 69f02855dabe1377aa8d641e908f1d6305b10ac7 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 12:32:38 -0700 Subject: [PATCH 07/43] feat: wrap Hyperliquid clients and market metadata (#7) --- docs/PROGRESS.md | 1 + package-lock.json | 8 ++++---- package.json | 2 +- src/core/hlclient.ts | 30 ++++++++++++++++++++++++++++++ test/hlclient.test.ts | 7 +++++++ 5 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 src/core/hlclient.ts create mode 100644 test/hlclient.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index a85ec76..e3aa85b 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -9,3 +9,4 @@ | #4 | done | Validated JSON/flags/stdin router, JSON schemas, envelopes and exit codes. | | #5 | done | Versioned validated config, atomic private writes, migration, commands and env overrides. | | #6 | done | JSON logging, recursive redaction, correlation IDs and size/day rotation. | +| #7 | done | Requested SDK adapter, lazy WS factory, cached metadata, decimal rounding and remote signer abstraction. SDK requires Node 22.12+; runtime smoke checked on Node 20. | diff --git a/package-lock.json b/package-lock.json index 6fdedb2..1512062 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "eslint": "^9.39.5", "prettier": "^3.9.10", "tsup": "^8.5.1", - "typescript": "^6.0.3", + "typescript": "^5.9.3", "typescript-eslint": "^8.71.2", "vitest": "^3.2.7" }, @@ -3293,9 +3293,9 @@ } }, "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index 290e94b..658cc03 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "eslint": "^9.39.5", "prettier": "^3.9.10", "tsup": "^8.5.1", - "typescript": "^6.0.3", + "typescript": "^5.9.3", "typescript-eslint": "^8.71.2", "vitest": "^3.2.7" }, diff --git a/src/core/hlclient.ts b/src/core/hlclient.ts new file mode 100644 index 0000000..3ffdc91 --- /dev/null +++ b/src/core/hlclient.ts @@ -0,0 +1,30 @@ +import { ExchangeClient, HttpTransport, InfoClient, SubscriptionClient, WebSocketTransport } from '@bloxwap/hyperliquid'; +import type { AbstractViemLocalAccount } from '@bloxwap/hyperliquid/signing'; +import { formatPrice, formatSize } from '@bloxwap/hyperliquid/utils'; +import { AgentCliError } from '../errors/index.js'; +export type Signer=AbstractViemLocalAccount; +export interface Market {name:string;szDecimals:number;maxLeverage:number;asset:number;} +export class MetaCache { + private markets=new Map(); private updated=0; + constructor(private fetchMeta:()=>Promise<{universe:{name:string;szDecimals:number;maxLeverage:number}[]}>,private ttl=60_000){} + async refresh(){const meta=await this.fetchMeta();this.markets=new Map(meta.universe.map((market,asset)=>[market.name,{...market,asset}]));this.updated=Date.now();} + async lookup(coin:string){if(Date.now()-this.updated>this.ttl) await this.refresh();const market=this.markets.get(coin);if(!market) throw new AgentCliError('UNKNOWN_MARKET',`Unknown market ${coin}`);return market;} +} +export function roundSize(size:string,szDecimals:number){ + if(!/^\d+(\.\d+)?$/.test(size)||Number(size)<=0) throw new AgentCliError('INVALID_SIZE','Size must be positive'); + const rounded=formatSize(size,szDecimals);if(Number(rounded)<=0) throw new AgentCliError('INVALID_SIZE','Size rounds to zero');return rounded; +} +export function roundPrice(price:string,szDecimals:number){ + if(!/^\d+(\.\d+)?$/.test(price)||Number(price)<=0) throw new AgentCliError('INVALID_PRICE','Price must be positive'); + const rounded=formatPrice(price,szDecimals);if(Number(rounded)<=0) throw new AgentCliError('INVALID_PRICE','Price rounds to zero');return rounded; +} +// Bound SDK generic depth at the adapter boundary; runtime clients remain unchanged. +const Exchange = ExchangeClient as unknown as new (options: {transport:HttpTransport;wallet:Signer;nonceManager?:()=>Promise}) => ExchangeAdapter; +export interface ExchangeAdapter { + order(params:Record):Promise; cancel(params:Record):Promise; cancelByCloid(params:Record):Promise; batchModify(params:Record):Promise; updateLeverage(params:Record):Promise; +} +export function createClients(network:'mainnet'|'testnet',signer?:Signer,nonce?:()=>Promise) { + const transport=new HttpTransport({isTestnet:network==='testnet'});const info=new InfoClient({transport}); + return {info,meta:new MetaCache((info as unknown as {meta:()=>Promise<{universe:{name:string;szDecimals:number;maxLeverage:number}[]}>}).meta.bind(info)),exchange:signer ? new Exchange({transport,wallet:signer,nonceManager:nonce}) : undefined, + subscriptions:()=>{const transport=new WebSocketTransport({isTestnet:network==='testnet'});return {transport,client:new SubscriptionClient({transport})};}}; +} diff --git a/test/hlclient.test.ts b/test/hlclient.test.ts new file mode 100644 index 0000000..7a104cd --- /dev/null +++ b/test/hlclient.test.ts @@ -0,0 +1,7 @@ +import { expect,test,vi } from 'vitest'; +import { MetaCache,roundPrice,roundSize,createClients } from '../src/core/hlclient.js'; +test('fixture metadata caches and refreshes',async()=>{ + const fixture={universe:[{name:'BTC',szDecimals:5,maxLeverage:40}]};const fetch=vi.fn(async()=>fixture);const cache=new MetaCache(fetch); + expect(await cache.lookup('BTC')).toEqual({...fixture.universe[0],asset:0});await cache.lookup('BTC');expect(fetch).toHaveBeenCalledTimes(1);await cache.refresh();expect(fetch).toHaveBeenCalledTimes(2);await expect(cache.lookup('NO')).rejects.toMatchObject({code:'UNKNOWN_MARKET'}); +}); +test('SDK rounds exact decimal fixtures',()=>{expect(roundSize('1.234567',5)).toBe('1.23456');expect(roundPrice('97123.456',5)).toBe('97123');expect(roundPrice('1.23456',3)).toBe('1.234');expect(()=>roundSize('0.000001',5)).toThrow();expect(createClients('testnet').exchange).toBeUndefined();}); From 56462160f11d14298ba14bc9a756a76646c44363 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 12:32:48 -0700 Subject: [PATCH 08/43] feat: map upstream service errors (#8) --- docs/PROGRESS.md | 1 + src/errors/mappers.ts | 14 ++++++++++++++ test/mappers.test.ts | 6 ++++++ 3 files changed, 21 insertions(+) create mode 100644 src/errors/mappers.ts create mode 100644 test/mappers.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index e3aa85b..ea15105 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -10,3 +10,4 @@ | #5 | done | Versioned validated config, atomic private writes, migration, commands and env overrides. | | #6 | done | JSON logging, recursive redaction, correlation IDs and size/day rotation. | | #7 | done | Requested SDK adapter, lazy WS factory, cached metadata, decimal rounding and remote signer abstraction. SDK requires Node 22.12+; runtime smoke checked on Node 20. | +| #8 | done | Service error adapters, redacted upstream detail, fixture-driven classification and retry defaults. | diff --git a/src/errors/mappers.ts b/src/errors/mappers.ts new file mode 100644 index 0000000..a216bfc --- /dev/null +++ b/src/errors/mappers.ts @@ -0,0 +1,14 @@ +import { AgentCliError, type ErrorCode } from './index.js'; +import { redact } from '../core/logger.js'; +function describe(error:unknown):string {if(error instanceof Error) return error.message;if(typeof error==='string') return error;return JSON.stringify(error)??'Unknown upstream error';} +function map(error:unknown,service:string):AgentCliError { + if(error instanceof AgentCliError) return error; + const message=String(redact(describe(error)));let code:ErrorCode='UPSTREAM_ERROR'; + const patterns:[RegExp,ErrorCode][]=[[/429|rate.?limit|too many requests/i,'RATE_LIMIT'],[/nonce|duplicate.*timestamp/i,'NONCE_COLLISION'],[/insufficient.*margin|margin.*insufficient/i,'INSUFFICIENT_MARGIN'],[/minimum.*(size|value)|min.*notional|invalid.*size|too small/i,'INVALID_SIZE'],[/tick|invalid.*price/i,'INVALID_PRICE'],[/policy|not allowed|denied/i,'POLICY_REJECTED'],[/agent.*expired/i,'AGENT_EXPIRED'],[/agent.*not.*approved|not.*authorized/i,'AGENT_NOT_APPROVED'],[/order.*not.*found|does not exist/i,'ORDER_NOT_FOUND']]; + for(const [pattern,candidate] of patterns) if(pattern.test(message)){code=candidate;break;} + return new AgentCliError(code,message,redact({service,upstream:error instanceof Error?{message:error.message}:error})); +} +export const mapHyperliquidError=(error:unknown)=>map(error,'hyperliquid'); +export const mapPrivyError=(error:unknown)=>map(error,'privy'); +export const mapRelayError=(error:unknown)=>map(error,'relay'); +export const mapNetworkError=(error:unknown)=>new AgentCliError('NETWORK_ERROR',String(redact(describe(error))),redact({service:'network',upstream:describe(error)})); diff --git a/test/mappers.test.ts b/test/mappers.test.ts new file mode 100644 index 0000000..3ec3b92 --- /dev/null +++ b/test/mappers.test.ts @@ -0,0 +1,6 @@ +import {expect,test} from 'vitest'; +import {mapHyperliquidError,mapPrivyError,mapRelayError,mapNetworkError} from '../src/errors/mappers.js'; +test.each([['Insufficient margin','INSUFFICIENT_MARGIN',false],['Order must have minimum value of $10','INVALID_SIZE',false],['Invalid price tick','INVALID_PRICE',false],['HTTP 429','RATE_LIMIT',true],['Nonce already used','NONCE_COLLISION',true]])('maps fixture %s',(message,code,retryable)=>{expect(mapHyperliquidError(message).toJSON()).toMatchObject({code,retryable});}); +test('policy, relay unknown and network flags redact details',()=>{ + expect(mapPrivyError('Policy denied').code).toBe('POLICY_REJECTED');expect(mapRelayError({message:'unexpected',token:'secret'}).toJSON()).toMatchObject({code:'UPSTREAM_ERROR',details:{upstream:{token:'[REDACTED]'}}});expect(mapNetworkError(new Error('fetch failed')).retryable).toBe(true); +}); From 8a1f57a2030bb4b8ef26860c6950fe76e06056f8 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 12:33:14 -0700 Subject: [PATCH 09/43] feat: add bounded retries and persistent nonce locking (#9) --- docs/PROGRESS.md | 1 + src/core/nonce.ts | 22 ++++++++++++++++++++++ src/core/retry.ts | 13 +++++++++++++ test/retry.test.ts | 16 ++++++++++++++++ 4 files changed, 52 insertions(+) create mode 100644 src/core/nonce.ts create mode 100644 src/core/retry.ts create mode 100644 test/retry.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index ea15105..2c4bb19 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -11,3 +11,4 @@ | #6 | done | JSON logging, recursive redaction, correlation IDs and size/day rotation. | | #7 | done | Requested SDK adapter, lazy WS factory, cached metadata, decimal rounding and remote signer abstraction. SDK requires Node 22.12+; runtime smoke checked on Node 20. | | #8 | done | Service error adapters, redacted upstream detail, fixture-driven classification and retry defaults. | +| #9 | done | Bounded jittered retries and per-signer persisted monotonic nonces guarded by exclusive locks. Stale locks fail closed. | diff --git a/src/core/nonce.ts b/src/core/nonce.ts new file mode 100644 index 0000000..acc326d --- /dev/null +++ b/src/core/nonce.ts @@ -0,0 +1,22 @@ +import { mkdir,readFile,open,unlink } from 'node:fs/promises'; +import { dirname,join } from 'node:path'; +import { atomicWrite,stateDir } from './config.js'; +import { AgentCliError } from '../errors/index.js'; +export async function nextNonce(signer:string,directory=join(stateDir(),'nonces'),now=Date.now):Promise{ + if(!/^0x[0-9a-fA-F]{40}$/.test(signer)) throw new AgentCliError('CONFIG_INVALID','Invalid nonce signer'); + const path=join(directory,`${signer.toLowerCase()}.json`);const lock=`${path}.lock`;await mkdir(dirname(path),{recursive:true,mode:0o700}); + for(let attempt=0;attempt<100;attempt++) { + let handle; + try{handle=await open(lock,'wx',0o600);}catch(error){ + if((error as NodeJS.ErrnoException).code!=='EEXIST') throw error; + await new Promise(resolve=>setTimeout(resolve,10));continue; + } + try{ + await handle.writeFile(String(process.pid));let previous=0; + try {previous=JSON.parse(await readFile(path,'utf8')) as number;}catch(error){if((error as NodeJS.ErrnoException).code!=='ENOENT') throw new AgentCliError('CONFIG_INVALID','Corrupt nonce state');} + if(!Number.isSafeInteger(previous)||previous<0) throw new AgentCliError('CONFIG_INVALID','Invalid nonce state'); + const nonce=Math.max(now(),previous+1);await atomicWrite(path,JSON.stringify(nonce));return nonce; + } finally{await handle.close();await unlink(lock);} + } + throw new AgentCliError('NONCE_COLLISION','Nonce lock busy; inspect stale lock before retrying'); +} diff --git a/src/core/retry.ts b/src/core/retry.ts new file mode 100644 index 0000000..a3bff84 --- /dev/null +++ b/src/core/retry.ts @@ -0,0 +1,13 @@ +import { normalizeError } from '../errors/index.js'; +export async function retry(operation:()=>Promise,options:{attempts?:number;baseMs?:number;maxMs?:number;random?:()=>number;sleep?:(ms:number)=>Promise}={}) { + const attempts=options.attempts??3; + if(!Number.isInteger(attempts)||attempts<1) throw new RangeError('attempts must be positive'); + for(let attempt=0;;attempt++) { + try{return await operation();}catch(error){ + const normalized=normalizeError(error);if(!normalized.retryable||attempt+1>=attempts) throw normalized; + const base=Math.min(options.maxMs??30_000,(options.baseMs??250)*2**attempt); + const jitter=0.5+(options.random??Math.random)(); + await (options.sleep??(ms=>new Promise(resolve=>setTimeout(resolve,ms))))(Math.min(options.maxMs??30_000,base*jitter)); + } + } +} diff --git a/test/retry.test.ts b/test/retry.test.ts new file mode 100644 index 0000000..aab8bbe --- /dev/null +++ b/test/retry.test.ts @@ -0,0 +1,16 @@ +import {expect,test} from 'vitest'; +import {mkdtemp,rm} from 'node:fs/promises'; +import {tmpdir} from 'node:os'; +import {join} from 'node:path'; +import {retry} from '../src/core/retry.js'; +import {nextNonce} from '../src/core/nonce.js'; +import {AgentCliError} from '../src/errors/index.js'; +test('bounded backoff only retries temporary errors',async()=>{ + let attempts=0;const sleeps:number[]=[]; + expect(await retry(async()=>{if(++attempts<3) throw new AgentCliError('RATE_LIMIT','wait');return 7;},{baseMs:100,random:()=>0.5,sleep:async ms=>{sleeps.push(ms);}})).toBe(7);expect(sleeps).toEqual([100,200]); + await expect(retry(async()=>{throw new AgentCliError('POLICY_REJECTED','stop');})).rejects.toMatchObject({retryable:false}); +}); +test('persisted nonces serialize concurrent callers and restarts',async()=>{ + const dir=await mkdtemp(join(tmpdir(),'bloxwap-nonce-'));const signer='0x'+'a'.repeat(40); + try{const values=await Promise.all(Array.from({length:8},()=>nextNonce(signer,dir,()=>1000)));expect(new Set(values).size).toBe(8);expect(await nextNonce(signer,dir,()=>500)).toBe(1008);}finally{await rm(dir,{recursive:true,force:true});} +}); From 71312e394095d25ca8e4a9712783c7542499aa80 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 12:38:24 -0700 Subject: [PATCH 10/43] feat: integrate Privy managed agent signing (#10) --- docs/PROGRESS.md | 1 + src/wallet/privy.ts | 35 +++++++++++++++++++++++++++++++++++ test/privy.test.ts | 14 ++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 src/wallet/privy.ts create mode 100644 test/privy.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 2c4bb19..da73953 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -12,3 +12,4 @@ | #7 | done | Requested SDK adapter, lazy WS factory, cached metadata, decimal rounding and remote signer abstraction. SDK requires Node 22.12+; runtime smoke checked on Node 20. | | #8 | done | Service error adapters, redacted upstream detail, fixture-driven classification and retry defaults. | | #9 | done | Bounded jittered retries and per-signer persisted monotonic nonces guarded by exclusive locks. Stale locks fail closed. | +| #10 | done | Privy server SDK create/link, P-256 owner, private authorization storage and typed-data signer; sandbox test env-gated. | diff --git a/src/wallet/privy.ts b/src/wallet/privy.ts new file mode 100644 index 0000000..e2a8347 --- /dev/null +++ b/src/wallet/privy.ts @@ -0,0 +1,35 @@ +import { PrivyClient, generateP256KeyPair } from '@privy-io/node'; +import { readFile,stat } from 'node:fs/promises'; +import { join } from 'node:path'; +import { atomicWrite,saveConfig,stateDir,type Config } from '../core/config.js'; +import type { Signer } from '../core/hlclient.js'; +import { AgentCliError } from '../errors/index.js'; +import { mapPrivyError } from '../errors/mappers.js'; +export async function resolveSecret(ref:string|undefined,env=process.env):Promise{ + if(!ref) throw new AgentCliError('CONFIG_INVALID','Missing secret reference'); + if(ref.startsWith('env:')) {const value=env[ref.slice(4)];if(value) return value;} + if(ref.startsWith('file:')) {const path=ref.slice(5);const info=await stat(path);if(process.platform!=='win32'&&(info.mode&0o077)!==0) throw new AgentCliError('CONFIG_INVALID','Secret file must have mode 0600');return (await readFile(path,'utf8')).trim();} + throw new AgentCliError('CONFIG_INVALID','Secret unavailable'); +} +export async function privyClient(config:Config){ + if(!config.privyAppId) throw new AgentCliError('CONFIG_INVALID','Set privyAppId'); + return new PrivyClient({appId:config.privyAppId,appSecret:await resolveSecret(config.privyAppSecretRef??'env:BLOXWAP_PRIVY_APP_SECRET')}); +} +export async function createAgent(config:Config,client:PrivyClient,path?:string,policyId?:string) { + try { + if(config.privyWalletId){const wallet=await client.wallets().get(config.privyWalletId);config.agentAddress=wallet.address;await saveConfig(config,path);return wallet;} + const pair=await generateP256KeyPair();const keyPath=join(stateDir(),`authorization-${Date.now()}.key`);await atomicWrite(keyPath,pair.privateKey); + const wallet=await client.wallets().create({chain_type:'ethereum',owner:{public_key:pair.publicKey},policy_ids:policyId?[policyId]:[],idempotency_key:`agent-${pair.publicKey}`}); + config.privyWalletId=wallet.id;config.agentAddress=wallet.address;config.authorizationKeyRef=`file:${keyPath}`;await saveConfig(config,path);return wallet; + }catch(error){throw mapPrivyError(error);} +} +export async function privySigner(config:Config,client:PrivyClient):Promise{ + if(!config.agentAddress||!config.privyWalletId) throw new AgentCliError('AGENT_NOT_APPROVED','Initialize agent wallet first'); + const authorizationKey=await resolveSecret(config.authorizationKeyRef); + return {address:config.agentAddress as `0x${string}`,signTypedData:async data=>{ + try { + const signed=await client.wallets().ethereum().signTypedData(config.privyWalletId!,{params:{typed_data:{domain:data.domain,types:Object.fromEntries(Object.entries(data.types).map(([key,fields])=>[key,[...fields]])),primary_type:data.primaryType,message:data.message}},authorization_context:{authorization_private_keys:[authorizationKey]}}); + return signed.signature as `0x${string}`; + }catch(error){throw mapPrivyError(error);} + }}; +} diff --git a/test/privy.test.ts b/test/privy.test.ts new file mode 100644 index 0000000..48209f5 --- /dev/null +++ b/test/privy.test.ts @@ -0,0 +1,14 @@ +import {expect,test,vi} from 'vitest'; +import type {PrivyClient} from '@privy-io/node'; +import {privySigner,resolveSecret,privyClient} from '../src/wallet/privy.js'; +import {migrate} from '../src/core/config.js'; +test('remote typed-data signing preserves EIP-712 and maps denial',async()=>{ + const signTypedData=vi.fn().mockResolvedValue({signature:'0x1234'});const client={wallets:()=>({ethereum:()=>({signTypedData})})} as unknown as PrivyClient; + process.env.BLOXWAP_TEST_AUTH='test-key'; + try{const signer=await privySigner(migrate({agentAddress:'0x'+'a'.repeat(40),privyWalletId:'wallet',authorizationKeyRef:'env:BLOXWAP_TEST_AUTH'}),client); + const data={domain:{name:'Exchange',version:'1',chainId:1337,verifyingContract:'0x'+'0'.repeat(40) as `0x${string}`},types:{Agent:[{name:'source',type:'string'}]},primaryType:'Agent',message:{source:'b'}}; + expect(await signer.signTypedData(data)).toBe('0x1234');expect(signTypedData.mock.calls[0][1].params.typed_data.primary_type).toBe('Agent');signTypedData.mockRejectedValue(new Error('Policy denied'));await expect(signer.signTypedData(data)).rejects.toMatchObject({code:'POLICY_REJECTED'}); + }finally{delete process.env.BLOXWAP_TEST_AUTH;} + expect(await resolveSecret('env:BLOXWAP_SECRET',{BLOXWAP_SECRET:'ok'})).toBe('ok'); +}); +test.skipIf(!process.env.BLOXWAP_PRIVY_SANDBOX)('Privy sandbox credentials',async()=>{const client=await privyClient(migrate({privyAppId:process.env.BLOXWAP_PRIVY_APP_ID}));expect(await client.wallets().list()).toBeDefined();}); From 5f53c05d3ac163a36aead89e63d6679554b447d9 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 12:43:53 -0700 Subject: [PATCH 11/43] feat: restrict agent custody policy and document boundary (#11) --- docs/PROGRESS.md | 1 + docs/custody.md | 16 ++++++++++++++++ policies/agent-trade-only.json | 8 ++++++++ src/wallet/policy.ts | 19 +++++++++++++++++++ test/policy.test.ts | 6 ++++++ 5 files changed, 50 insertions(+) create mode 100644 docs/custody.md create mode 100644 policies/agent-trade-only.json create mode 100644 src/wallet/policy.ts create mode 100644 test/policy.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index da73953..f2febab 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -13,3 +13,4 @@ | #8 | done | Service error adapters, redacted upstream detail, fixture-driven classification and retry defaults. | | #9 | done | Bounded jittered retries and per-signer persisted monotonic nonces guarded by exclusive locks. Stale locks fail closed. | | #10 | done | Privy server SDK create/link, P-256 owner, private authorization storage and typed-data signer; sandbox test env-gated. | +| #11 | partial | Versioned domain/network Privy policy and local trade allowlist; L1 action hash prevents Privy per-action/notional inspection. Exchange agent permissions enforce withdrawal boundary. | diff --git a/docs/custody.md b/docs/custody.md new file mode 100644 index 0000000..ae8e6fa --- /dev/null +++ b/docs/custody.md @@ -0,0 +1,16 @@ +# Custody boundary + +The master wallet approves a separate Hyperliquid API agent. Its key stays in Privy; +the CLI holds a P-256 authorization key, never the master private key. Hyperliquid +agent permissions prevent withdrawals and master-only user-signed actions. + +The versioned Privy policy permits only EIP-712 Agent messages in the Hyperliquid +L1 domain (chain 1337, zero verifying contract, correct network source). Unmatched +RPC methods are denied by default. User-signed withdrawal and approval messages +have a different domain/type. Local dispatch allows only trading operations. + +Privy sees the L1 action hash, not its preimage: it cannot independently distinguish +an order from another L1 action or enforce notional limits inside that hash. +Local limits and exchange agent permissions supply those checks. Do not describe +this policy as independent server-side per-action/notional enforcement. Confirm +policy behavior in the credential-gated sandbox before production use. diff --git a/policies/agent-trade-only.json b/policies/agent-trade-only.json new file mode 100644 index 0000000..353d662 --- /dev/null +++ b/policies/agent-trade-only.json @@ -0,0 +1,8 @@ +{ + "version":"1.0","name":"agent-cli trade-only v1","chain_type":"ethereum", + "rules":[{"name":"Hyperliquid L1 agent domain","method":"eth_signTypedData_v4","action":"ALLOW","conditions":[ + {"field_source":"ethereum_typed_data_domain","field":"chainId","operator":"eq","value":"1337"}, + {"field_source":"ethereum_typed_data_domain","field":"verifyingContract","operator":"eq","value":"0x0000000000000000000000000000000000000000"}, + {"field_source":"ethereum_typed_data_message","field":"source","operator":"eq","value":"b","typed_data":{"primary_type":"Agent","types":{"Agent":[{"name":"source","type":"string"},{"name":"connectionId","type":"bytes32"}]}}} + ]}] +} diff --git a/src/wallet/policy.ts b/src/wallet/policy.ts new file mode 100644 index 0000000..631c06f --- /dev/null +++ b/src/wallet/policy.ts @@ -0,0 +1,19 @@ +import type { PrivyClient } from '@privy-io/node'; +import template from '../../policies/agent-trade-only.json' with { type: 'json' }; +import { AgentCliError } from '../errors/index.js'; +import { mapPrivyError } from '../errors/mappers.js'; +import { saveConfig,type Config } from '../core/config.js'; +export const policyVersion=1; +const allowed=new Set(['order','cancel','cancelByCloid','batchModify','modify','updateLeverage','scheduleCancel','noop']); +export function assertTradeAction(action:string){if(!allowed.has(action)) throw new AgentCliError('POLICY_REJECTED',`Agent cannot sign ${action}`);} +export function policyFor(network:Config['network']) { + const policy=structuredClone(template);policy.rules[0].conditions[2].value=network==='mainnet'?'a':'b';return policy; +} +export async function applyPolicy(config:Config,client:PrivyClient,path?:string){ + if(config.policyId) return config.policyId; + try{ + const policy=await client.policies().create(policyFor(config.network) as Parameters['create']>[0]); + if(config.privyWalletId) await client.wallets().update(config.privyWalletId,{policy_ids:[policy.id]}); + config.policyId=policy.id;config.policyVersion=policyVersion;await saveConfig(config,path);return policy.id; + }catch(error){throw mapPrivyError(error);} +} diff --git a/test/policy.test.ts b/test/policy.test.ts new file mode 100644 index 0000000..58ca2f8 --- /dev/null +++ b/test/policy.test.ts @@ -0,0 +1,6 @@ +import {expect,test} from 'vitest'; +import {assertTradeAction,policyFor} from '../src/wallet/policy.js'; +test('trade-only action whitelist rejects withdrawal and approval',()=>{ + for(const action of ['withdraw','withdraw3','usdSend','spotSend','approveAgent','approveBuilderFee','usdClassTransfer']) expect(()=>assertTradeAction(action)).toThrow(expect.objectContaining({code:'POLICY_REJECTED'})); + assertTradeAction('order');expect(policyFor('mainnet').rules[0].conditions[2].value).toBe('a'); +}); From 99cfc711e96e2169530ab6efa9f0d0459308bf55 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 12:44:43 -0700 Subject: [PATCH 12/43] feat: implement resumable master-wallet onboarding (#12) --- docs/PROGRESS.md | 1 + src/cli.ts | 1 + src/commands/init.ts | 28 ++++++++++++++++++++++++++++ src/core/http.ts | 7 +++++++ src/wallet/approval.ts | 22 ++++++++++++++++++++++ test/approval.test.ts | 7 +++++++ 6 files changed, 66 insertions(+) create mode 100644 src/commands/init.ts create mode 100644 src/core/http.ts create mode 100644 src/wallet/approval.ts create mode 100644 test/approval.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index f2febab..b46c93e 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -14,3 +14,4 @@ | #9 | done | Bounded jittered retries and per-signer persisted monotonic nonces guarded by exclusive locks. Stale locks fail closed. | | #10 | done | Privy server SDK create/link, P-256 owner, private authorization storage and typed-data signer; sandbox test env-gated. | | #11 | partial | Versioned domain/network Privy policy and local trade allowlist; L1 action hash prevents Privy per-action/notional inspection. Exchange agent permissions enforce withdrawal boundary. | +| #12 | done | Idempotent managed-wallet onboarding, persisted external master EIP-712 approvals/deeplink, expiry bound, agent/builder verification and master-key rejection. | diff --git a/src/cli.ts b/src/cli.ts index d3d6ae2..3d6b493 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,6 +1,7 @@ #!/usr/bin/env node import { dispatch } from './core/router.js'; import './commands/config.js'; +import './commands/init.js'; let stdin=''; if (!process.stdin.isTTY) for await (const chunk of process.stdin) stdin += String(chunk); const result=await dispatch(process.argv.slice(2),stdin); diff --git a/src/commands/init.ts b/src/commands/init.ts new file mode 100644 index 0000000..48bae89 --- /dev/null +++ b/src/commands/init.ts @@ -0,0 +1,28 @@ +import {z} from 'zod'; +import {readFile} from 'node:fs/promises'; +import {join} from 'node:path'; +import {register} from '../core/router.js'; +import {loadConfig,saveConfig,atomicWrite,stateDir} from '../core/config.js'; +import {privyClient,createAgent} from '../wallet/privy.js'; +import {applyPolicy} from '../wallet/policy.js'; +import {approval,submitApproval,rejectMasterKey,type Approval} from '../wallet/approval.js'; +import {hlInfo} from '../core/http.js'; +import {AgentCliError} from '../errors/index.js'; +register('init',{description:'Create agent and emit resumable master-wallet approvals',schema:z.object({masterAddress:z.string().regex(/^0x[0-9a-fA-F]{40}$/).optional(),validUntil:z.number().int().optional(),signature:z.string().optional(),builderSignature:z.string().optional(),qr:z.boolean().default(true)}),execute:async(input,context)=>{ + rejectMasterKey(input);const config=await loadConfig(context.configPath);if(context.network) config.network=context.network; + if(config.approved&&!config.revoked) return config; + config.masterAddress=input.masterAddress as string??config.masterAddress;if(!config.masterAddress) throw new AgentCliError('CONFIG_INVALID','Provide masterAddress'); + const expiry=input.validUntil as number??config.agentExpiry??Date.now()+179*86400000; + if(expiry<=Date.now()||expiry>Date.now()+180*86400000) throw new AgentCliError('CONFIG_INVALID','Expiry must be within 180 days');config.agentExpiry=expiry; + const client=await privyClient(config);const policyId=await applyPolicy(config,client,context.configPath);await createAgent(config,client,context.configPath,policyId); + const path=join(stateDir(),`approval-${config.agentAddress}.json`);let pending:{agent:Approval;builder?:Approval}; + try{pending=JSON.parse(await readFile(path,'utf8'));}catch(error){if((error as NodeJS.ErrnoException).code!=='ENOENT') throw error;pending={agent:approval(config.network,'approveAgent',{agentAddress:config.agentAddress,agentName:`bloxwap valid_until ${expiry}`})};if(config.builder.address) pending.builder=approval(config.network,'approveBuilderFee',{builder:config.builder.address,maxFeeRate:`${config.builder.fee/1000}%`},Date.now()+1);await atomicWrite(path,JSON.stringify(pending));} + if(input.signature) await submitApproval(config.network,pending.agent,input.signature as string); + if(input.builderSignature&&pending.builder) await submitApproval(config.network,pending.builder,input.builderSignature as string); + const agents=await hlInfo<{address:string;validUntil:number}[]>(config.network,'extraAgents',{user:config.masterAddress}); + config.approved=agents.some(agent=>agent.address.toLowerCase()===config.agentAddress?.toLowerCase()&&agent.validUntil>=expiry); + if(config.builder.address){const max=await hlInfo(config.network,'maxBuilderFee',{user:config.masterAddress,builder:config.builder.address});config.approved=config.approved&&max>=config.builder.fee;} + config.revoked=false;await saveConfig(config,context.configPath); + const deeplink=`ethereum:${config.masterAddress}?request=${encodeURIComponent(JSON.stringify(pending.agent.typedData))}`; + return {masterAddress:config.masterAddress,agentAddress:config.agentAddress,expiry,policyId,approved:config.approved,requiresSignature:!config.approved,approvals:pending,deeplink}; +}}); diff --git a/src/core/http.ts b/src/core/http.ts new file mode 100644 index 0000000..6c54a49 --- /dev/null +++ b/src/core/http.ts @@ -0,0 +1,7 @@ +import {mapNetworkError} from '../errors/mappers.js'; +export async function jsonRequest(url:string,body?:unknown,fetcher:typeof fetch=fetch){ + let response:Response;try{response=await fetcher(url,{method:body===undefined?'GET':'POST',headers:{'content-type':'application/json'},body:body===undefined?undefined:JSON.stringify(body),signal:AbortSignal.timeout(15_000)});}catch(error){throw mapNetworkError(error);} + const data:unknown=await response.json();if(!response.ok) throw {status:response.status,...(data&&typeof data==='object'?data:{message:String(data)})};return data; +} +export const hlEndpoint=(network:'mainnet'|'testnet')=>`https://api${network==='testnet'?'.hyperliquid-testnet':'.hyperliquid'}.xyz`; +export async function hlInfo(network:'mainnet'|'testnet',type:string,params:Record={}):Promise{return await jsonRequest(`${hlEndpoint(network)}/info`,{type,...params}) as T;} diff --git a/src/wallet/approval.ts b/src/wallet/approval.ts new file mode 100644 index 0000000..2a2f44f --- /dev/null +++ b/src/wallet/approval.ts @@ -0,0 +1,22 @@ +import {ApproveAgentTypes} from '@bloxwap/hyperliquid/api/exchange/approveAgent'; +import {ApproveBuilderFeeTypes} from '@bloxwap/hyperliquid/api/exchange/approveBuilderFee'; +import {AgentCliError} from '../errors/index.js'; +import {jsonRequest,hlEndpoint} from '../core/http.js'; +export interface Approval {action:Record;typedData:Record;} +export function approval(network:'mainnet'|'testnet',type:'approveAgent'|'approveBuilderFee',fields:Record,nonce=Date.now()):Approval{ + const types=type==='approveAgent'?ApproveAgentTypes:ApproveBuilderFeeTypes; + const action={type,signatureChainId:'0xa4b1',hyperliquidChain:network==='mainnet'?'Mainnet':'Testnet',...fields,nonce}; + const {type:_type,signatureChainId:_chain,...message}=action;void _type;void _chain; + return {action,typedData:{domain:{name:'HyperliquidSignTransaction',version:'1',chainId:42161,verifyingContract:'0x'+'0'.repeat(40)},types,primaryType:Object.keys(types)[0],message}}; +} +export async function submitApproval(network:'mainnet'|'testnet',pending:Approval,signature:string){ + if(!/^0x[0-9a-fA-F]{130}$/.test(signature)) throw new AgentCliError('CONFIG_INVALID','Expected 65-byte signature'); + const recovery=parseInt(signature.slice(-2),16);const v=recovery<27?recovery+27:recovery;if(v!==27&&v!==28) throw new AgentCliError('CONFIG_INVALID','Invalid signature recovery'); + const response=await jsonRequest(`${hlEndpoint(network)}/exchange`,{action:pending.action,nonce:pending.action.nonce,signature:{r:signature.slice(0,66),s:'0x'+signature.slice(66,130),v}}) as {status:string;response:unknown}; + if(response.status!=='ok') throw new AgentCliError('UPSTREAM_ERROR','Approval failed');return response; +} +export function rejectMasterKey(value:unknown){ + if(typeof value==='string'&&/^(?:0x)?[a-fA-F0-9]{64}$/.test(value.trim())) throw new AgentCliError('POLICY_REJECTED','Never supply a master private key'); + if(Array.isArray(value)) value.forEach(rejectMasterKey); + else if(value&&typeof value==='object') for(const [key,field] of Object.entries(value)){if(/private.?key|mnemonic|seedPhrase/i.test(key)) throw new AgentCliError('POLICY_REJECTED','Master keys are forbidden');rejectMasterKey(field);} +} diff --git a/test/approval.test.ts b/test/approval.test.ts new file mode 100644 index 0000000..2806118 --- /dev/null +++ b/test/approval.test.ts @@ -0,0 +1,7 @@ +import {expect,test} from 'vitest'; +import {approval,rejectMasterKey} from '../src/wallet/approval.js'; +test('user signature payload follows SDK types and master secrets are refused',()=>{ + const result=approval('testnet','approveAgent',{agentAddress:'0x'+'a'.repeat(40),agentName:'agent'},123); + expect(result.typedData).toMatchObject({primaryType:'HyperliquidTransaction:ApproveAgent',message:{nonce:123,hyperliquidChain:'Testnet'}}); + expect(()=>rejectMasterKey({key:'0x'+'ab'.repeat(32)})).toThrow();expect(()=>rejectMasterKey({privateKey:'anything'})).toThrow();rejectMasterKey({signature:'0x'+'ab'.repeat(65)}); +}); From 6914bb6ebdef255f36dfd481c5604f0ccb1fe3a9 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 12:45:01 -0700 Subject: [PATCH 13/43] feat: validate builder fee approvals (#13) --- docs/PROGRESS.md | 1 + docs/config.md | 6 ++++++ src/core/builder.ts | 11 +++++++++++ test/builder.test.ts | 7 +++++++ 4 files changed, 25 insertions(+) create mode 100644 src/core/builder.ts create mode 100644 test/builder.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index b46c93e..860ae64 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -15,3 +15,4 @@ | #10 | done | Privy server SDK create/link, P-256 owner, private authorization storage and typed-data signer; sandbox test env-gated. | | #11 | partial | Versioned domain/network Privy policy and local trade allowlist; L1 action hash prevents Privy per-action/notional inspection. Exchange agent permissions enforce withdrawal boundary. | | #12 | done | Idempotent managed-wallet onboarding, persisted external master EIP-712 approvals/deeplink, expiry bound, agent/builder verification and master-key rejection. | +| #13 | partial | Builder attachment helper checks approved maximum; fee reporting. No official builder address/rate supplied in specs, so default is zero fee until configured. | diff --git a/docs/config.md b/docs/config.md index 1d76805..e75bb74 100644 --- a/docs/config.md +++ b/docs/config.md @@ -9,3 +9,9 @@ Unknown versions fail closed. Writes use a private temporary file and atomic ren persisted values; `config validate` validates them without overrides. Secrets use `env:BLOXWAP_PRIVY_APP_SECRET` and `env:BLOXWAP_AUTHORIZATION_KEY` references; authorization keys may also use `file:/absolute/private/path`. + +Builder fees use tenths of a basis point (`fee:10` means 0.01%). Set +`builder.address` and `builder.fee`; each order verifies the master approved maximum. +The specs do not supply a Bloxwap builder address/fee schedule, so defaults disable +fees (`fee:0`, no address) instead of inventing a fee recipient. Deployments must +configure the official recipient before enabling builder fees. diff --git a/src/core/builder.ts b/src/core/builder.ts new file mode 100644 index 0000000..eb3c624 --- /dev/null +++ b/src/core/builder.ts @@ -0,0 +1,11 @@ +import type {Config} from './config.js'; +import {hlInfo} from './http.js'; +import {AgentCliError} from '../errors/index.js'; +export async function approvedBuilder(config:Config,fetchMaximum=(user:string,builder:string)=>hlInfo(config.network,'maxBuilderFee',{user,builder})){ + if(!config.builder.address){if(config.builder.fee) throw new AgentCliError('CONFIG_INVALID','Builder address is required for a fee');return undefined;} + if(!config.masterAddress) throw new AgentCliError('AGENT_NOT_APPROVED','Master address missing'); + const maximum=await fetchMaximum(config.masterAddress,config.builder.address); + if(config.builder.fee>maximum) throw new AgentCliError('AGENT_NOT_APPROVED','Approve configured builder fee with master wallet',{approvedMax:maximum,requested:config.builder.fee}); + return {b:config.builder.address.toLowerCase(),f:config.builder.fee}; +} +export const builderRate=(config:Config)=>`${config.builder.fee/1000}%`; diff --git a/test/builder.test.ts b/test/builder.test.ts new file mode 100644 index 0000000..decd6de --- /dev/null +++ b/test/builder.test.ts @@ -0,0 +1,7 @@ +import {expect,test} from 'vitest'; +import {approvedBuilder} from '../src/core/builder.js'; +import {migrate} from '../src/core/config.js'; +test('builder fee cannot exceed master approval',async()=>{ + const config=migrate({masterAddress:'0x'+'a'.repeat(40),builder:{address:'0x'+'b'.repeat(40),fee:10}}); + expect(await approvedBuilder(config,async()=>10)).toEqual({b:config.builder.address,f:10});await expect(approvedBuilder(config,async()=>9)).rejects.toMatchObject({code:'AGENT_NOT_APPROVED'}); +}); From c09530a70d48219ac87822da563c4a161dc47983 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:01:00 -0700 Subject: [PATCH 14/43] feat: add Relay HyperCore deposit quote client (#14) --- docs/PROGRESS.md | 1 + src/funding/relay.ts | 17 +++++++++++++++++ test/relay.test.ts | 8 ++++++++ 3 files changed, 26 insertions(+) create mode 100644 src/funding/relay.ts create mode 100644 test/relay.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 860ae64..cb00112 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -16,3 +16,4 @@ | #11 | partial | Versioned domain/network Privy policy and local trade allowlist; L1 action hash prevents Privy per-action/notional inspection. Exchange agent permissions enforce withdrawal boundary. | | #12 | done | Idempotent managed-wallet onboarding, persisted external master EIP-712 approvals/deeplink, expiry bound, agent/builder verification and master-key rejection. | | #13 | partial | Builder attachment helper checks approved maximum; fee reporting. No official builder address/rate supplied in specs, so default is zero fee until configured. | +| #14 | done | Typed validated Relay quote/status adapter, perps USDC destination, multiple origin routes and fee parsing; mocked fixtures. | diff --git a/src/funding/relay.ts b/src/funding/relay.ts new file mode 100644 index 0000000..0862d7c --- /dev/null +++ b/src/funding/relay.ts @@ -0,0 +1,17 @@ +import {z} from 'zod'; +import {jsonRequest} from '../core/http.js'; +import {mapRelayError} from '../errors/mappers.js'; +export const PERPS_USDC='0x00000000000000000000000000000000'; +export interface QuoteRequest {user:string;recipient:string;originChainId:number;originCurrency:string;amount:string;destinationChainId:1337;destinationCurrency:typeof PERPS_USDC;useDepositAddress:true;tradeType:'EXACT_INPUT';} +const item=z.object({status:z.string(),data:z.record(z.string(),z.unknown()),check:z.object({endpoint:z.string(),method:z.string()}).optional()}).passthrough(); +export const quoteSchema=z.object({steps:z.array(z.object({id:z.string(),kind:z.string(),requestId:z.string().optional(),depositAddress:z.string().optional(),items:z.array(item)}).passthrough()),fees:z.record(z.string(),z.unknown()).default({}),details:z.object({currencyOut:z.object({amount:z.string(),currency:z.object({decimals:z.number()}).passthrough()}).passthrough(),currencyIn:z.object({amount:z.string()}).passthrough().optional(),minAmount:z.string().optional(),expiresAt:z.number().optional()}).passthrough()}).passthrough(); +export type RelayQuote=z.infer; +export class RelayClient { + constructor(readonly base='https://api.relay.link',private fetcher:typeof fetch=fetch){} + async quote(request:QuoteRequest){try{return quoteSchema.parse(await jsonRequest(`${this.base}/quote/v2`,request,this.fetcher));}catch(error){throw mapRelayError(error);}} + async status(requestId:string){try{return await jsonRequest(`${this.base}/intents/status/v3?requestId=${encodeURIComponent(requestId)}`,undefined,this.fetcher) as {status:string};}catch(error){throw mapRelayError(error);}} +} +export function parseQuote(quote:RelayQuote){ + const deposit=quote.steps.find(step=>step.depositAddress);if(!deposit?.requestId) throw mapRelayError('Quote missing deposit address/requestId'); + return {depositId:deposit.requestId,depositAddress:deposit.depositAddress!,fees:{app:quote.fees.app??quote.fees.appFees??null,gas:quote.fees.gas??null,relayer:quote.fees.relayer??quote.fees.relayerService??null,raw:quote.fees},minAmount:quote.details.minAmount??'0',expectedReceived:quote.details.currencyOut.amount,receivedDecimals:quote.details.currencyOut.currency.decimals,expiresAt:quote.details.expiresAt??Date.now()+30_000,steps:quote.steps}; +} diff --git a/test/relay.test.ts b/test/relay.test.ts new file mode 100644 index 0000000..da650ea --- /dev/null +++ b/test/relay.test.ts @@ -0,0 +1,8 @@ +import {expect,test,vi} from 'vitest'; +import {RelayClient,PERPS_USDC,parseQuote} from '../src/funding/relay.js'; +export const quoteFixture={steps:[{id:'deposit',kind:'transaction',requestId:'deposit-1',depositAddress:'0x'+'a'.repeat(40),items:[{status:'incomplete',data:{chainId:42161}}]}],fees:{app:{amount:'10'},gas:{amount:'20'},relayerService:{amount:'30'}},details:{currencyOut:{amount:'100000000',currency:{decimals:8}},minAmount:'1000000',expiresAt:2000000000000}}; +test('Relay request uses perps destination and parses fees/address',async()=>{ + const fetcher=vi.fn(async()=>new Response(JSON.stringify(quoteFixture))) as unknown as typeof fetch; + const client=new RelayClient('https://api.relay.link',fetcher);const quote=await client.quote({user:'master',recipient:'master',originChainId:42161,originCurrency:'USDC',amount:'1000000',destinationChainId:1337,destinationCurrency:PERPS_USDC,useDepositAddress:true,tradeType:'EXACT_INPUT'}); + expect(parseQuote(quote)).toMatchObject({depositId:'deposit-1',expectedReceived:'100000000',fees:{app:{amount:'10'},gas:{amount:'20'},relayer:{amount:'30'}}}); +}); From 779dbb424a00a2efa82ff13acf93a7c109253f8a Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:01:15 -0700 Subject: [PATCH 15/43] feat: add exact USDC decimal conversion (#15) --- docs/PROGRESS.md | 1 + src/funding/amounts.ts | 11 +++++++++++ test/amounts.test.ts | 6 ++++++ 3 files changed, 18 insertions(+) create mode 100644 src/funding/amounts.ts create mode 100644 test/amounts.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index cb00112..b0ce3d4 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -17,3 +17,4 @@ | #12 | done | Idempotent managed-wallet onboarding, persisted external master EIP-712 approvals/deeplink, expiry bound, agent/builder verification and master-key rejection. | | #13 | partial | Builder attachment helper checks approved maximum; fee reporting. No official builder address/rate supplied in specs, so default is zero fee until configured. | | #14 | done | Typed validated Relay quote/status adapter, perps USDC destination, multiple origin routes and fee parsing; mocked fixtures. | +| #15 | done | Exact bigint decimal parse/format and loss-detecting 6↔8 conversions with property-style round trips. | diff --git a/src/funding/amounts.ts b/src/funding/amounts.ts new file mode 100644 index 0000000..ac4b1d4 --- /dev/null +++ b/src/funding/amounts.ts @@ -0,0 +1,11 @@ +import {AgentCliError} from '../errors/index.js'; +function checkDecimals(decimals:number){if(!Number.isInteger(decimals)||decimals<0||decimals>30) throw new RangeError('Invalid decimals');} +export function parseUsdc(value:string,decimals:number):bigint{ + checkDecimals(decimals);if(!/^\d+(\.\d+)?$/.test(value)) throw new AgentCliError('INVALID_SIZE','Expected nonnegative decimal string'); + const [whole,fraction='']=value.split('.');if(fraction.length>decimals) throw new AgentCliError('INVALID_SIZE','Too many decimal places');return BigInt(whole)*10n**BigInt(decimals)+BigInt(fraction.padEnd(decimals,'0')||'0'); +} +export function formatUsdc(value:bigint,decimals:number):string{ + checkDecimals(decimals);const negative=value<0n;const absolute=negative?-value:value;const scale=10n**BigInt(decimals);const fraction=(absolute%scale).toString().padStart(decimals,'0').replace(/0+$/,'');return `${negative?'-':''}${absolute/scale}${fraction?'.'+fraction:''}`; +} +export const toHyperCore=(evm:bigint)=>evm*100n; +export function fromHyperCore(core:bigint){if(core%100n!==0n) throw new AgentCliError('INVALID_SIZE','Conversion would lose HyperCore precision');return core/100n;} diff --git a/test/amounts.test.ts b/test/amounts.test.ts new file mode 100644 index 0000000..1baa964 --- /dev/null +++ b/test/amounts.test.ts @@ -0,0 +1,6 @@ +import {expect,test} from 'vitest'; +import {parseUsdc,formatUsdc,toHyperCore,fromHyperCore} from '../src/funding/amounts.js'; +test('exact bigint round trips across decimals and large values',()=>{ + for(const decimals of [0,6,8]) for(let i=0n;i<1000n;i+=7n){const value=i*123456789012345678901n;expect(parseUsdc(formatUsdc(value,decimals),decimals)).toBe(value);} + expect(fromHyperCore(toHyperCore(1234567n))).toBe(1234567n);expect(()=>fromHyperCore(101n)).toThrow();expect(()=>parseUsdc('1.0000001',6)).toThrow(); +}); From b4b670cdb463e3666361f41caf7c2e90159e60f5 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:01:30 -0700 Subject: [PATCH 16/43] feat: render terminal and file QR codes (#16) --- docs/PROGRESS.md | 1 + src/commands/init.ts | 3 ++- src/funding/qr.ts | 9 +++++++++ test/__snapshots__/qr.test.ts.snap | 17 +++++++++++++++++ test/qr.test.ts | 6 ++++++ 5 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/funding/qr.ts create mode 100644 test/__snapshots__/qr.test.ts.snap create mode 100644 test/qr.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index b0ce3d4..68f76d5 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -18,3 +18,4 @@ | #13 | partial | Builder attachment helper checks approved maximum; fee reporting. No official builder address/rate supplied in specs, so default is zero fee until configured. | | #14 | done | Typed validated Relay quote/status adapter, perps USDC destination, multiple origin routes and fee parsing; mocked fixtures. | | #15 | done | Exact bigint decimal parse/format and loss-detecting 6↔8 conversions with property-style round trips. | +| #16 | done | Terminal QR on stderr and JSON field, optional PNG/SVG, no-qr support and snapshot. | diff --git a/src/commands/init.ts b/src/commands/init.ts index 48bae89..85a8fb2 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -1,4 +1,5 @@ import {z} from 'zod'; +import {renderQr} from '../funding/qr.js'; import {readFile} from 'node:fs/promises'; import {join} from 'node:path'; import {register} from '../core/router.js'; @@ -24,5 +25,5 @@ register('init',{description:'Create agent and emit resumable master-wallet appr if(config.builder.address){const max=await hlInfo(config.network,'maxBuilderFee',{user:config.masterAddress,builder:config.builder.address});config.approved=config.approved&&max>=config.builder.fee;} config.revoked=false;await saveConfig(config,context.configPath); const deeplink=`ethereum:${config.masterAddress}?request=${encodeURIComponent(JSON.stringify(pending.agent.typedData))}`; - return {masterAddress:config.masterAddress,agentAddress:config.agentAddress,expiry,policyId,approved:config.approved,requiresSignature:!config.approved,approvals:pending,deeplink}; + return {masterAddress:config.masterAddress,agentAddress:config.agentAddress,expiry,policyId,approved:config.approved,requiresSignature:!config.approved,approvals:pending,deeplink,...await renderQr(deeplink,{enabled:input.qr as boolean})}; }}); diff --git a/src/funding/qr.ts b/src/funding/qr.ts new file mode 100644 index 0000000..6df32e7 --- /dev/null +++ b/src/funding/qr.ts @@ -0,0 +1,9 @@ +import QRCode from 'qrcode'; +import {writeFile} from 'node:fs/promises'; +export async function renderQr(value:string,options:{enabled?:boolean;path?:string;stderr?:(value:string)=>void}={}){ + if(options.enabled===false) return {qrAscii:null,qrPath:null}; + const qrAscii=await QRCode.toString(value,{type:'terminal',small:true}); + (options.stderr??(value=>process.stderr.write(value)))(qrAscii+'\n'); + if(options.path){if(options.path.endsWith('.svg')) await writeFile(options.path,await QRCode.toString(value,{type:'svg'}),{mode:0o600});else await writeFile(options.path,await QRCode.toBuffer(value,{type:'png'}),{mode:0o600});} + return {qrAscii,qrPath:options.path??null}; +} diff --git a/test/__snapshots__/qr.test.ts.snap b/test/__snapshots__/qr.test.ts.snap new file mode 100644 index 0000000..6fe5140 --- /dev/null +++ b/test/__snapshots__/qr.test.ts.snap @@ -0,0 +1,17 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`QR deterministic terminal snapshot stays off stdout 1`] = ` +" ▄▄▄▄▄▄▄ ▄▄ ▄▄▄▄▄▄▄  + █ ▄▄▄ █ ▄▄▀▄▄ █ ▄▄▄ █  + █ ███ █ █ █▄ █ ███ █  + █▄▄▄▄▄█ █ ▄ █ █▄▄▄▄▄█  + ▄ ▄▄▄▄▄ ▀▀ ▀█ ▄▄▄▄▄  + █▀▄▄▀█▄█ █▄▀ █▄▀▀▄▄▀  + ▀ █▄█▄▄▄ ▄▀▄ █▄█ ▀▀ ▀  + ▄▄▄▄▄▄▄ ▀ █ █▀ ▀█ ▄▀  + █ ▄▄▄ █ █ ▄▄█▀▄▄ █▀▄▄  + █ ███ █ █▄▄█▄█ ▀▀ ▄  + █▄▄▄▄▄█ ▄ ▀▀▄█▄ ▀█▀▄  +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +" +`; diff --git a/test/qr.test.ts b/test/qr.test.ts new file mode 100644 index 0000000..e0b1c64 --- /dev/null +++ b/test/qr.test.ts @@ -0,0 +1,6 @@ +import {expect,test} from 'vitest'; +import {renderQr} from '../src/funding/qr.js'; +test('QR deterministic terminal snapshot stays off stdout',async()=>{ + let stderr='';const result=await renderQr('bloxwap:test',{stderr:value=>{stderr=value;}}); + expect(result.qrAscii).toMatchSnapshot();expect(stderr).toContain(result.qrAscii);expect(await renderQr('disabled',{enabled:false})).toEqual({qrAscii:null,qrPath:null}); +}); From 240404788d8260c9d8959e0e79eca380b6d0be0a Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:02:10 -0700 Subject: [PATCH 17/43] feat: implement idempotent funding command (#17) --- docs/PROGRESS.md | 1 + src/cli.ts | 1 + src/commands/fund.ts | 26 ++++++++++++++++++++++++++ src/funding/store.ts | 8 ++++++++ test/fund.test.ts | 15 +++++++++++++++ 5 files changed, 51 insertions(+) create mode 100644 src/commands/fund.ts create mode 100644 src/funding/store.ts create mode 100644 test/fund.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 68f76d5..9499c48 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -19,3 +19,4 @@ | #14 | done | Typed validated Relay quote/status adapter, perps USDC destination, multiple origin routes and fee parsing; mocked fixtures. | | #15 | done | Exact bigint decimal parse/format and loss-detecting 6↔8 conversions with property-style round trips. | | #16 | done | Terminal QR on stderr and JSON field, optional PNG/SVG, no-qr support and snapshot. | +| #17 | done | Funding command returns address/fees/minimum/expiry/8-decimal received amount, persists deposits and reuses valid quotes. | diff --git a/src/cli.ts b/src/cli.ts index 3d6b493..df51639 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,6 +2,7 @@ import { dispatch } from './core/router.js'; import './commands/config.js'; import './commands/init.js'; +import './commands/fund.js'; let stdin=''; if (!process.stdin.isTTY) for await (const chunk of process.stdin) stdin += String(chunk); const result=await dispatch(process.argv.slice(2),stdin); diff --git a/src/commands/fund.ts b/src/commands/fund.ts new file mode 100644 index 0000000..24b884a --- /dev/null +++ b/src/commands/fund.ts @@ -0,0 +1,26 @@ +import {z} from 'zod'; +import {register} from '../core/router.js'; +import {loadConfig,type Config} from '../core/config.js'; +import {hlInfo} from '../core/http.js'; +import {RelayClient,PERPS_USDC,parseQuote} from '../funding/relay.js'; +import {parseUsdc,formatUsdc} from '../funding/amounts.js'; +import {renderQr} from '../funding/qr.js'; +import {readDeposits,writeDeposits} from '../funding/store.js'; +import {builderRate} from '../core/builder.js'; +import {AgentCliError} from '../errors/index.js'; +export const fundSchema=z.object({originChain:z.number().int().positive().default(42161),originCurrency:z.string().default('0xaf88d065e77c8cC2239327C5EDb3A432268e5831'),originDecimals:z.number().int().min(0).max(30).default(6),amount:z.string().default('10'),qr:z.boolean().default(true),qrPath:z.string().optional()}); +export async function fund(config:Config,input:z.infer,dependencies:{relay?:RelayClient;path?:string;baseline?:()=>Promise;qr?:typeof renderQr}={}){ + if(!config.masterAddress) throw new AgentCliError('AGENT_NOT_APPROVED','Initialize master address'); + const amount=parseUsdc(input.amount,input.originDecimals);if(amount<=0n) throw new AgentCliError('INVALID_SIZE','Positive funding amount required'); + const route=JSON.stringify([config.network,config.masterAddress,input.originChain,input.originCurrency,amount.toString()]); + const deposits=await readDeposits(dependencies.path);let deposit=deposits.find(d=>d.route===route&&d.expiresAt>Date.now()&&d.state==='awaiting_funds'); + if(!deposit){ + const quote=await (dependencies.relay??new RelayClient(config.network==='testnet'?'https://api.testnets.relay.link':'https://api.relay.link')).quote({user:config.masterAddress,recipient:config.masterAddress,originChainId:input.originChain,originCurrency:input.originCurrency,amount:amount.toString(),destinationChainId:1337,destinationCurrency:PERPS_USDC,useDepositAddress:true,tradeType:'EXACT_INPUT'}); + const parsed=parseQuote(quote);if(amount(config.network,'clearinghouseState',{user:config.masterAddress})).marginSummary.accountValue; + deposit={id:parsed.depositId,network:config.network,recipient:config.masterAddress,route,quote,expiresAt:parsed.expiresAt,state:'awaiting_funds',baseline,createdAt:Date.now(),completedSteps:[]};deposits.push(deposit);await writeDeposits(deposits,dependencies.path); + } + const parsed=parseQuote(deposit.quote);const received=formatUsdc(BigInt(parsed.expectedReceived),parsed.receivedDecimals); + return {...parsed,expectedReceived:parseUsdc(received,8).toString(),expectedReceivedHuman:received,originChain:input.originChain,originCurrency:input.originCurrency,minDeposit:formatUsdc(BigInt(parsed.minAmount),input.originDecimals),builderFeeRate:builderRate(config),state:deposit.state,...await (dependencies.qr??renderQr)(parsed.depositAddress,{enabled:input.qr,path:input.qrPath})}; +} +register('fund',{description:'Quote funding with address, QR and transparent fees',schema:fundSchema,execute:async(input,context)=>{const config=await loadConfig(context.configPath);if(context.network) config.network=context.network;return fund(config,input as z.infer);}}); diff --git a/src/funding/store.ts b/src/funding/store.ts new file mode 100644 index 0000000..2d92bb6 --- /dev/null +++ b/src/funding/store.ts @@ -0,0 +1,8 @@ +import {readFile} from 'node:fs/promises'; +import {join} from 'node:path'; +import {atomicWrite,stateDir} from '../core/config.js'; +import type {RelayQuote} from './relay.js'; +export interface Deposit {id:string;network:'mainnet'|'testnet';recipient:string;route:string;quote:RelayQuote;expiresAt:number;state:'awaiting_funds'|'pending'|'settled'|'failed'|'refunded';baseline:string;createdAt:number;completedSteps:string[];} +export const depositPath=()=>join(stateDir(),'deposits.json'); +export async function readDeposits(path=depositPath()):Promise{try{return JSON.parse(await readFile(path,'utf8'));}catch(error){if((error as NodeJS.ErrnoException).code==='ENOENT') return [];throw error;}} +export async function writeDeposits(deposits:Deposit[],path=depositPath()){await atomicWrite(path,JSON.stringify(deposits));} diff --git a/test/fund.test.ts b/test/fund.test.ts new file mode 100644 index 0000000..4dce652 --- /dev/null +++ b/test/fund.test.ts @@ -0,0 +1,15 @@ +import {expect,test,vi} from 'vitest'; +import {mkdtemp,rm} from 'node:fs/promises'; +import {join} from 'node:path'; +import {tmpdir} from 'node:os'; +import {fund,fundSchema} from '../src/commands/fund.js'; +import {migrate} from '../src/core/config.js'; +import {RelayClient} from '../src/funding/relay.js'; +import {quoteFixture} from './relay.test.js'; +test('fund persists and reuses unexpired quotes',async()=>{ + const dir=await mkdtemp(join(tmpdir(),'bloxwap-fund-'));const quote=vi.fn(async()=>quoteFixture);const config=migrate({masterAddress:'0x'+'a'.repeat(40)}); + try{const dependencies={path:join(dir,'deposits.json'),relay:{quote} as unknown as RelayClient,baseline:async()=>'0',qr:async()=>({qrAscii:null,qrPath:null})}; + const input=fundSchema.parse({amount:'10',qr:false});const first=await fund(config,input,dependencies);expect(first.expectedReceived).toBe('100000000');expect((await fund(config,input,dependencies)).depositAddress).toBe(first.depositAddress);expect(quote).toHaveBeenCalledTimes(1); + await expect(fund(config,{...input,amount:'0.1'},dependencies)).rejects.toMatchObject({code:'INVALID_SIZE'}); + }finally{await rm(dir,{recursive:true,force:true});} +}); From 65ee8bb33f6f0ca932d5713dd13c7426bbc77a0b Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:02:43 -0700 Subject: [PATCH 18/43] feat: support resumable deposit signature steps (#18) --- docs/PROGRESS.md | 1 + docs/funding.md | 16 ++++++++++++++++ src/cli.ts | 1 + src/commands/deposit.ts | 9 +++++++++ src/commands/fund.ts | 3 ++- src/funding/signatures.ts | 15 +++++++++++++++ 6 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 docs/funding.md create mode 100644 src/commands/deposit.ts create mode 100644 src/funding/signatures.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 9499c48..165cf49 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -20,3 +20,4 @@ | #15 | done | Exact bigint decimal parse/format and loss-detecting 6↔8 conversions with property-style round trips. | | #16 | done | Terminal QR on stderr and JSON field, optional PNG/SVG, no-qr support and snapshot. | | #17 | done | Funding command returns address/fees/minimum/expiry/8-decimal received amount, persists deposits and reuses valid quotes. | +| #18 | done | Resumable ordered master-signature steps, trusted Relay submission endpoints and funding flow docs. | diff --git a/docs/funding.md b/docs/funding.md new file mode 100644 index 0000000..a52edad --- /dev/null +++ b/docs/funding.md @@ -0,0 +1,16 @@ +# Funding and signatures + +Run `fund --amount 10 --origin-chain 42161` and show the returned QR/address to the +user. Inspect the origin token, minimum, fees and quote expiry before sending. +Amounts are decimal strings; HyperCore perps amounts use 8 decimals, EVM USDC 6. + +Some routes return two master-wallet signatures: a Relay nonce/deposit mapping +and a Hyperliquid deposit authorization. The exact payloads come from the quote; +never synthesize them or sign them using the trade-only agent. `fund` emits +`signatureSteps` with payloads and `requiresSignature`. Query `deposit --deposit-id ID` +to resume; submit each in order with `--step-id ID --signature 0x...`. Completed +steps are persisted immediately. Retrying a completed flow is harmless. + +Poll `status` until the deposit is settled; a bridge status alone is insufficient +without the corresponding Hyperliquid balance increase. Testnet uses Relay's +testnet API and may require a mocked quote if that route is unavailable. diff --git a/src/cli.ts b/src/cli.ts index df51639..3fe0c87 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -3,6 +3,7 @@ import { dispatch } from './core/router.js'; import './commands/config.js'; import './commands/init.js'; import './commands/fund.js'; +import './commands/deposit.js'; let stdin=''; if (!process.stdin.isTTY) for await (const chunk of process.stdin) stdin += String(chunk); const result=await dispatch(process.argv.slice(2),stdin); diff --git a/src/commands/deposit.ts b/src/commands/deposit.ts new file mode 100644 index 0000000..b4422a9 --- /dev/null +++ b/src/commands/deposit.ts @@ -0,0 +1,9 @@ +import {z} from 'zod'; +import {register} from '../core/router.js'; +import {readDeposits} from '../funding/store.js'; +import {signatureSteps,submitDepositSignature} from '../funding/signatures.js'; +import {AgentCliError} from '../errors/index.js'; +register('deposit',{description:'Resume deposit steps or submit a master-wallet signature',schema:z.object({depositId:z.string(),stepId:z.string().optional(),signature:z.string().optional()}),execute:async(input)=>{ + if(input.signature&&input.stepId) return submitDepositSignature(input.depositId as string,input.stepId as string,input.signature as string); + const deposit=(await readDeposits()).find(d=>d.id===input.depositId);if(!deposit) throw new AgentCliError('CONFIG_INVALID','Unknown deposit');return {depositId:deposit.id,steps:signatureSteps(deposit)}; +}}); diff --git a/src/commands/fund.ts b/src/commands/fund.ts index 24b884a..3ea92cc 100644 --- a/src/commands/fund.ts +++ b/src/commands/fund.ts @@ -6,6 +6,7 @@ import {RelayClient,PERPS_USDC,parseQuote} from '../funding/relay.js'; import {parseUsdc,formatUsdc} from '../funding/amounts.js'; import {renderQr} from '../funding/qr.js'; import {readDeposits,writeDeposits} from '../funding/store.js'; +import {signatureSteps} from '../funding/signatures.js'; import {builderRate} from '../core/builder.js'; import {AgentCliError} from '../errors/index.js'; export const fundSchema=z.object({originChain:z.number().int().positive().default(42161),originCurrency:z.string().default('0xaf88d065e77c8cC2239327C5EDb3A432268e5831'),originDecimals:z.number().int().min(0).max(30).default(6),amount:z.string().default('10'),qr:z.boolean().default(true),qrPath:z.string().optional()}); @@ -21,6 +22,6 @@ export async function fund(config:Config,input:z.infer,depend deposit={id:parsed.depositId,network:config.network,recipient:config.masterAddress,route,quote,expiresAt:parsed.expiresAt,state:'awaiting_funds',baseline,createdAt:Date.now(),completedSteps:[]};deposits.push(deposit);await writeDeposits(deposits,dependencies.path); } const parsed=parseQuote(deposit.quote);const received=formatUsdc(BigInt(parsed.expectedReceived),parsed.receivedDecimals); - return {...parsed,expectedReceived:parseUsdc(received,8).toString(),expectedReceivedHuman:received,originChain:input.originChain,originCurrency:input.originCurrency,minDeposit:formatUsdc(BigInt(parsed.minAmount),input.originDecimals),builderFeeRate:builderRate(config),state:deposit.state,...await (dependencies.qr??renderQr)(parsed.depositAddress,{enabled:input.qr,path:input.qrPath})}; + return {...parsed,expectedReceived:parseUsdc(received,8).toString(),expectedReceivedHuman:received,originChain:input.originChain,originCurrency:input.originCurrency,minDeposit:formatUsdc(BigInt(parsed.minAmount),input.originDecimals),builderFeeRate:builderRate(config),state:deposit.state,signatureSteps:signatureSteps(deposit),...await (dependencies.qr??renderQr)(parsed.depositAddress,{enabled:input.qr,path:input.qrPath})}; } register('fund',{description:'Quote funding with address, QR and transparent fees',schema:fundSchema,execute:async(input,context)=>{const config=await loadConfig(context.configPath);if(context.network) config.network=context.network;return fund(config,input as z.infer);}}); diff --git a/src/funding/signatures.ts b/src/funding/signatures.ts new file mode 100644 index 0000000..743d9ff --- /dev/null +++ b/src/funding/signatures.ts @@ -0,0 +1,15 @@ +import {AgentCliError} from '../errors/index.js'; +import {jsonRequest} from '../core/http.js'; +import {readDeposits,writeDeposits,type Deposit} from './store.js'; +export function signatureSteps(deposit:Deposit){return deposit.quote.steps.flatMap((step)=>step.kind==='signature'?step.items.map((item,index)=>({id:`${step.id}:${index}`,requiresSignature:!deposit.completedSteps.includes(`${step.id}:${index}`),payload:item.data.sign,post:item.data.post})):[]);} +export async function submitDepositSignature(depositId:string,stepId:string,signature:string,options:{path?:string;base?:string;submit?:(url:string,body:unknown)=>Promise}={}){ + if(!/^0x[0-9a-fA-F]{130}$/.test(signature)) throw new AgentCliError('CONFIG_INVALID','Expected wallet signature'); + const deposits=await readDeposits(options.path);const deposit=deposits.find(d=>d.id===depositId);if(!deposit) throw new AgentCliError('CONFIG_INVALID','Unknown deposit'); + const pending=signatureSteps(deposit).filter(s=>s.requiresSignature);if(!pending.length) return {complete:true};const step=pending[0]; + if(step.id!==stepId) throw new AgentCliError('CONFIG_INVALID','Submit signatures in step order',{nextStep:step.id}); + const post=step.post as {endpoint:string;method:string;body:Record}; + if(!post||post.method!=='POST'||!post.endpoint.startsWith('/')||post.endpoint.startsWith('//')) throw new AgentCliError('UPSTREAM_ERROR','Invalid signature submission endpoint'); + const base=options.base??(deposit.network==='testnet'?'https://api.testnets.relay.link':'https://api.relay.link');const url=new URL(post.endpoint,base);if(url.origin!==new URL(base).origin) throw new AgentCliError('POLICY_REJECTED','Untrusted signature destination'); + await (options.submit??jsonRequest)(url.toString(),{...post.body,signature});deposit.completedSteps.push(step.id);await writeDeposits(deposits,options.path); + return {depositId,steps:signatureSteps(deposit)}; +} From bfce6e5a94ad39e29f0f846c4de1dbd839fbaf52 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:03:46 -0700 Subject: [PATCH 19/43] feat: track deposit settlement lifecycle (#19) --- docs/PROGRESS.md | 1 + src/funding/status.ts | 26 ++++++++++++++++++++++++++ test/deposit-status.test.ts | 8 ++++++++ 3 files changed, 35 insertions(+) create mode 100644 src/funding/status.ts create mode 100644 test/deposit-status.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 165cf49..5bf5a7e 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -21,3 +21,4 @@ | #16 | done | Terminal QR on stderr and JSON field, optional PNG/SVG, no-qr support and snapshot. | | #17 | done | Funding command returns address/fees/minimum/expiry/8-decimal received amount, persists deposits and reuses valid quotes. | | #18 | done | Resumable ordered master-signature steps, trusted Relay submission endpoints and funding flow docs. | +| #19 | done | Persisted terminal deposit lifecycle, Relay + exact balance settlement, retryable trading gate and runner emission hook. | diff --git a/src/funding/status.ts b/src/funding/status.ts new file mode 100644 index 0000000..2ba719c --- /dev/null +++ b/src/funding/status.ts @@ -0,0 +1,26 @@ +import {readDeposits,writeDeposits,type Deposit} from './store.js'; +import {RelayClient,parseQuote} from './relay.js'; +import {parseUsdc} from './amounts.js'; +import {AgentCliError} from '../errors/index.js'; +export function advanceDeposit(deposit:Deposit,relayStatus:string,balance:string){ + if(['settled','failed','refunded'].includes(deposit.state)) return deposit.state; + if(relayStatus==='failure'||relayStatus==='failed') return 'failed' as const; + if(relayStatus==='refunded') return 'refunded' as const; + const quote=parseQuote(deposit.quote);const needed=BigInt(quote.expectedReceived)*10n**BigInt(8-quote.receivedDecimals); + if(relayStatus==='success'&&parseUsdc(balance,8)>=parseUsdc(deposit.baseline,8)+needed) return 'settled' as const; + if(['success','pending','deposit-confirmed','waiting'].includes(relayStatus)) return 'pending' as const; + return deposit.state; +} +export async function pollDeposits(network:Deposit['network'],recipient:string,balance:string,options:{path?:string;relay?:RelayClient;emit?:(deposit:Deposit)=>Promise}={}){ + const deposits=await readDeposits(options.path);let changed=false; + for(const deposit of deposits.filter(d=>d.network===network&&d.recipient.toLowerCase()===recipient.toLowerCase())){ + if(['settled','failed','refunded'].includes(deposit.state)) continue; + const status=await (options.relay??new RelayClient(network==='testnet'?'https://api.testnets.relay.link':'https://api.relay.link')).status(deposit.id); + const next=advanceDeposit(deposit,status.status,balance);if(next!==deposit.state){deposit.state=next;changed=true;await options.emit?.(deposit);} + } + if(changed) await writeDeposits(deposits,options.path);return deposits.filter(d=>d.network===network&&d.recipient.toLowerCase()===recipient.toLowerCase()); +} +export async function assertDepositsSettled(network:Deposit['network'],recipient:string,path?:string){ + const pending=(await readDeposits(path)).filter(d=>d.network===network&&d.recipient.toLowerCase()===recipient.toLowerCase()&&['awaiting_funds','pending'].includes(d.state)); + if(pending.length) throw new AgentCliError('DEPOSIT_PENDING','Wait for deposit settlement',{depositIds:pending.map(d=>d.id)}); +} diff --git a/test/deposit-status.test.ts b/test/deposit-status.test.ts new file mode 100644 index 0000000..5bd7f43 --- /dev/null +++ b/test/deposit-status.test.ts @@ -0,0 +1,8 @@ +import {expect,test} from 'vitest'; +import {advanceDeposit} from '../src/funding/status.js'; +import type {Deposit} from '../src/funding/store.js'; +import {quoteFixture} from './relay.test.js'; +test('settlement requires Relay success plus expected balance increase',()=>{ + const deposit={quote:quoteFixture,state:'awaiting_funds',baseline:'2',id:'one',network:'testnet',recipient:'master',route:'route',expiresAt:2000000000000,createdAt:0,completedSteps:[]} satisfies Deposit; + expect(advanceDeposit(deposit,'success','2.9')).toBe('pending');expect(advanceDeposit(deposit,'success','3')).toBe('settled');expect(advanceDeposit(deposit,'failed','3')).toBe('failed');expect(advanceDeposit({...deposit,state:'settled'},'failed','0')).toBe('settled'); +}); From e55065c6113b169ad7fa37887f649e46839b048a Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:03:56 -0700 Subject: [PATCH 20/43] feat: add account status balance positions and orders (#20) --- docs/PROGRESS.md | 1 + src/cli.ts | 1 + src/commands/read.ts | 25 +++++++++++++++++++++++++ test/read.test.ts | 5 +++++ 4 files changed, 32 insertions(+) create mode 100644 src/commands/read.ts create mode 100644 test/read.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 5bf5a7e..20d299f 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -22,3 +22,4 @@ | #17 | done | Funding command returns address/fees/minimum/expiry/8-decimal received amount, persists deposits and reuses valid quotes. | | #18 | done | Resumable ordered master-signature steps, trusted Relay submission endpoints and funding flow docs. | | #19 | done | Persisted terminal deposit lifecycle, Relay + exact balance settlement, retryable trading gate and runner emission hook. | +| #20 | done | Account read commands, numeric strings, deposits/approval/policy/expiry status; runner preference wired in #27. | diff --git a/src/cli.ts b/src/cli.ts index 3fe0c87..2a950cf 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,6 +4,7 @@ import './commands/config.js'; import './commands/init.js'; import './commands/fund.js'; import './commands/deposit.js'; +import './commands/read.js'; let stdin=''; if (!process.stdin.isTTY) for await (const chunk of process.stdin) stdin += String(chunk); const result=await dispatch(process.argv.slice(2),stdin); diff --git a/src/commands/read.ts b/src/commands/read.ts new file mode 100644 index 0000000..0fb671f --- /dev/null +++ b/src/commands/read.ts @@ -0,0 +1,25 @@ +import {z} from 'zod'; +import {register,type Context} from '../core/router.js'; +import {loadConfig,type Config} from '../core/config.js'; +import {hlInfo} from '../core/http.js'; +import {pollDeposits} from '../funding/status.js'; +import {parseUsdc} from '../funding/amounts.js'; +import {AgentCliError} from '../errors/index.js'; +import {mapHyperliquidError} from '../errors/mappers.js'; +export interface AccountState {marginSummary:{accountValue:string;totalMarginUsed:string;totalNtlPos:string};withdrawable:string;assetPositions:{position:{coin:string;szi:string;entryPx:string|null;unrealizedPnl:string;liquidationPx:string|null;leverage:{type:string;value:number}}}[];} +export interface OpenOrder {coin:string;oid:number;cloid?:string;sz:string;limitPx:string;side:'A'|'B';} +export function accountBalance(state:AccountState){return {accountValue:state.marginSummary.accountValue,accountValueUnits:parseUsdc(state.marginSummary.accountValue,8).toString(),withdrawable:state.withdrawable,marginUsed:state.marginSummary.totalMarginUsed};} +export function positions(state:AccountState){return state.assetPositions.map(({position:p})=>({coin:p.coin,size:p.szi,entryPrice:p.entryPx,unrealizedPnl:p.unrealizedPnl,liquidationPrice:p.liquidationPx,leverage:String(p.leverage.value),marginMode:p.leverage.type}));} +export function expiryStatus(config:Config,now=Date.now()){const days=config.agentExpiry===undefined?null:Math.max(0,(config.agentExpiry-now)/86400000);return {expiry:config.agentExpiry??null,daysRemaining:days===null?null:String(days),warnings:days===null?[]:config.expiryWarningDays.filter(threshold=>days<=threshold).map(threshold=>`Agent expires within ${threshold} days`)};} +export async function readCommand(name:string,config:Config){ + if(!config.masterAddress) throw new AgentCliError('AGENT_NOT_APPROVED','Initialize master address'); + try{ + if(name==='orders') return (await hlInfo(config.network,'frontendOpenOrders',{user:config.masterAddress})).map(order=>({...order,oid:String(order.oid)})); + const state=await hlInfo(config.network,'clearinghouseState',{user:config.masterAddress}); + if(name==='balance') return accountBalance(state);if(name==='positions') return positions(state); + const [agents,deposits,maxBuilderFee]=await Promise.all([hlInfo<{address:string;validUntil:number}[]>(config.network,'extraAgents',{user:config.masterAddress}),pollDeposits(config.network,config.masterAddress,state.marginSummary.accountValue),config.builder.address?hlInfo(config.network,'maxBuilderFee',{user:config.masterAddress,builder:config.builder.address}):Promise.resolve(0)]); + return {balance:accountBalance(state),positions:positions(state),agent:{address:config.agentAddress,approved:agents.some(a=>a.address.toLowerCase()===config.agentAddress?.toLowerCase()),revoked:config.revoked,...expiryStatus(config)},policy:{id:config.policyId??null,version:String(config.policyVersion??0)},deposits:deposits.map(d=>({id:d.id,state:d.state})),approvedMaxBuilderFee:String(maxBuilderFee),runner:null}; + }catch(error){throw mapHyperliquidError(error);} +} +export async function commandConfig(context:Context){const config=await loadConfig(context.configPath);if(context.network) config.network=context.network;return config;} +for(const name of ['status','balance','positions','orders']) register(name,{description:`Read account ${name}`,schema:z.object({}),execute:async(_input,context)=>readCommand(name,await commandConfig(context))}); diff --git a/test/read.test.ts b/test/read.test.ts new file mode 100644 index 0000000..633b569 --- /dev/null +++ b/test/read.test.ts @@ -0,0 +1,5 @@ +import {expect,test} from 'vitest'; +import {accountBalance,positions,expiryStatus,type AccountState} from '../src/commands/read.js'; +import {migrate} from '../src/core/config.js'; +export const accountFixture:AccountState={marginSummary:{accountValue:'100.12345678',totalMarginUsed:'4',totalNtlPos:'20'},withdrawable:'96',assetPositions:[{position:{coin:'BTC',szi:'0.001',entryPx:'20000',unrealizedPnl:'1',liquidationPx:'10000',leverage:{type:'cross',value:5}}}]}; +test('read fixtures retain numeric strings and exact balances',()=>{expect(accountBalance(accountFixture).accountValueUnits).toBe('10012345678');expect(positions(accountFixture)[0]).toMatchObject({size:'0.001',leverage:'5',entryPrice:'20000'});expect(expiryStatus(migrate({agentExpiry:86400000}),0).warnings).toHaveLength(3);}); From bd697f25eedfb0a1bde0101031556f90502e1ac2 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:04:23 -0700 Subject: [PATCH 21/43] feat: enforce local pre-trade risk limits (#21) --- docs/PROGRESS.md | 1 + src/core/risk.ts | 21 +++++++++++++++++++++ test/risk.test.ts | 6 ++++++ 3 files changed, 28 insertions(+) create mode 100644 src/core/risk.ts create mode 100644 test/risk.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 20d299f..18fb463 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -23,3 +23,4 @@ | #18 | done | Resumable ordered master-signature steps, trusted Relay submission endpoints and funding flow docs. | | #19 | done | Persisted terminal deposit lifecycle, Relay + exact balance settlement, retryable trading gate and runner emission hook. | | #20 | done | Account read commands, numeric strings, deposits/approval/policy/expiry status; runner preference wired in #27. | +| #21 | done | Exact decimal risk evaluation covers markets, size/notional/position/leverage/loss/open-order limits; trade hooks and audit wired in #22/#35. | diff --git a/src/core/risk.ts b/src/core/risk.ts new file mode 100644 index 0000000..59f2de2 --- /dev/null +++ b/src/core/risk.ts @@ -0,0 +1,21 @@ +import type {Config} from './config.js'; +import {AgentCliError} from '../errors/index.js'; +import {parseUsdc} from '../funding/amounts.js'; +export interface RiskIntent {coin:string;size:string;price:string;side:'buy'|'sell';leverage:number;reduceOnly?:boolean;} +export interface RiskState {positionSize:string;dailyLoss:string;openOrders:number;marketMaxLeverage:number;pendingSize?:string;} +const scale=10n**18n; +const decimal=(value:string)=>value.startsWith('-')?-parseUsdc(value.slice(1),18):parseUsdc(value,18); +const absolute=(value:bigint)=>value<0n?-value:value; +export function evaluateRisk(config:Config,intent:RiskIntent,state:RiskState){ + const fail=(limit:string,actual:unknown,maximum:unknown):never=>{throw new AgentCliError('POLICY_REJECTED',`Risk limit: ${limit}`,{limit,actual,maximum});}; + if(config.allowedMarkets.length&&!config.allowedMarkets.includes(intent.coin)) fail('allowedMarkets',intent.coin,config.allowedMarkets); + if(!Number.isInteger(intent.leverage)||intent.leverage<1||intent.leverage>Math.min(config.limits.maxLeverage??Infinity,state.marketMaxLeverage)) fail('maxLeverage',intent.leverage,Math.min(config.limits.maxLeverage??Infinity,state.marketMaxLeverage)); + const size=decimal(intent.size),price=decimal(intent.price);if(size<=0n||price<=0n) fail('positive size/price',intent.size,intent.price); + const notional=size*price/scale;const signed=intent.side==='buy'?size:-size;const position=decimal(state.positionSize); + if(intent.reduceOnly&&((position===0n)||(position>0n&&signed>0n)||(position<0n&&signed<0n))) fail('reduceOnly',intent.side,state.positionSize); + const projected=intent.reduceOnly?absolute(position)- (size>absolute(position)?absolute(position):size):absolute(position+signed+decimal(state.pendingSize??'0')); + const comparisons:[string,bigint,number|undefined][]=[['maxOrderSize',size,config.limits.maxOrderSize],['maxNotional',notional,config.limits.maxNotional],['maxPositionNotional',projected*price/scale,config.limits.maxPositionNotional],['dailyLoss',decimal(state.dailyLoss),config.limits.dailyLoss]]; + for(const [name,actual,maximum] of comparisons) if(maximum!==undefined&&actual>decimal(String(maximum))) fail(name,actual.toString(),maximum); + if(config.limits.maxOpenOrders!==undefined&&state.openOrders>=config.limits.maxOpenOrders) fail('maxOpenOrders',state.openOrders,config.limits.maxOpenOrders); + return {allowed:true,notional: notional.toString()}; +} diff --git a/test/risk.test.ts b/test/risk.test.ts new file mode 100644 index 0000000..c61ba3e --- /dev/null +++ b/test/risk.test.ts @@ -0,0 +1,6 @@ +import {expect,test} from 'vitest'; +import {evaluateRisk} from '../src/core/risk.js'; +import {migrate} from '../src/core/config.js'; +const intent={coin:'BTC',side:'buy' as const,size:'2',price:'10',leverage:2};const state={positionSize:'1',dailyLoss:'5',openOrders:2,marketMaxLeverage:40}; +test.each([{allowedMarkets:['ETH']},{limits:{maxOrderSize:1}},{limits:{maxNotional:19}},{limits:{maxPositionNotional:29}},{limits:{dailyLoss:4}},{limits:{maxOpenOrders:2}},{limits:{maxLeverage:1}}])('enforces each risk bound %j',config=>{expect(()=>evaluateRisk(migrate(config),intent,state)).toThrow(expect.objectContaining({code:'POLICY_REJECTED'}));}); +test('reduce-only cannot increase exposure and decimals remain exact',()=>{expect(evaluateRisk(migrate({}),intent,state).allowed).toBe(true);expect(()=>evaluateRisk(migrate({}),{...intent,reduceOnly:true},state)).toThrow();expect(evaluateRisk(migrate({}),{...intent,side:'sell',reduceOnly:true},state).allowed).toBe(true);}); From eb74b5774ad1b195587f5b5c1569f94c7272a608 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:05:11 -0700 Subject: [PATCH 22/43] feat: implement risk-checked order placement (#22) --- docs/PROGRESS.md | 1 + src/cli.ts | 1 + src/commands/order.ts | 25 +++++++++++++++++++++++++ src/core/router.ts | 3 ++- src/core/trading.ts | 38 ++++++++++++++++++++++++++++++++++++++ test/order.test.ts | 11 +++++++++++ 6 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 src/commands/order.ts create mode 100644 src/core/trading.ts create mode 100644 test/order.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 18fb463..fca17aa 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -24,3 +24,4 @@ | #19 | done | Persisted terminal deposit lifecycle, Relay + exact balance settlement, retryable trading gate and runner emission hook. | | #20 | done | Account read commands, numeric strings, deposits/approval/policy/expiry status; runner preference wired in #27. | | #21 | done | Exact decimal risk evaluation covers markets, size/notional/position/leverage/loss/open-order limits; trade hooks and audit wired in #22/#35. | +| #22 | done | Limit Gtc/Ioc/Alo, slippage IOC market, TP/SL, rounding, cloid, risk/deposit/expiry gates, builder and Privy signing; runner routing in #27. | diff --git a/src/cli.ts b/src/cli.ts index 2a950cf..fc8dacc 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -5,6 +5,7 @@ import './commands/init.js'; import './commands/fund.js'; import './commands/deposit.js'; import './commands/read.js'; +import './commands/order.js'; let stdin=''; if (!process.stdin.isTTY) for await (const chunk of process.stdin) stdin += String(chunk); const result=await dispatch(process.argv.slice(2),stdin); diff --git a/src/commands/order.ts b/src/commands/order.ts new file mode 100644 index 0000000..347042b --- /dev/null +++ b/src/commands/order.ts @@ -0,0 +1,25 @@ +import {z} from 'zod'; +import {register} from '../core/router.js'; +import {commandConfig} from './read.js'; +import {type Config} from '../core/config.js'; +import {roundSize,roundPrice} from '../core/hlclient.js'; +import {approvedBuilder,builderRate} from '../core/builder.js'; +import {assertCanTrade,tradingServices,checkRisk,exchangeAction,type TradingServices} from '../core/trading.js'; +import {AgentCliError} from '../errors/index.js'; +export const orderSchema=z.object({coin:z.string(),side:z.enum(['buy','sell']),size:z.string(),price:z.string().optional(),type:z.enum(['limit','market','tp','sl']).default('limit'),tif:z.enum(['Gtc','Ioc','Alo']).default('Gtc'),reduceOnly:z.boolean().default(false),cloid:z.string().regex(/^0x[0-9a-fA-F]{32}$/).optional(),triggerPrice:z.string().optional(),slippage:z.number().min(0).max(0.1).default(0.01),intent:z.string().optional()}); +export type OrderInput=z.infer; +export async function prepareOrder(config:Config,input:OrderInput,services:TradingServices){ + const market=await services.market(input.coin);const size=roundSize(input.size,market.szDecimals); + let price=input.price;if(input.type==='market'){const mid=(await services.mids())[input.coin];if(!mid) throw new AgentCliError('UNKNOWN_MARKET','Missing market price');price=String(Number(mid)*(input.side==='buy'?1+input.slippage:1-input.slippage));} + if(!price) throw new AgentCliError('INVALID_PRICE','Provide limit price');price=roundPrice(price,market.szDecimals); + const state=await services.state();const leverage=state.assetPositions.find(p=>p.position.coin===input.coin)?.position.leverage.value??1; + await checkRisk(config,{coin:input.coin,size,price,side:input.side,leverage,reduceOnly:input.reduceOnly},services); + let type:unknown={limit:{tif:input.type==='market'?'Ioc':input.tif}}; + if(input.type==='tp'||input.type==='sl'){if(!input.triggerPrice) throw new AgentCliError('INVALID_PRICE','Trigger price required');type={trigger:{isMarket:!input.price,triggerPx:roundPrice(input.triggerPrice,market.szDecimals),tpsl:input.type}};} + return {a:market.asset,b:input.side==='buy',p:price,s:size,r:input.reduceOnly,t:type,...(input.cloid?{c:input.cloid}:{})}; +} +export async function placeOrder(config:Config,input:OrderInput,services?:TradingServices){ + await assertCanTrade(config);const clients=services??await tradingServices(config);const order=await prepareOrder(config,input,clients);const builder=await approvedBuilder(config); + const result=await exchangeAction('order',()=>clients.exchange.order({orders:[order],grouping:'na',...(builder?{builder}:{})}));return {result,builderFeeRate:builderRate(config),cloid:input.cloid??null}; +} +register('order',{description:'Place limit, IOC market or TP/SL order',schema:orderSchema,execute:async(input,context)=>placeOrder(await commandConfig(context),input as OrderInput)}); diff --git a/src/core/router.ts b/src/core/router.ts index a6e8b12..5600648 100644 --- a/src/core/router.ts +++ b/src/core/router.ts @@ -24,7 +24,8 @@ export async function dispatch(argv: string[], stdin = '') { if (arg.startsWith('--no-')) { input[arg.slice(5).replace(/-([a-z])/g,(_,c: string)=>c.toUpperCase())]=false; continue; } if (arg.startsWith('--')) { const [flag,inline] = arg.slice(2).split('=',2); const key=flag.replace(/-([a-z])/g,(_,c: string)=>c.toUpperCase()); - input[key]=inline !== undefined ? parseValue(inline) : argv[i+1] && !argv[i+1].startsWith('--') ? parseValue(argv[++i]) : true; continue; + const raw=inline !== undefined ? inline : argv[i+1] && !argv[i+1].startsWith('--') ? argv[++i] : undefined; + input[key]=raw===undefined?true:['size','price','amount','triggerPrice','signature','builderSignature','cloid'].includes(key)?raw:parseValue(raw);continue; } positional.push(arg); } diff --git a/src/core/trading.ts b/src/core/trading.ts new file mode 100644 index 0000000..6e77961 --- /dev/null +++ b/src/core/trading.ts @@ -0,0 +1,38 @@ +import {randomUUID} from 'node:crypto'; +import {type Config} from './config.js'; +import {createClients,type ExchangeAdapter,type Market} from './hlclient.js'; +import {privyClient,privySigner} from '../wallet/privy.js'; +import {nextNonce} from './nonce.js'; +import {hlInfo} from './http.js'; +import {type AccountState,type OpenOrder} from '../commands/read.js'; +import {AgentCliError} from '../errors/index.js'; +import {assertDepositsSettled} from '../funding/status.js'; +import {assertTradeAction} from '../wallet/policy.js'; +import {mapHyperliquidError} from '../errors/mappers.js'; +import {evaluateRisk,type RiskIntent} from './risk.js'; +import {Logger} from './logger.js'; +export interface TradingServices {exchange:ExchangeAdapter;market:(coin:string)=>Promise;state:()=>Promise;orders:()=>Promise;mids:()=>Promise>;} +export async function tradingServices(config:Config):Promise{ + const signer=await privySigner(config,await privyClient(config));const clients=createClients(config.network,signer,()=>nextNonce(signer.address)); + return {exchange:clients.exchange!,market:coin=>clients.meta.lookup(coin),state:()=>hlInfo(config.network,'clearinghouseState',{user:config.masterAddress}),orders:()=>hlInfo(config.network,'frontendOpenOrders',{user:config.masterAddress}),mids:()=>hlInfo(config.network,'allMids')}; +} +export async function assertCanTrade(config:Config){ + if(config.revoked||!config.approved||!config.masterAddress) throw new AgentCliError('AGENT_NOT_APPROVED','Agent approval required'); + if(!config.agentExpiry||config.agentExpiry<=Date.now()) throw new AgentCliError('AGENT_EXPIRED','Rotate expired agent'); + await assertDepositsSettled(config.network,config.masterAddress); +} +export async function checkRisk(config:Config,intent:RiskIntent,services:TradingServices,ignoreOid?:number){ + const [market,state,orders]=await Promise.all([services.market(intent.coin),services.state(),services.orders()]); + const position=state.assetPositions.find(p=>p.position.coin===intent.coin)?.position; + const open=orders.filter(o=>o.oid!==ignoreOid);const pending=open.filter(o=>o.coin===intent.coin).reduce((sum,o)=>sum+(o.side==='B'?1:-1)*Number(o.sz),0); + const logger=new Logger({level:config.logLevel,intentId:randomUUID()}); + try{return evaluateRisk(config,intent,{positionSize:position?.szi??'0',dailyLoss:String(Math.max(0,-state.assetPositions.reduce((sum,p)=>sum+Number(p.position.unrealizedPnl),0))),openOrders:open.length,marketMaxLeverage:market.maxLeverage,pendingSize:String(pending)});}catch(error){logger.log('warn','Risk rejected',{error:mapHyperliquidError(error).toJSON()});await logger.flush();throw error;} +} +export async function exchangeAction(action:string,operation:()=>Promise){ + assertTradeAction(action);try{ + const response=await operation();const data=response as {status?:string;response?:{data?:{statuses?:unknown[]}}}; + if(data.status==='err') throw mapHyperliquidError(response); + for(const status of data.response?.data?.statuses??[]) if(status&&typeof status==='object'&&'error' in status) throw mapHyperliquidError((status as {error:string}).error); + return response; + }catch(error){throw mapHyperliquidError(error);} +} diff --git a/test/order.test.ts b/test/order.test.ts new file mode 100644 index 0000000..3c0d053 --- /dev/null +++ b/test/order.test.ts @@ -0,0 +1,11 @@ +import {expect,test,vi} from 'vitest'; +import {prepareOrder,orderSchema} from '../src/commands/order.js'; +import {migrate} from '../src/core/config.js'; +import type {TradingServices} from '../src/core/trading.js'; +import {accountFixture} from './read.test.js'; +export function mockTrading():TradingServices{return {market:async()=>({name:'BTC',szDecimals:5,maxLeverage:40,asset:0}),state:async()=>accountFixture,orders:async()=>[],mids:async()=>({BTC:'20000'}),exchange:{order:vi.fn(async()=>({status:'ok'})),cancel:vi.fn(async()=>({status:'ok'})),cancelByCloid:vi.fn(async()=>({status:'ok'})),batchModify:vi.fn(async()=>({status:'ok'})),updateLeverage:vi.fn(async()=>({status:'ok'}))}};} +test('order rounds limit and bounds market IOC',async()=>{ + const clients=mockTrading();expect(await prepareOrder(migrate({}),orderSchema.parse({coin:'BTC',side:'buy',size:'0.0012345',price:'20000.1'}),clients)).toMatchObject({a:0,s:'0.00123',p:'20000',t:{limit:{tif:'Gtc'}}}); + expect(await prepareOrder(migrate({}),orderSchema.parse({coin:'BTC',side:'sell',size:'0.001',type:'market'}),clients)).toMatchObject({p:'19800',t:{limit:{tif:'Ioc'}}}); + await expect(prepareOrder(migrate({}),orderSchema.parse({coin:'BTC',side:'buy',size:'0',price:'1'}),clients)).rejects.toMatchObject({code:'INVALID_SIZE'}); +}); From 3b06ce11ffafba83a7da1e1ea96acf6acb8f73a3 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:05:39 -0700 Subject: [PATCH 23/43] feat: add single and batch order cancellation (#23) --- docs/PROGRESS.md | 1 + src/cli.ts | 1 + src/commands/cancel.ts | 18 ++++++++++++++++++ test/cancel.test.ts | 8 ++++++++ 4 files changed, 28 insertions(+) create mode 100644 src/commands/cancel.ts create mode 100644 test/cancel.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index fca17aa..8c6ec6f 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -25,3 +25,4 @@ | #20 | done | Account read commands, numeric strings, deposits/approval/policy/expiry status; runner preference wired in #27. | | #21 | done | Exact decimal risk evaluation covers markets, size/notional/position/leverage/loss/open-order limits; trade hooks and audit wired in #22/#35. | | #22 | done | Limit Gtc/Ioc/Alo, slippage IOC market, TP/SL, rounding, cloid, risk/deposit/expiry gates, builder and Privy signing; runner routing in #27. | +| #23 | done | Batch cancellation by oid/cloid, coin or account; unknown order normalization and mocked tests. | diff --git a/src/cli.ts b/src/cli.ts index fc8dacc..67b4daa 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -6,6 +6,7 @@ import './commands/fund.js'; import './commands/deposit.js'; import './commands/read.js'; import './commands/order.js'; +import './commands/cancel.js'; let stdin=''; if (!process.stdin.isTTY) for await (const chunk of process.stdin) stdin += String(chunk); const result=await dispatch(process.argv.slice(2),stdin); diff --git a/src/commands/cancel.ts b/src/commands/cancel.ts new file mode 100644 index 0000000..f901688 --- /dev/null +++ b/src/commands/cancel.ts @@ -0,0 +1,18 @@ +import {z} from 'zod'; +import {register} from '../core/router.js'; +import {commandConfig} from './read.js'; +import type {Config} from '../core/config.js'; +import {assertCanTrade,tradingServices,exchangeAction,type TradingServices} from '../core/trading.js'; +import {AgentCliError} from '../errors/index.js'; +export const cancelSchema=z.object({oid:z.number().int().positive().optional(),oids:z.array(z.number().int().positive()).optional(),cloid:z.string().optional(),cloids:z.array(z.string()).optional(),coin:z.string().optional(),all:z.boolean().default(false),intent:z.string().optional()}); +export async function cancelOrders(config:Config,input:z.infer,services?:TradingServices){ + await assertCanTrade(config);const clients=services??await tradingServices(config);const orders=await clients.orders(); + const oids=input.oids??(input.oid?[input.oid]:[]),cloids=input.cloids??(input.cloid?[input.cloid]:[]); + if(!input.all&&!input.coin&&!oids.length&&!cloids.length) throw new AgentCliError('CONFIG_INVALID','Select oid, cloid, coin or all'); + for(const oid of oids) if(!orders.some(o=>o.oid===oid)) throw new AgentCliError('ORDER_NOT_FOUND',`Order ${oid} absent`); + for(const cloid of cloids) if(!orders.some(o=>o.cloid===cloid)) throw new AgentCliError('ORDER_NOT_FOUND',`Order ${cloid} absent`); + const selected=orders.filter(o=>(!input.coin||o.coin===input.coin)&&(input.all||input.coin&&!oids.length&&!cloids.length||oids.includes(o.oid)||!!o.cloid&&cloids.includes(o.cloid))); + const cancels=await Promise.all(selected.map(async o=>({a:(await clients.market(o.coin)).asset,o:o.oid}))); + if(!cancels.length) return {cancelled:[],result:null};return {cancelled:selected.map(o=>String(o.oid)),result:await exchangeAction('cancel',()=>clients.exchange.cancel({cancels}))}; +} +register('cancel',{description:'Cancel by oid/cloid, coin or all',schema:cancelSchema,execute:async(input,context)=>cancelOrders(await commandConfig(context),input as z.infer)}); diff --git a/test/cancel.test.ts b/test/cancel.test.ts new file mode 100644 index 0000000..52d42d7 --- /dev/null +++ b/test/cancel.test.ts @@ -0,0 +1,8 @@ +import {expect,test} from 'vitest'; +import {cancelOrders,cancelSchema} from '../src/commands/cancel.js'; +import {mockTrading} from './order.test.js'; +import {migrate} from '../src/core/config.js'; +test('batch cancellation selects requested orders and rejects unknown ids',async()=>{ + const config=migrate({approved:true,masterAddress:'0x'+'1'.repeat(40),agentExpiry:Date.now()+10000});const services=mockTrading();services.orders=async()=>[{coin:'BTC',oid:1,sz:'1',limitPx:'2',side:'B'},{coin:'BTC',oid:2,sz:'1',limitPx:'2',side:'B'}]; + expect((await cancelOrders(config,cancelSchema.parse({oids:[1,2]}),services)).cancelled).toEqual(['1','2']);await expect(cancelOrders(config,cancelSchema.parse({oid:9}),services)).rejects.toMatchObject({code:'ORDER_NOT_FOUND'}); +}); From 98b8c2896840f8788fa70b4f20f841c83df04a9b Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:06:27 -0700 Subject: [PATCH 24/43] feat: add risk-checked order modification (#24) --- docs/PROGRESS.md | 1 + src/commands/modify.ts | 22 ++++++++++++++++++++++ test/modify.test.ts | 9 +++++++++ 3 files changed, 32 insertions(+) create mode 100644 src/commands/modify.ts create mode 100644 test/modify.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 8c6ec6f..237b561 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -26,3 +26,4 @@ | #21 | done | Exact decimal risk evaluation covers markets, size/notional/position/leverage/loss/open-order limits; trade hooks and audit wired in #22/#35. | | #22 | done | Limit Gtc/Ioc/Alo, slippage IOC market, TP/SL, rounding, cloid, risk/deposit/expiry gates, builder and Privy signing; runner routing in #27. | | #23 | done | Batch cancellation by oid/cloid, coin or account; unknown order normalization and mocked tests. | +| #24 | done | Single/batch modification by oid/cloid with rounding and pre-signing risk checks. | diff --git a/src/commands/modify.ts b/src/commands/modify.ts new file mode 100644 index 0000000..ab1b89f --- /dev/null +++ b/src/commands/modify.ts @@ -0,0 +1,22 @@ +import {z} from 'zod'; +import {register} from '../core/router.js'; +import {commandConfig} from './read.js'; +import type {Config} from '../core/config.js'; +import {assertCanTrade,tradingServices,checkRisk,exchangeAction,type TradingServices} from '../core/trading.js'; +import {roundSize,roundPrice} from '../core/hlclient.js'; +import {AgentCliError} from '../errors/index.js'; +const modification=z.object({oid:z.number().int().positive().optional(),cloid:z.string().regex(/^0x[0-9a-fA-F]{32}$/).optional(),size:z.string().optional(),price:z.string().optional()}); +export const modifySchema=modification.extend({modifications:z.array(modification).optional(),intent:z.string().optional()}); +export async function modifyOrders(config:Config,input:z.infer,services?:TradingServices){ + await assertCanTrade(config);const clients=services??await tradingServices(config);const orders=await clients.orders();const requests=input.modifications??[input];const modifies:Record[]=[]; + for(const request of requests){ + if(!request.oid&&!request.cloid) throw new AgentCliError('CONFIG_INVALID','Specify oid or cloid'); + const original=orders.find(o=>request.oid?o.oid===request.oid:o.cloid===request.cloid);if(!original) throw new AgentCliError('ORDER_NOT_FOUND','Resting order absent'); + const market=await clients.market(original.coin);const size=roundSize(request.size??original.sz,market.szDecimals);const price=roundPrice(request.price??original.limitPx,market.szDecimals); + const state=await clients.state();const leverage=state.assetPositions.find(p=>p.position.coin===original.coin)?.position.leverage.value??1; + await checkRisk(config,{coin:original.coin,size,price,side:original.side==='B'?'buy':'sell',leverage},clients,original.oid); + modifies.push({oid:request.cloid??original.oid,order:{a:market.asset,b:original.side==='B',p:price,s:size,r:false,t:{limit:{tif:'Gtc'}},...(original.cloid?{c:original.cloid}:{})}}); + } + return exchangeAction('batchModify',()=>clients.exchange.batchModify({modifies})); +} +register('modify',{description:'Modify price/size of single or batch resting orders',schema:modifySchema,execute:async(input,context)=>modifyOrders(await commandConfig(context),input as z.infer)}); diff --git a/test/modify.test.ts b/test/modify.test.ts new file mode 100644 index 0000000..f898177 --- /dev/null +++ b/test/modify.test.ts @@ -0,0 +1,9 @@ +import {expect,test} from 'vitest'; +import {modifyOrders,modifySchema} from '../src/commands/modify.js'; +import {mockTrading} from './order.test.js'; +import {migrate} from '../src/core/config.js'; +test('modify rounds replacements and sends batch only after validation',async()=>{ + const config=migrate({approved:true,masterAddress:'0x'+'1'.repeat(40),agentExpiry:Date.now()+10000});const services=mockTrading();services.orders=async()=>[{coin:'BTC',oid:1,sz:'0.001',limitPx:'20000',side:'B'}]; + await modifyOrders(config,modifySchema.parse({modifications:[{oid:1,price:'20000.5',size:'0.0012345'}]}),services);expect(services.exchange.batchModify).toHaveBeenCalledWith({modifies:[{oid:1,order:{a:0,b:true,p:'20000',s:'0.00123',r:false,t:{limit:{tif:'Gtc'}}}}]}); + await expect(modifyOrders(config,modifySchema.parse({oid:4}),services)).rejects.toMatchObject({code:'ORDER_NOT_FOUND'}); +}); From d58af369f59d6811635160fdebe0cee467aecb2e Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:06:38 -0700 Subject: [PATCH 25/43] feat: add validated leverage and margin mode updates (#25) --- docs/PROGRESS.md | 1 + src/cli.ts | 2 ++ src/commands/leverage.ts | 13 +++++++++++++ test/leverage.test.ts | 8 ++++++++ 4 files changed, 24 insertions(+) create mode 100644 src/commands/leverage.ts create mode 100644 test/leverage.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 237b561..22b9580 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -27,3 +27,4 @@ | #22 | done | Limit Gtc/Ioc/Alo, slippage IOC market, TP/SL, rounding, cloid, risk/deposit/expiry gates, builder and Privy signing; runner routing in #27. | | #23 | done | Batch cancellation by oid/cloid, coin or account; unknown order normalization and mocked tests. | | #24 | done | Single/batch modification by oid/cloid with rounding and pre-signing risk checks. | +| #25 | done | Cross/isolated leverage updates enforce market and configured ceilings with mocked tests. | diff --git a/src/cli.ts b/src/cli.ts index 67b4daa..1faeef9 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -7,6 +7,8 @@ import './commands/deposit.js'; import './commands/read.js'; import './commands/order.js'; import './commands/cancel.js'; +import './commands/modify.js'; +import './commands/leverage.js'; let stdin=''; if (!process.stdin.isTTY) for await (const chunk of process.stdin) stdin += String(chunk); const result=await dispatch(process.argv.slice(2),stdin); diff --git a/src/commands/leverage.ts b/src/commands/leverage.ts new file mode 100644 index 0000000..0948513 --- /dev/null +++ b/src/commands/leverage.ts @@ -0,0 +1,13 @@ +import {z} from 'zod'; +import {register} from '../core/router.js'; +import {commandConfig} from './read.js'; +import type {Config} from '../core/config.js'; +import {assertCanTrade,tradingServices,exchangeAction,type TradingServices} from '../core/trading.js'; +import {AgentCliError} from '../errors/index.js'; +export const leverageSchema=z.object({coin:z.string(),leverage:z.number().int().positive(),mode:z.enum(['cross','isolated']).default('cross'),intent:z.string().optional()}); +export async function setLeverage(config:Config,input:z.infer,services?:TradingServices){ + await assertCanTrade(config);const clients=services??await tradingServices(config);const market=await clients.market(input.coin); + if((config.allowedMarkets.length&&!config.allowedMarkets.includes(input.coin))||input.leverage>Math.min(market.maxLeverage,config.limits.maxLeverage??Infinity)) throw new AgentCliError('POLICY_REJECTED','Leverage or market exceeds configured limits'); + return exchangeAction('updateLeverage',()=>clients.exchange.updateLeverage({asset:market.asset,isCross:input.mode==='cross',leverage:input.leverage})); +} +register('leverage',{description:'Set market leverage and cross/isolated margin mode',schema:leverageSchema,execute:async(input,context)=>setLeverage(await commandConfig(context),input as z.infer)}); diff --git a/test/leverage.test.ts b/test/leverage.test.ts new file mode 100644 index 0000000..50fe394 --- /dev/null +++ b/test/leverage.test.ts @@ -0,0 +1,8 @@ +import {expect,test} from 'vitest'; +import {setLeverage,leverageSchema} from '../src/commands/leverage.js'; +import {mockTrading} from './order.test.js'; +import {migrate} from '../src/core/config.js'; +test('cross/isolated leverage enforces both ceilings',async()=>{ + const config=migrate({approved:true,masterAddress:'0x'+'1'.repeat(40),agentExpiry:Date.now()+10000,limits:{maxLeverage:5}});const services=mockTrading(); + await setLeverage(config,leverageSchema.parse({coin:'BTC',leverage:5,mode:'isolated'}),services);expect(services.exchange.updateLeverage).toHaveBeenCalledWith({asset:0,isCross:false,leverage:5});await expect(setLeverage(config,leverageSchema.parse({coin:'BTC',leverage:6}),services)).rejects.toMatchObject({code:'POLICY_REJECTED'}); +}); From d861988958007180e2b37e440108b122e63c21be Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:14:39 -0700 Subject: [PATCH 26/43] feat: add persistent runner lifecycle (#26) --- docs/PROGRESS.md | 1 + src/cli.ts | 1 + src/commands/run.ts | 8 ++++++++ src/runner/lifecycle.ts | 32 ++++++++++++++++++++++++++++++++ test/runner.test.ts | 11 +++++++++++ 5 files changed, 53 insertions(+) create mode 100644 src/commands/run.ts create mode 100644 src/runner/lifecycle.ts create mode 100644 test/runner.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 22b9580..1b47d99 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -28,3 +28,4 @@ | #23 | done | Batch cancellation by oid/cloid, coin or account; unknown order normalization and mocked tests. | | #24 | done | Single/batch modification by oid/cloid with rounding and pre-signing risk checks. | | #25 | done | Cross/isolated leverage updates enforce market and configured ceilings with mocked tests. | +| #26 | done | Private exclusive PID lock, managed signer session, foreground/detach, idempotency and graceful shutdown; persistent stores survive restart. Stale locks fail closed. | diff --git a/src/cli.ts b/src/cli.ts index 1faeef9..a67bc43 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -9,6 +9,7 @@ import './commands/order.js'; import './commands/cancel.js'; import './commands/modify.js'; import './commands/leverage.js'; +import './commands/run.js'; let stdin=''; if (!process.stdin.isTTY) for await (const chunk of process.stdin) stdin += String(chunk); const result=await dispatch(process.argv.slice(2),stdin); diff --git a/src/commands/run.ts b/src/commands/run.ts new file mode 100644 index 0000000..e3fe60c --- /dev/null +++ b/src/commands/run.ts @@ -0,0 +1,8 @@ +import {z} from 'zod'; +import {register} from '../core/router.js'; +import {commandConfig} from './read.js'; +import {Runner,detachRunner,setActiveRunner} from '../runner/lifecycle.js'; +register('run',{description:'Start persistent signing runner (foreground or detach)',schema:z.object({detach:z.boolean().default(false),daemon:z.boolean().default(false)}),execute:async(input,context)=>{ + if(input.detach||input.daemon) return detachRunner([...(context.configPath?['--config',context.configPath]:[]),...(context.network?['--network',context.network]:[])]); + const runner=new Runner(await commandConfig(context));const info=await runner.start();if(!info.existing){setActiveRunner(runner);runner.signals();}return info; +}}); diff --git a/src/runner/lifecycle.ts b/src/runner/lifecycle.ts new file mode 100644 index 0000000..27498a5 --- /dev/null +++ b/src/runner/lifecycle.ts @@ -0,0 +1,32 @@ +import {open,readFile,unlink,mkdir} from 'node:fs/promises'; +import {join} from 'node:path'; +import {spawn} from 'node:child_process'; +import {stateDir,type Config} from '../core/config.js'; +import {Logger} from '../core/logger.js'; +import {privyClient,privySigner} from '../wallet/privy.js'; +import type {Signer} from '../core/hlclient.js'; +import {AgentCliError} from '../errors/index.js'; +export interface RunnerInfo {pid:number;startedAt:number;network:Config['network'];} +export const runnerPidPath=()=>join(stateDir(),'runner.pid'); +export function isAlive(pid:number){try{process.kill(pid,0);return true;}catch(error){return (error as NodeJS.ErrnoException).code==='EPERM';}} +export async function runningInfo(path=runnerPidPath()):Promise{try{const info=JSON.parse(await readFile(path,'utf8')) as RunnerInfo;return Number.isInteger(info.pid)&&info.pid>0&&isAlive(info.pid)?info:null;}catch(error){if((error as NodeJS.ErrnoException).code==='ENOENT'||error instanceof SyntaxError) return null;throw error;}} +export class Runner { + readonly info:RunnerInfo;readonly logger:Logger;signer?:Signer;private timer?:NodeJS.Timeout;private cleanup:(()=>Promise)[]=[];private closing=false; + constructor(readonly config:Config,readonly directory=stateDir()){this.info={pid:process.pid,startedAt:Date.now(),network:config.network};this.logger=new Logger({level:config.logLevel,directory:join(directory,'logs')});} + async start(openSigner:()=>Promise=async()=>privySigner(this.config,await privyClient(this.config))){ + await mkdir(this.directory,{recursive:true,mode:0o700});const path=join(this.directory,'runner.pid'); + let handle;try{handle=await open(path,'wx',0o600);}catch(error){if((error as NodeJS.ErrnoException).code!=='EEXIST') throw error;const existing=await runningInfo(path);if(existing) return {existing:true,...existing};throw new AgentCliError('RUNNER_NOT_RUNNING','Stale runner lock; inspect and remove runner.pid before restart');} + try{await handle.writeFile(JSON.stringify(this.info));await handle.close();this.signer=await openSigner();this.timer=setInterval(()=>{},30_000);this.logger.log('info','Runner started',{...this.info});await this.logger.flush();return {existing:false,...this.info};}catch(error){await handle.close().catch(()=>{});await unlink(path).catch(()=>{});throw error;} + } + onShutdown(cleanup:()=>Promise){this.cleanup.push(cleanup);} + async shutdown(){if(this.closing) return;this.closing=true;if(this.timer) clearInterval(this.timer);for(const cleanup of this.cleanup.reverse()) await cleanup();this.signer=undefined;this.logger.log('info','Runner stopped');await this.logger.flush();await unlink(join(this.directory,'runner.pid')).catch(error=>{if((error as NodeJS.ErrnoException).code!=='ENOENT') throw error;});} + signals(){const handler=()=>{void this.shutdown().catch(error=>{process.stderr.write(String(error)+'\n');process.exitCode=1;});};process.once('SIGINT',handler);process.once('SIGTERM',handler);this.onShutdown(async()=>{process.off('SIGINT',handler);process.off('SIGTERM',handler);});} +} +export async function detachRunner(args:string[],path=runnerPidPath()){ + const existing=await runningInfo(path);if(existing) return existing; + const child=spawn(process.execPath,[process.argv[1],'run',...args],{detached:true,stdio:'ignore',env:process.env});child.unref(); + for(let i=0;i<100;i++){const info=await runningInfo(path);if(info) return info;await new Promise(resolve=>setTimeout(resolve,50));} + throw new AgentCliError('RUNNER_NOT_RUNNING','Detached runner did not become ready'); +} +export let activeRunner:Runner|undefined; +export function setActiveRunner(runner:Runner|undefined){activeRunner=runner;} diff --git a/test/runner.test.ts b/test/runner.test.ts new file mode 100644 index 0000000..1311d15 --- /dev/null +++ b/test/runner.test.ts @@ -0,0 +1,11 @@ +import {expect,test} from 'vitest'; +import {mkdtemp,rm,readFile} from 'node:fs/promises'; +import {join} from 'node:path'; +import {tmpdir} from 'node:os'; +import {Runner,runningInfo} from '../src/runner/lifecycle.js'; +import {migrate} from '../src/core/config.js'; +test('single instance and graceful cleanup preserve external state',async()=>{ + const dir=await mkdtemp(join(tmpdir(),'bloxwap-runner-'));const signer={address:'0x'+'a'.repeat(40) as `0x${string}`,signTypedData:async()=>'0x123' as `0x${string}`}; + const first=new Runner(migrate({}),dir);const second=new Runner(migrate({}),dir); + try{expect((await first.start(async()=>signer)).existing).toBe(false);expect((await second.start(async()=>signer)).existing).toBe(true);expect(await runningInfo(join(dir,'runner.pid'))).toMatchObject({pid:process.pid});await first.shutdown();expect(await runningInfo(join(dir,'runner.pid'))).toBeNull();expect(await readFile(join(dir,'logs','runner.jsonl'),'utf8')).toContain('Runner stopped');}finally{await first.shutdown();await rm(dir,{recursive:true,force:true});} +}); From f584a9a3bf95a792e72d5f00060bc9bbe40d260f Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:20:26 -0700 Subject: [PATCH 27/43] feat: route CLI commands through private runner RPC (#27) --- docs/PROGRESS.md | 1 + src/cli.ts | 4 +++- src/commands/run.ts | 3 ++- src/core/router.ts | 7 +++++-- src/core/trading.ts | 3 ++- src/runner/rpc.ts | 49 +++++++++++++++++++++++++++++++++++++++++++++ test/rpc.test.ts | 13 ++++++++++++ 7 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 src/runner/rpc.ts create mode 100644 test/rpc.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 1b47d99..d3272f2 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -29,3 +29,4 @@ | #24 | done | Single/batch modification by oid/cloid with rounding and pre-signing risk checks. | | #25 | done | Cross/isolated leverage updates enforce market and configured ceilings with mocked tests. | | #26 | done | Private exclusive PID lock, managed signer session, foreground/detach, idempotency and graceful shutdown; persistent stores survive restart. Stale locks fail closed. | +| #27 | done | Bounded newline JSON-RPC socket with private mode, method aliases, serialized trading, normalized errors and configurable direct fallback. | diff --git a/src/cli.ts b/src/cli.ts index a67bc43..78d44ff 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { dispatch } from './core/router.js'; +import { dispatch,setRouting } from './core/router.js'; import './commands/config.js'; import './commands/init.js'; import './commands/fund.js'; @@ -10,6 +10,8 @@ import './commands/cancel.js'; import './commands/modify.js'; import './commands/leverage.js'; import './commands/run.js'; +import {routeToRunner} from './runner/rpc.js'; +setRouting(routeToRunner); let stdin=''; if (!process.stdin.isTTY) for await (const chunk of process.stdin) stdin += String(chunk); const result=await dispatch(process.argv.slice(2),stdin); diff --git a/src/commands/run.ts b/src/commands/run.ts index e3fe60c..9da636b 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -1,8 +1,9 @@ +import {rpcServer} from '../runner/rpc.js'; import {z} from 'zod'; import {register} from '../core/router.js'; import {commandConfig} from './read.js'; import {Runner,detachRunner,setActiveRunner} from '../runner/lifecycle.js'; register('run',{description:'Start persistent signing runner (foreground or detach)',schema:z.object({detach:z.boolean().default(false),daemon:z.boolean().default(false)}),execute:async(input,context)=>{ if(input.detach||input.daemon) return detachRunner([...(context.configPath?['--config',context.configPath]:[]),...(context.network?['--network',context.network]:[])]); - const runner=new Runner(await commandConfig(context));const info=await runner.start();if(!info.existing){setActiveRunner(runner);runner.signals();}return info; + const runner=new Runner(await commandConfig(context));const info=await runner.start();if(!info.existing){setActiveRunner(runner);runner.signals();try{await rpcServer(runner,context);}catch(error){await runner.shutdown();throw error;}}return info; }}); diff --git a/src/core/router.ts b/src/core/router.ts index 5600648..d6eb621 100644 --- a/src/core/router.ts +++ b/src/core/router.ts @@ -1,7 +1,9 @@ import { z } from 'zod'; import { AgentCliError, normalizeError, success } from '../errors/index.js'; -export interface Context { configPath?: string; network?: 'mainnet' | 'testnet'; pretty: boolean; } +export interface Context { configPath?: string; network?: 'mainnet' | 'testnet'; pretty: boolean; runner?: boolean; } export interface Command { description: string; schema: z.ZodType; execute: (input: Record, context: Context) => Promise; } +export let routing:((name:string,input:Record,context:Context)=>Promise<{routed:boolean;data?:unknown}>)|undefined; +export function setRouting(value:typeof routing){routing=value;} export const commands = new Map(); export function register(name: string, command: Command) { commands.set(name, command); } export function schemas(name: string) { @@ -37,7 +39,8 @@ export async function dispatch(argv: string[], stdin = '') { if (args.length) input.args=args; const parsed=command.schema.safeParse(input); if (!parsed.success) throw new AgentCliError('CONFIG_INVALID','Invalid command input',parsed.error.flatten()); - const data=await command.execute(parsed.data as Record,context); + const route=await routing?.(name,parsed.data as Record,context); + const data=route?.routed?route.data:await command.execute(parsed.data as Record,context); return {body:success(data),exitCode:0,context}; } catch (error) { const normalized=error instanceof z.ZodError || error instanceof SyntaxError ? new AgentCliError('CONFIG_INVALID','Invalid JSON or flags') : normalizeError(error); diff --git a/src/core/trading.ts b/src/core/trading.ts index 6e77961..8fe6aab 100644 --- a/src/core/trading.ts +++ b/src/core/trading.ts @@ -1,3 +1,4 @@ +import {activeRunner} from '../runner/lifecycle.js'; import {randomUUID} from 'node:crypto'; import {type Config} from './config.js'; import {createClients,type ExchangeAdapter,type Market} from './hlclient.js'; @@ -13,7 +14,7 @@ import {evaluateRisk,type RiskIntent} from './risk.js'; import {Logger} from './logger.js'; export interface TradingServices {exchange:ExchangeAdapter;market:(coin:string)=>Promise;state:()=>Promise;orders:()=>Promise;mids:()=>Promise>;} export async function tradingServices(config:Config):Promise{ - const signer=await privySigner(config,await privyClient(config));const clients=createClients(config.network,signer,()=>nextNonce(signer.address)); + const signer=activeRunner?.signer??await privySigner(config,await privyClient(config));const clients=createClients(config.network,signer,()=>nextNonce(signer.address)); return {exchange:clients.exchange!,market:coin=>clients.meta.lookup(coin),state:()=>hlInfo(config.network,'clearinghouseState',{user:config.masterAddress}),orders:()=>hlInfo(config.network,'frontendOpenOrders',{user:config.masterAddress}),mids:()=>hlInfo(config.network,'allMids')}; } export async function assertCanTrade(config:Config){ diff --git a/src/runner/rpc.ts b/src/runner/rpc.ts new file mode 100644 index 0000000..4567628 --- /dev/null +++ b/src/runner/rpc.ts @@ -0,0 +1,49 @@ +import {createServer,createConnection,type Server} from 'node:net'; +import {chmod,unlink} from 'node:fs/promises'; +import {join} from 'node:path'; +import {createHash} from 'node:crypto'; +import {z} from 'zod'; +import {stateDir,loadConfig} from '../core/config.js'; +import {AgentCliError,normalizeError} from '../errors/index.js'; +import {commands,type Context} from '../core/router.js'; +import type {Runner} from './lifecycle.js'; +export const socketPath=(directory=stateDir())=>process.platform==='win32'?`\\\\.\\pipe\\bloxwap-${createHash('sha256').update(directory).digest('hex').slice(0,24)}`:join(directory,'run.sock'); +const aliases:Record={'status.get':'status','balance.get':'balance','positions.get':'positions','orders.get':'orders','order.place':'order','order.cancel':'cancel','order.modify':'modify','leverage.set':'leverage','config.get':'config','runner.shutdown':'shutdown'}; +export const routedCommands=new Set(['status','balance','positions','orders','order','cancel','modify','leverage']); +const requestSchema=z.object({jsonrpc:z.literal('2.0'),id:z.union([z.string(),z.number(),z.null()]),method:z.string(),params:z.record(z.string(),z.unknown()).default({})}); +export async function rpcServer(runner:Runner,context:Context,execute?: (name:string,input:Record)=>Promise):Promise{ + const path=socketPath(runner.directory);let queue=Promise.resolve(); + const server=createServer(socket=>{ + let buffer='';socket.setEncoding('utf8');socket.setTimeout(30_000,()=>socket.destroy()); + socket.on('data',chunk=>{ + buffer+=chunk;if(Buffer.byteLength(buffer)>1_048_576){socket.destroy();return;} + let end:number;while((end=buffer.indexOf('\n'))>=0){const line=buffer.slice(0,end);buffer=buffer.slice(end+1);queue=queue.then(async()=>{ + let id:unknown=null; + try{const request=requestSchema.parse(JSON.parse(line));id=request.id;const name=aliases[request.method]??request.method; + if(name==='shutdown'){socket.write(JSON.stringify({jsonrpc:'2.0',id,result:{stopping:true}})+'\n');setImmediate(()=>{void runner.shutdown();});return;} + if(!routedCommands.has(name)&&name!=='config') throw new AgentCliError('CONFIG_INVALID','Unknown runner method'); + const command=commands.get(name);if(!command&&!execute) throw new AgentCliError('CONFIG_INVALID','Unavailable method'); + const input=name==='config'?{args:['get']}:request.params;const parsed=command?.schema.safeParse(input);if(parsed&&!parsed.success) throw new AgentCliError('CONFIG_INVALID','Invalid runner input'); + const data=execute?await execute(name,input):await command!.execute(parsed!.data as Record,{...context,runner:true}); + socket.write(JSON.stringify({jsonrpc:'2.0',id,result:data})+'\n'); + }catch(error){const normalized=error instanceof SyntaxError||error instanceof z.ZodError?new AgentCliError('CONFIG_INVALID','Invalid JSON-RPC request'):normalizeError(error);socket.write(JSON.stringify({jsonrpc:'2.0',id,error:{code:-32000,message:normalized.message,data:normalized.toJSON()}})+'\n');} + }).catch(error=>{runner.logger.log('error','RPC write failed',{error:String(error)});socket.destroy();});} + });socket.on('error',()=>{}); + }); + await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(path,()=>{server.off('error',reject);resolve();});}); + if(process.platform!=='win32') await chmod(path,0o600); + runner.onShutdown(async()=>{await queue;await new Promise(resolve=>server.close(()=>resolve()));if(process.platform!=='win32') await unlink(path).catch(()=>{});});return server; +} +export function rpcCall(method:string,params:Record={},path=socketPath()):Promise{ + return new Promise((resolve,reject)=>{const id=1;const socket=createConnection(path);let buffer='';socket.setEncoding('utf8');socket.setTimeout(15_000,()=>socket.destroy(new Error('Runner timeout'))); + socket.once('connect',()=>socket.write(JSON.stringify({jsonrpc:'2.0',id,method,params})+'\n')); + socket.on('data',chunk=>{buffer+=chunk;if(buffer.length>1_048_576){socket.destroy(new Error('Oversize runner response'));return;}if(!buffer.includes('\n')) return; + try{const response=JSON.parse(buffer.slice(0,buffer.indexOf('\n')));if(response.jsonrpc!=='2.0'||response.id!==id) throw new Error('Invalid runner response');if(response.error){const error=response.error.data;reject(new AgentCliError(error.code,error.message,error.details,error.retryable));}else resolve(response.result);socket.end();}catch(error){reject(error);socket.destroy();}}); + socket.once('error',error=>reject(new AgentCliError('RUNNER_NOT_RUNNING',error.message)));socket.once('end',()=>{if(!buffer.includes('\n')) reject(new AgentCliError('RUNNER_NOT_RUNNING','Runner disconnected'));}); + }); +} +export async function routeToRunner(name:string,input:Record,context:Context){ + if(context.runner||!routedCommands.has(name)) return {routed:false as const}; + const config=await loadConfig(context.configPath); + try{return {routed:true as const,data:await rpcCall(name,{...input,...(context.network?{network:context.network}:{})})};}catch(error){if(error instanceof AgentCliError&&error.code==='RUNNER_NOT_RUNNING'&&config.runnerFallback) return {routed:false as const};throw error;} +} diff --git a/test/rpc.test.ts b/test/rpc.test.ts new file mode 100644 index 0000000..77ded29 --- /dev/null +++ b/test/rpc.test.ts @@ -0,0 +1,13 @@ +import {expect,test} from 'vitest'; +import {mkdtemp,rm,stat} from 'node:fs/promises'; +import {tmpdir} from 'node:os'; +import {join} from 'node:path'; +import {Runner} from '../src/runner/lifecycle.js'; +import {rpcServer,rpcCall,socketPath} from '../src/runner/rpc.js'; +import {migrate} from '../src/core/config.js'; +test('private RPC framing, multi-client serialization and normalized errors',async()=>{ + const dir=await mkdtemp(join(tmpdir(),'bloxwap-rpc-'));const runner=new Runner(migrate({}),dir); + try{await rpcServer(runner,{pretty:false},async(name,input)=>({name,input}));const path=socketPath(dir);if(process.platform!=='win32') expect((await stat(path)).mode&0o777).toBe(0o600); + expect(await Promise.all([rpcCall('status.get',{},path),rpcCall('balance',{},path)])).toEqual([{name:'status',input:{}},{name:'balance',input:{}}]);await expect(rpcCall('withdraw',{},path)).rejects.toMatchObject({code:'CONFIG_INVALID'}); + }finally{await runner.shutdown();await rm(dir,{recursive:true,force:true});} +}); From 52b143c6393ac18332c3d39b9905207fb9a79591 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:21:24 -0700 Subject: [PATCH 28/43] feat: maintain reconnecting Hyperliquid subscriptions (#28) --- docs/PROGRESS.md | 1 + package-lock.json | 31 +++++++++++++++++++++++++++++++ package.json | 2 ++ src/commands/run.ts | 3 ++- src/runner/subscriptions.ts | 37 +++++++++++++++++++++++++++++++++++++ test/subscriptions.test.ts | 12 ++++++++++++ 6 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/runner/subscriptions.ts create mode 100644 test/subscriptions.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index d3272f2..87f17bd 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -30,3 +30,4 @@ | #25 | done | Cross/isolated leverage updates enforce market and configured ceilings with mocked tests. | | #26 | done | Private exclusive PID lock, managed signer session, foreground/detach, idempotency and graceful shutdown; persistent stores survive restart. Stale locks fail closed. | | #27 | done | Bounded newline JSON-RPC socket with private mode, method aliases, serialized trading, normalized errors and configurable direct fallback. | +| #28 | done | SDK reconnect/resubscribe + heartbeat transport, user/market subscriptions, info gap-fill and persisted tid dedupe; simulated drop tests. | diff --git a/package-lock.json b/package-lock.json index 1512062..91125d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@bloxwap/hyperliquid": "^0.6.0", "@privy-io/node": "^0.35.0", "qrcode": "^1.5.4", + "ws": "^8.22.0", "zod": "^4.6.7" }, "bin": { @@ -19,6 +20,7 @@ "devDependencies": { "@types/node": "^26.6.5", "@types/qrcode": "^1.5.6", + "@types/ws": "^8.18.2", "eslint": "^9.39.5", "prettier": "^3.9.10", "tsup": "^8.5.1", @@ -1209,6 +1211,15 @@ "@types/node": "*" } }, + "node_modules/@types/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.71.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.2.tgz", @@ -3597,6 +3608,26 @@ "node": ">=8" } }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", diff --git a/package.json b/package.json index 658cc03..c60413d 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "devDependencies": { "@types/node": "^26.6.5", "@types/qrcode": "^1.5.6", + "@types/ws": "^8.18.2", "eslint": "^9.39.5", "prettier": "^3.9.10", "tsup": "^8.5.1", @@ -33,6 +34,7 @@ "@bloxwap/hyperliquid": "^0.6.0", "@privy-io/node": "^0.35.0", "qrcode": "^1.5.4", + "ws": "^8.22.0", "zod": "^4.6.7" } } diff --git a/src/commands/run.ts b/src/commands/run.ts index 9da636b..93a6f19 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -1,3 +1,4 @@ +import {startSubscriptions} from '../runner/subscriptions.js'; import {rpcServer} from '../runner/rpc.js'; import {z} from 'zod'; import {register} from '../core/router.js'; @@ -5,5 +6,5 @@ import {commandConfig} from './read.js'; import {Runner,detachRunner,setActiveRunner} from '../runner/lifecycle.js'; register('run',{description:'Start persistent signing runner (foreground or detach)',schema:z.object({detach:z.boolean().default(false),daemon:z.boolean().default(false)}),execute:async(input,context)=>{ if(input.detach||input.daemon) return detachRunner([...(context.configPath?['--config',context.configPath]:[]),...(context.network?['--network',context.network]:[])]); - const runner=new Runner(await commandConfig(context));const info=await runner.start();if(!info.existing){setActiveRunner(runner);runner.signals();try{await rpcServer(runner,context);}catch(error){await runner.shutdown();throw error;}}return info; + const runner=new Runner(await commandConfig(context));const info=await runner.start();if(!info.existing){setActiveRunner(runner);runner.signals();try{await rpcServer(runner,context);await startSubscriptions(runner,async(type,data)=>{runner.logger.log(type==='error'?'error':'info',type,{data});});}catch(error){await runner.shutdown();throw error;}}return info; }}); diff --git a/src/runner/subscriptions.ts b/src/runner/subscriptions.ts new file mode 100644 index 0000000..d6c9200 --- /dev/null +++ b/src/runner/subscriptions.ts @@ -0,0 +1,37 @@ +import {SubscriptionClient,WebSocketTransport} from '@bloxwap/hyperliquid'; +import WebSocket from 'ws'; +import {readFile} from 'node:fs/promises'; +import {join} from 'node:path'; +import {atomicWrite,type Config} from '../core/config.js'; +import {hlInfo} from '../core/http.js'; +import {normalizeError} from '../errors/index.js'; +import type {Runner} from './lifecycle.js'; +interface Fill {tid:number;time:number;[key:string]:unknown;} +interface Backend {subscribe:(type:string,params:Record,listener:(data:unknown)=>void)=>Promise<{unsubscribe:()=>Promise}>;onState:(listener:(state:string)=>void)=>void;close:()=>Promise;gap:()=>Promise;} +export function subscriptionSet(config:Config){return ['userEvents','userFills','orderUpdates','clearinghouseState',...(config.allowedMarkets.length?['allMids',...config.allowedMarkets.map(coin=>`l2Book:${coin}`)]:[])];} +export class Subscriptions { + state='connecting';lastMessage=Date.now();lastFill:number|null=null;private seen=new Set();private queue=Promise.resolve();private subscriptions:{unsubscribe:()=>Promise}[]=[]; + constructor(private runner:Runner,private backend:Backend,private emit:(type:string,data:unknown)=>Promise){} + async start(){ + const path=join(this.runner.directory,'subscriptions.json');try{const saved=JSON.parse(await readFile(path,'utf8'));this.seen=new Set(saved.seen);this.lastFill=saved.lastFill;}catch(error){if((error as NodeJS.ErrnoException).code!=='ENOENT') throw error;} + this.backend.onState(state=>{const reconnect=this.state!=='connecting'&&state==='connected';this.state=state;this.lastMessage=Date.now();if(reconnect) this.enqueue(async()=>{for(const fill of await this.backend.gap()) await this.fill(fill);await this.emit('runner_state',{wsState:state});});}); + for(const name of subscriptionSet(this.runner.config)){ + const [type,coin]=name.split(':');const params=coin?{coin}:type==='allMids'?{}:{user:this.runner.config.masterAddress}; + this.subscriptions.push(await this.backend.subscribe(type,params,data=>{this.lastMessage=Date.now();this.enqueue(async()=>{ + if(type==='userFills'){for(const fill of (data as {fills:Fill[]}).fills) await this.fill(fill);} + else if(type==='userEvents'&&'fills' in (data as object)){for(const fill of (data as {fills:Fill[]}).fills) await this.fill(fill);} + else if(type==='orderUpdates') await this.emit('order_update',data);else if(type==='clearinghouseState') await this.emit('position_change',data); + });})); + } + this.runner.onShutdown(async()=>{await this.queue;await Promise.all(this.subscriptions.map(s=>s.unsubscribe()));await this.backend.close();}); + } + private enqueue(operation:()=>Promise){this.queue=this.queue.then(operation).catch(async error=>{await this.emit('error',normalizeError(error).toJSON());});} + async fill(fill:Fill){if(this.seen.has(fill.tid)) return;this.seen.add(fill.tid);this.lastFill=fill.time;await this.emit('fill',fill);await atomicWrite(join(this.runner.directory,'subscriptions.json'),JSON.stringify({seen:[...this.seen].slice(-10000),lastFill:this.lastFill}));} + async flush(){await this.queue;} +} +export function sdkBackend(config:Config):Backend { + const transport=new WebSocketTransport({isTestnet:config.network==='testnet',resubscribe:true,webSocketFactory:(url,protocols)=>new WebSocket(url,protocols) as unknown as globalThis.WebSocket}); + const client=new SubscriptionClient({transport}) as unknown as Record,listener:(data:unknown)=>void)=>Promise<{unsubscribe:()=>Promise}>>; + return {subscribe:(type,params,listener)=>client[type](params,listener),onState:listener=>transport.events.addEventListener('connectionstatechange',event=>listener(event.detail)),close:async()=>{await transport.close();},gap:()=>hlInfo(config.network,'userFills',{user:config.masterAddress})}; +} +export async function startSubscriptions(runner:Runner,emit:(type:string,data:unknown)=>Promise){const subscriptions=new Subscriptions(runner,sdkBackend(runner.config),emit);await subscriptions.start();return subscriptions;} diff --git a/test/subscriptions.test.ts b/test/subscriptions.test.ts new file mode 100644 index 0000000..694510e --- /dev/null +++ b/test/subscriptions.test.ts @@ -0,0 +1,12 @@ +import {expect,test} from 'vitest'; +import {mkdtemp,rm} from 'node:fs/promises'; +import {tmpdir} from 'node:os'; +import {join} from 'node:path'; +import {Runner} from '../src/runner/lifecycle.js'; +import {Subscriptions,subscriptionSet} from '../src/runner/subscriptions.js'; +import {migrate} from '../src/core/config.js'; +test('reconnect gap-fill dedupes fills and persists subscription cursor',async()=>{ + const dir=await mkdtemp(join(tmpdir(),'bloxwap-ws-'));const runner=new Runner(migrate({allowedMarkets:['BTC']}),dir);const events:unknown[]=[];let state:(value:string)=>void=()=>{}; + const backend={subscribe:async()=>({unsubscribe:async()=>{}}),onState:(listener:typeof state)=>{state=listener;},close:async()=>{},gap:async()=>[{tid:1,time:1},{tid:2,time:2}]};const manager=new Subscriptions(runner,backend,async(type,data)=>{events.push({type,data});}); + try{await manager.start();await manager.fill({tid:1,time:1});state('connected');state('reconnecting');state('connected');await manager.flush();expect(events.filter(e=>(e as {type:string}).type==='fill')).toHaveLength(2);expect(subscriptionSet(runner.config)).toContain('l2Book:BTC');await runner.shutdown();}finally{await rm(dir,{recursive:true,force:true});} +}); From 4a9488f9d30050310b0aea984b6384e1b44ad7d9 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:29:36 -0700 Subject: [PATCH 29/43] feat: add durable JSON-line event stream (#29) --- docs/PROGRESS.md | 1 + docs/events.md | 11 +++++++++++ src/cli.ts | 3 ++- src/commands/events.ts | 8 ++++++++ src/commands/read.ts | 4 +++- src/commands/run.ts | 3 ++- src/core/router.ts | 2 +- src/runner/events.ts | 28 ++++++++++++++++++++++++++++ test/events.test.ts | 9 +++++++++ 9 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 docs/events.md create mode 100644 src/commands/events.ts create mode 100644 src/runner/events.ts create mode 100644 test/events.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 87f17bd..a62ee2b 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -31,3 +31,4 @@ | #26 | done | Private exclusive PID lock, managed signer session, foreground/detach, idempotency and graceful shutdown; persistent stores survive restart. Stale locks fail closed. | | #27 | done | Bounded newline JSON-RPC socket with private mode, method aliases, serialized trading, normalized errors and configurable direct fallback. | | #28 | done | SDK reconnect/resubscribe + heartbeat transport, user/market subscriptions, info gap-fill and persisted tid dedupe; simulated drop tests. | +| #29 | done | Typed persisted monotonic event journal, replay/filter/follow stream, subscription/deposit emission and shutdown flush. | diff --git a/docs/events.md b/docs/events.md new file mode 100644 index 0000000..11925d7 --- /dev/null +++ b/docs/events.md @@ -0,0 +1,11 @@ +# Events + +Each JSON line is `{seq,timestamp,type,data}`. Sequence numbers strictly increase +across restarts; timestamps are ISO UTC. Types: `fill` (upstream fill with tid), +`order_update`, `position_change`, `deposit` (id/state), `error` (normalized +failure), `agent_expiry_warning`, `runner_state`. Secret fields are redacted. + +`events --since 12` returns events after sequence 12. `--follow` is an explicit +streaming exception to the one-document command contract: each event is one JSON +line. Filter with `--types '["fill","error"]'`. Save the last processed sequence +and replay it after reconnect. Shutdown flushes the queue before releasing state. diff --git a/src/cli.ts b/src/cli.ts index 78d44ff..ab906c9 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -10,10 +10,11 @@ import './commands/cancel.js'; import './commands/modify.js'; import './commands/leverage.js'; import './commands/run.js'; +import './commands/events.js'; import {routeToRunner} from './runner/rpc.js'; setRouting(routeToRunner); let stdin=''; if (!process.stdin.isTTY) for await (const chunk of process.stdin) stdin += String(chunk); const result=await dispatch(process.argv.slice(2),stdin); -process.stdout.write(JSON.stringify(result.body,null,result.context.pretty ? 2 : undefined)+'\n'); +if(!result.context.streaming) process.stdout.write(JSON.stringify(result.body,null,result.context.pretty ? 2 : undefined)+'\n'); process.exitCode=result.exitCode; diff --git a/src/commands/events.ts b/src/commands/events.ts new file mode 100644 index 0000000..afcebdb --- /dev/null +++ b/src/commands/events.ts @@ -0,0 +1,8 @@ +import {z} from 'zod'; +import {register} from '../core/router.js'; +import {readEvents,followEvents,eventTypes,type EventType} from '../runner/events.js'; +register('events',{description:'Replay or follow JSON-line runner events',schema:z.object({follow:z.boolean().default(false),since:z.number().int().nonnegative().default(0),types:z.array(z.enum(eventTypes)).default([])}),execute:async(input,context)=>{ + if(!input.follow) return readEvents(undefined,input.since as number,input.types as EventType[]); + context.streaming=true;const abort=new AbortController();const stop=()=>abort.abort();process.once('SIGINT',stop);process.once('SIGTERM',stop); + try{await followEvents({since:input.since as number,types:input.types as EventType[],signal:abort.signal,write:event=>process.stdout.write(JSON.stringify(event)+'\n')});}finally{process.off('SIGINT',stop);process.off('SIGTERM',stop);}return null; +}}); diff --git a/src/commands/read.ts b/src/commands/read.ts index 0fb671f..cc66e3a 100644 --- a/src/commands/read.ts +++ b/src/commands/read.ts @@ -1,3 +1,5 @@ +import {events} from '../runner/events.js'; +import {activeRunner} from '../runner/lifecycle.js'; import {z} from 'zod'; import {register,type Context} from '../core/router.js'; import {loadConfig,type Config} from '../core/config.js'; @@ -17,7 +19,7 @@ export async function readCommand(name:string,config:Config){ if(name==='orders') return (await hlInfo(config.network,'frontendOpenOrders',{user:config.masterAddress})).map(order=>({...order,oid:String(order.oid)})); const state=await hlInfo(config.network,'clearinghouseState',{user:config.masterAddress}); if(name==='balance') return accountBalance(state);if(name==='positions') return positions(state); - const [agents,deposits,maxBuilderFee]=await Promise.all([hlInfo<{address:string;validUntil:number}[]>(config.network,'extraAgents',{user:config.masterAddress}),pollDeposits(config.network,config.masterAddress,state.marginSummary.accountValue),config.builder.address?hlInfo(config.network,'maxBuilderFee',{user:config.masterAddress,builder:config.builder.address}):Promise.resolve(0)]); + const [agents,deposits,maxBuilderFee]=await Promise.all([hlInfo<{address:string;validUntil:number}[]>(config.network,'extraAgents',{user:config.masterAddress}),pollDeposits(config.network,config.masterAddress,state.marginSummary.accountValue,{emit:activeRunner?async deposit=>{await events.append('deposit',{id:deposit.id,state:deposit.state});}:undefined}),config.builder.address?hlInfo(config.network,'maxBuilderFee',{user:config.masterAddress,builder:config.builder.address}):Promise.resolve(0)]); return {balance:accountBalance(state),positions:positions(state),agent:{address:config.agentAddress,approved:agents.some(a=>a.address.toLowerCase()===config.agentAddress?.toLowerCase()),revoked:config.revoked,...expiryStatus(config)},policy:{id:config.policyId??null,version:String(config.policyVersion??0)},deposits:deposits.map(d=>({id:d.id,state:d.state})),approvedMaxBuilderFee:String(maxBuilderFee),runner:null}; }catch(error){throw mapHyperliquidError(error);} } diff --git a/src/commands/run.ts b/src/commands/run.ts index 93a6f19..589589d 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -1,3 +1,4 @@ +import {events,eventTypes,type EventType} from '../runner/events.js'; import {startSubscriptions} from '../runner/subscriptions.js'; import {rpcServer} from '../runner/rpc.js'; import {z} from 'zod'; @@ -6,5 +7,5 @@ import {commandConfig} from './read.js'; import {Runner,detachRunner,setActiveRunner} from '../runner/lifecycle.js'; register('run',{description:'Start persistent signing runner (foreground or detach)',schema:z.object({detach:z.boolean().default(false),daemon:z.boolean().default(false)}),execute:async(input,context)=>{ if(input.detach||input.daemon) return detachRunner([...(context.configPath?['--config',context.configPath]:[]),...(context.network?['--network',context.network]:[])]); - const runner=new Runner(await commandConfig(context));const info=await runner.start();if(!info.existing){setActiveRunner(runner);runner.signals();try{await rpcServer(runner,context);await startSubscriptions(runner,async(type,data)=>{runner.logger.log(type==='error'?'error':'info',type,{data});});}catch(error){await runner.shutdown();throw error;}}return info; + const runner=new Runner(await commandConfig(context));const info=await runner.start();if(!info.existing){setActiveRunner(runner);runner.signals();runner.onShutdown(()=>events.flush());try{await rpcServer(runner,context);await startSubscriptions(runner,async(type,data)=>{if(eventTypes.includes(type as EventType)) await events.append(type as EventType,data);runner.logger.log(type==='error'?'error':'info',type,{data});});}catch(error){await runner.shutdown();throw error;}}return info; }}); diff --git a/src/core/router.ts b/src/core/router.ts index d6eb621..0f537c0 100644 --- a/src/core/router.ts +++ b/src/core/router.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; import { AgentCliError, normalizeError, success } from '../errors/index.js'; -export interface Context { configPath?: string; network?: 'mainnet' | 'testnet'; pretty: boolean; runner?: boolean; } +export interface Context { configPath?: string; network?: 'mainnet' | 'testnet'; pretty: boolean; runner?: boolean; streaming?:boolean; } export interface Command { description: string; schema: z.ZodType; execute: (input: Record, context: Context) => Promise; } export let routing:((name:string,input:Record,context:Context)=>Promise<{routed:boolean;data?:unknown}>)|undefined; export function setRouting(value:typeof routing){routing=value;} diff --git a/src/runner/events.ts b/src/runner/events.ts new file mode 100644 index 0000000..2beacae --- /dev/null +++ b/src/runner/events.ts @@ -0,0 +1,28 @@ +import {appendFile,mkdir,readFile,chmod} from 'node:fs/promises'; +import {dirname,join} from 'node:path'; +import {z} from 'zod'; +import {stateDir} from '../core/config.js'; +import {redact} from '../core/logger.js'; +export const eventTypes=['fill','order_update','position_change','deposit','error','agent_expiry_warning','runner_state'] as const; +export type EventType=typeof eventTypes[number]; +export const eventSchema=z.object({seq:z.number().int().positive(),timestamp:z.string(),type:z.enum(eventTypes),data:z.unknown()}); +export type RunnerEvent=z.infer; +export async function readEvents(path=join(stateDir(),'events.jsonl'),since=0,types:EventType[]=[]):Promise{ + let contents:string;try{contents=await readFile(path,'utf8');}catch(error){if((error as NodeJS.ErrnoException).code==='ENOENT') return [];throw error;} + return contents.split('\n').filter(Boolean).map(line=>eventSchema.parse(JSON.parse(line))).filter(event=>event.seq>since&&(!types.length||types.includes(event.type))); +} +export class EventStore { + private queue=Promise.resolve(); + constructor(readonly path=join(stateDir(),'events.jsonl')){} + append(type:EventType,data:unknown){let event:RunnerEvent;const operation=this.queue.then(async()=>{ + const previous=(await readEvents(this.path)).at(-1)?.seq??0;event=eventSchema.parse({seq:previous+1,timestamp:new Date().toISOString(),type,data:redact(data)}); + await mkdir(dirname(this.path),{recursive:true,mode:0o700});await appendFile(this.path,JSON.stringify(event)+'\n',{mode:0o600});await chmod(this.path,0o600);return event; + });this.queue=operation.then(()=>{});return operation; + } + async flush(){await this.queue;} +} +export async function followEvents(options:{path?:string;since?:number;types?:EventType[];signal?:AbortSignal;write:(event:RunnerEvent)=>void;interval?:number}){ + let since=options.since??0; + while(!options.signal?.aborted){for(const event of await readEvents(options.path,since,options.types)){options.write(event);since=event.seq;}await new Promise(resolve=>{const timer=setTimeout(done,options.interval??250);function done(){clearTimeout(timer);options.signal?.removeEventListener('abort',done);resolve();}options.signal?.addEventListener('abort',done,{once:true});});} +} +export const events=new EventStore(); diff --git a/test/events.test.ts b/test/events.test.ts new file mode 100644 index 0000000..2c3e7ca --- /dev/null +++ b/test/events.test.ts @@ -0,0 +1,9 @@ +import {expect,test} from 'vitest'; +import {mkdtemp,rm} from 'node:fs/promises'; +import {tmpdir} from 'node:os'; +import {join} from 'node:path'; +import {EventStore,readEvents} from '../src/runner/events.js'; +test('event replay preserves monotonic seq across restart and filters',async()=>{ + const dir=await mkdtemp(join(tmpdir(),'bloxwap-events-'));const path=join(dir,'events.jsonl'); + try{const store=new EventStore(path);await Promise.all([store.append('fill',{tid:1}),store.append('error',{ok:false,code:'NETWORK_ERROR'})]);await new EventStore(path).append('deposit',{id:'deposit'});expect((await readEvents(path)).map(e=>e.seq)).toEqual([1,2,3]);expect((await readEvents(path,1,['deposit']))).toHaveLength(1);}finally{await rm(dir,{recursive:true,force:true});} +}); From 24f068dcec4718148f0497258195f7f88c5924d7 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:30:15 -0700 Subject: [PATCH 30/43] feat: add filtered runner log tailing (#30) --- docs/PROGRESS.md | 1 + src/cli.ts | 1 + src/commands/logs.ts | 15 +++++++++++++++ test/logs.test.ts | 10 ++++++++++ 4 files changed, 27 insertions(+) create mode 100644 src/commands/logs.ts create mode 100644 test/logs.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index a62ee2b..7b8c1cf 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -32,3 +32,4 @@ | #27 | done | Bounded newline JSON-RPC socket with private mode, method aliases, serialized trading, normalized errors and configurable direct fallback. | | #28 | done | SDK reconnect/resubscribe + heartbeat transport, user/market subscriptions, info gap-fill and persisted tid dedupe; simulated drop tests. | | #29 | done | Typed persisted monotonic event journal, replay/filter/follow stream, subscription/deposit emission and shutdown flush. | +| #30 | done | JSON-line log replay/follow across rotated files, minimum-level/time/request filtering and redaction. | diff --git a/src/cli.ts b/src/cli.ts index ab906c9..c95e03d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -11,6 +11,7 @@ import './commands/modify.js'; import './commands/leverage.js'; import './commands/run.js'; import './commands/events.js'; +import './commands/logs.js'; import {routeToRunner} from './runner/rpc.js'; setRouting(routeToRunner); let stdin=''; diff --git a/src/commands/logs.ts b/src/commands/logs.ts new file mode 100644 index 0000000..2c86d63 --- /dev/null +++ b/src/commands/logs.ts @@ -0,0 +1,15 @@ +import {z} from 'zod'; +import {readdir,readFile} from 'node:fs/promises'; +import {join} from 'node:path'; +import {register} from '../core/router.js'; +import {stateDir} from '../core/config.js'; +import {levels,redact,type Level} from '../core/logger.js'; +export async function readLogs(options:{directory?:string;level?:Level;since?:string;requestId?:string}={}){ + const directory=options.directory??join(stateDir(),'logs');let files:string[];try{files=await readdir(directory);}catch(error){if((error as NodeJS.ErrnoException).code==='ENOENT') return [];throw error;} + const lines=(await Promise.all(files.filter(f=>/^runner.*\.jsonl$/.test(f)).map(f=>readFile(join(directory,f),'utf8')))).flatMap(content=>content.split('\n').filter(Boolean).map(line=>JSON.parse(line) as Record)); + return lines.filter(line=>(!options.level||levels[line.level as Level]>=levels[options.level])&&(!options.since||String(line.time)>=options.since)&&(!options.requestId||line.requestId===options.requestId)).sort((a,b)=>String(a.time).localeCompare(String(b.time))).map(redact); +} +register('logs',{description:'Read/follow redacted JSON-line runner logs',schema:z.object({follow:z.boolean().default(false),level:z.enum(['debug','info','warn','error']).optional(),since:z.string().datetime().optional(),requestId:z.string().optional()}),execute:async(input,context)=>{ + context.streaming=true;let cursor=0;const seen=new Set();const abort=new AbortController();const stop=()=>abort.abort();process.once('SIGINT',stop);process.once('SIGTERM',stop); + try{do{const logs=await readLogs(input);for(const log of logs){const key=JSON.stringify(log);if(!seen.has(key)){seen.add(key);process.stdout.write(key+'\n');cursor++;}}if(input.follow&&!abort.signal.aborted) await new Promise(resolve=>setTimeout(resolve,250));}while(input.follow&&!abort.signal.aborted);}finally{process.off('SIGINT',stop);process.off('SIGTERM',stop);}return {lines:cursor}; +}}); diff --git a/test/logs.test.ts b/test/logs.test.ts new file mode 100644 index 0000000..2cba073 --- /dev/null +++ b/test/logs.test.ts @@ -0,0 +1,10 @@ +import {expect,test} from 'vitest'; +import {mkdtemp,rm,writeFile} from 'node:fs/promises'; +import {tmpdir} from 'node:os'; +import {join} from 'node:path'; +import {readLogs} from '../src/commands/logs.js'; +test('logs filter level, date and request while preserving redaction',async()=>{ + const directory=await mkdtemp(join(tmpdir(),'bloxwap-logread-'));try{ + await writeFile(join(directory,'runner.jsonl'),[{level:'info',time:'2026-01-01',requestId:'a'},{level:'warn',time:'2026-01-02',requestId:'b',token:'sensitive'}].map(value=>JSON.stringify(value)).join('\n')+'\n'); + expect(await readLogs({directory,level:'warn',since:'2026-01-02',requestId:'b'})).toEqual([{level:'warn',time:'2026-01-02',requestId:'b',token:'[REDACTED]'}]);}finally{await rm(directory,{recursive:true,force:true});} +}); From 003ff6228247ec496ea2929d767ebda5228b8c5c Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:30:48 -0700 Subject: [PATCH 31/43] feat: expose runner heartbeat and connection health (#31) --- docs/PROGRESS.md | 1 + src/commands/read.ts | 3 ++- src/commands/run.ts | 3 ++- src/runner/health.ts | 18 ++++++++++++++++++ src/runner/lifecycle.ts | 3 ++- test/health.test.ts | 9 +++++++++ 6 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 src/runner/health.ts create mode 100644 test/health.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 7b8c1cf..44614d6 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -33,3 +33,4 @@ | #28 | done | SDK reconnect/resubscribe + heartbeat transport, user/market subscriptions, info gap-fill and persisted tid dedupe; simulated drop tests. | | #29 | done | Typed persisted monotonic event journal, replay/filter/follow stream, subscription/deposit emission and shutdown flush. | | #30 | done | JSON-line log replay/follow across rotated files, minimum-level/time/request filtering and redaction. | +| #31 | done | Runner heartbeat/state events, PID/uptime/WS/signing health and stale detection; background deposit polling. | diff --git a/src/commands/read.ts b/src/commands/read.ts index cc66e3a..0c9792c 100644 --- a/src/commands/read.ts +++ b/src/commands/read.ts @@ -1,3 +1,4 @@ +import {runnerHealth} from '../runner/health.js'; import {events} from '../runner/events.js'; import {activeRunner} from '../runner/lifecycle.js'; import {z} from 'zod'; @@ -20,7 +21,7 @@ export async function readCommand(name:string,config:Config){ const state=await hlInfo(config.network,'clearinghouseState',{user:config.masterAddress}); if(name==='balance') return accountBalance(state);if(name==='positions') return positions(state); const [agents,deposits,maxBuilderFee]=await Promise.all([hlInfo<{address:string;validUntil:number}[]>(config.network,'extraAgents',{user:config.masterAddress}),pollDeposits(config.network,config.masterAddress,state.marginSummary.accountValue,{emit:activeRunner?async deposit=>{await events.append('deposit',{id:deposit.id,state:deposit.state});}:undefined}),config.builder.address?hlInfo(config.network,'maxBuilderFee',{user:config.masterAddress,builder:config.builder.address}):Promise.resolve(0)]); - return {balance:accountBalance(state),positions:positions(state),agent:{address:config.agentAddress,approved:agents.some(a=>a.address.toLowerCase()===config.agentAddress?.toLowerCase()),revoked:config.revoked,...expiryStatus(config)},policy:{id:config.policyId??null,version:String(config.policyVersion??0)},deposits:deposits.map(d=>({id:d.id,state:d.state})),approvedMaxBuilderFee:String(maxBuilderFee),runner:null}; + return {balance:accountBalance(state),positions:positions(state),agent:{address:config.agentAddress,approved:agents.some(a=>a.address.toLowerCase()===config.agentAddress?.toLowerCase()),revoked:config.revoked,...expiryStatus(config)},policy:{id:config.policyId??null,version:String(config.policyVersion??0)},deposits:deposits.map(d=>({id:d.id,state:d.state})),approvedMaxBuilderFee:String(maxBuilderFee),runner:await runnerHealth()}; }catch(error){throw mapHyperliquidError(error);} } export async function commandConfig(context:Context){const config=await loadConfig(context.configPath);if(context.network) config.network=context.network;return config;} diff --git a/src/commands/run.ts b/src/commands/run.ts index 589589d..86c98e2 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -1,3 +1,4 @@ +import {startHeartbeat} from '../runner/health.js'; import {events,eventTypes,type EventType} from '../runner/events.js'; import {startSubscriptions} from '../runner/subscriptions.js'; import {rpcServer} from '../runner/rpc.js'; @@ -7,5 +8,5 @@ import {commandConfig} from './read.js'; import {Runner,detachRunner,setActiveRunner} from '../runner/lifecycle.js'; register('run',{description:'Start persistent signing runner (foreground or detach)',schema:z.object({detach:z.boolean().default(false),daemon:z.boolean().default(false)}),execute:async(input,context)=>{ if(input.detach||input.daemon) return detachRunner([...(context.configPath?['--config',context.configPath]:[]),...(context.network?['--network',context.network]:[])]); - const runner=new Runner(await commandConfig(context));const info=await runner.start();if(!info.existing){setActiveRunner(runner);runner.signals();runner.onShutdown(()=>events.flush());try{await rpcServer(runner,context);await startSubscriptions(runner,async(type,data)=>{if(eventTypes.includes(type as EventType)) await events.append(type as EventType,data);runner.logger.log(type==='error'?'error':'info',type,{data});});}catch(error){await runner.shutdown();throw error;}}return info; + const runner=new Runner(await commandConfig(context));const info=await runner.start();if(!info.existing){setActiveRunner(runner);runner.signals();runner.onShutdown(()=>events.flush());try{await rpcServer(runner,context);const subscriptions=await startSubscriptions(runner,async(type,data)=>{if(eventTypes.includes(type as EventType)) await events.append(type as EventType,data);runner.logger.log(type==='error'?'error':'info',type,{data});});startHeartbeat(runner,subscriptions);}catch(error){await runner.shutdown();throw error;}}return info; }}); diff --git a/src/runner/health.ts b/src/runner/health.ts new file mode 100644 index 0000000..0899e30 --- /dev/null +++ b/src/runner/health.ts @@ -0,0 +1,18 @@ +import {activeRunner,runningInfo,type Runner} from './lifecycle.js'; +import type {Subscriptions} from './subscriptions.js'; +import {events} from './events.js'; +import {hlInfo} from '../core/http.js'; +import {pollDeposits, } from '../funding/status.js'; +import {normalizeError} from '../errors/index.js'; +export function health(runner:Runner,now=Date.now()){ + const subscriptions=runner.subscriptions;return {pid:String(runner.info.pid),uptimeSeconds:String(Math.max(0,(now-runner.info.startedAt)/1000)),wsState:subscriptions?.state??'disconnected',wsStale:!subscriptions||now-subscriptions.lastMessage>90_000,lastFillTime:subscriptions?.lastFill??null,signingSession:runner.signer?'ready':'unavailable'}; +} +export async function runnerHealth(){if(activeRunner) return health(activeRunner);const info=await runningInfo();return info?{pid:String(info.pid),uptimeSeconds:String((Date.now()-info.startedAt)/1000),wsState:'unknown',signingSession:'unknown'}:null;} +export function startHeartbeat(runner:Runner,subscriptions:Subscriptions){ + runner.subscriptions=subscriptions;let queue=Promise.resolve(); + const tick=()=>{queue=queue.then(async()=>{ + await events.append('runner_state',health(runner)); + if(runner.config.masterAddress){const state=await hlInfo<{marginSummary:{accountValue:string}}>(runner.config.network,'clearinghouseState',{user:runner.config.masterAddress});await pollDeposits(runner.config.network,runner.config.masterAddress,state.marginSummary.accountValue,{emit:async deposit=>{await events.append('deposit',{id:deposit.id,state:deposit.state});}});} + }).catch(async error=>{await events.append('error',normalizeError(error).toJSON());});}; + tick();const timer=setInterval(tick,30_000);runner.onShutdown(async()=>{clearInterval(timer);await queue;}); +} diff --git a/src/runner/lifecycle.ts b/src/runner/lifecycle.ts index 27498a5..58c2b20 100644 --- a/src/runner/lifecycle.ts +++ b/src/runner/lifecycle.ts @@ -1,3 +1,4 @@ +import type {Subscriptions} from './subscriptions.js'; import {open,readFile,unlink,mkdir} from 'node:fs/promises'; import {join} from 'node:path'; import {spawn} from 'node:child_process'; @@ -11,7 +12,7 @@ export const runnerPidPath=()=>join(stateDir(),'runner.pid'); export function isAlive(pid:number){try{process.kill(pid,0);return true;}catch(error){return (error as NodeJS.ErrnoException).code==='EPERM';}} export async function runningInfo(path=runnerPidPath()):Promise{try{const info=JSON.parse(await readFile(path,'utf8')) as RunnerInfo;return Number.isInteger(info.pid)&&info.pid>0&&isAlive(info.pid)?info:null;}catch(error){if((error as NodeJS.ErrnoException).code==='ENOENT'||error instanceof SyntaxError) return null;throw error;}} export class Runner { - readonly info:RunnerInfo;readonly logger:Logger;signer?:Signer;private timer?:NodeJS.Timeout;private cleanup:(()=>Promise)[]=[];private closing=false; + subscriptions?:Subscriptions;readonly info:RunnerInfo;readonly logger:Logger;signer?:Signer;private timer?:NodeJS.Timeout;private cleanup:(()=>Promise)[]=[];private closing=false; constructor(readonly config:Config,readonly directory=stateDir()){this.info={pid:process.pid,startedAt:Date.now(),network:config.network};this.logger=new Logger({level:config.logLevel,directory:join(directory,'logs')});} async start(openSigner:()=>Promise=async()=>privySigner(this.config,await privyClient(this.config))){ await mkdir(this.directory,{recursive:true,mode:0o700});const path=join(this.directory,'runner.pid'); diff --git a/test/health.test.ts b/test/health.test.ts new file mode 100644 index 0000000..69923f9 --- /dev/null +++ b/test/health.test.ts @@ -0,0 +1,9 @@ +import {expect,test} from 'vitest'; +import {health} from '../src/runner/health.js'; +import {Runner} from '../src/runner/lifecycle.js'; +import {migrate} from '../src/core/config.js'; +import type {Subscriptions} from '../src/runner/subscriptions.js'; +test('health reports staleness, uptime and signer state',()=>{ + const runner=new Runner(migrate({}));runner.subscriptions={state:'connected',lastMessage:1000,lastFill:500} as Subscriptions; + expect(health(runner,2000)).toMatchObject({wsState:'connected',wsStale:false,lastFillTime:500,signingSession:'unavailable'});expect(health(runner,100000).wsStale).toBe(true); +}); From 364dfa599971cd81b62729f9b366011b3dfb0ac6 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:31:19 -0700 Subject: [PATCH 32/43] feat: detect agent expiry and emit warnings (#32) --- docs/PROGRESS.md | 1 + src/core/trading.ts | 3 ++- src/runner/health.ts | 3 ++- src/wallet/expiry.ts | 15 +++++++++++++++ test/expiry.test.ts | 10 ++++++++++ 5 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 src/wallet/expiry.ts create mode 100644 test/expiry.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 44614d6..43de849 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -34,3 +34,4 @@ | #29 | done | Typed persisted monotonic event journal, replay/filter/follow stream, subscription/deposit emission and shutdown flush. | | #30 | done | JSON-line log replay/follow across rotated files, minimum-level/time/request filtering and redaction. | | #31 | done | Runner heartbeat/state events, PID/uptime/WS/signing health and stale detection; background deposit polling. | +| #32 | done | Persisted once-per-expiry threshold warnings in runner events and nonretryable expiry trading block; mocked clock tests. | diff --git a/src/core/trading.ts b/src/core/trading.ts index 8fe6aab..3f0e16c 100644 --- a/src/core/trading.ts +++ b/src/core/trading.ts @@ -1,3 +1,4 @@ +import {assertAgentValid} from '../wallet/expiry.js'; import {activeRunner} from '../runner/lifecycle.js'; import {randomUUID} from 'node:crypto'; import {type Config} from './config.js'; @@ -19,7 +20,7 @@ export async function tradingServices(config:Config):Promise{ } export async function assertCanTrade(config:Config){ if(config.revoked||!config.approved||!config.masterAddress) throw new AgentCliError('AGENT_NOT_APPROVED','Agent approval required'); - if(!config.agentExpiry||config.agentExpiry<=Date.now()) throw new AgentCliError('AGENT_EXPIRED','Rotate expired agent'); + assertAgentValid(config); await assertDepositsSettled(config.network,config.masterAddress); } export async function checkRisk(config:Config,intent:RiskIntent,services:TradingServices,ignoreOid?:number){ diff --git a/src/runner/health.ts b/src/runner/health.ts index 0899e30..c26c499 100644 --- a/src/runner/health.ts +++ b/src/runner/health.ts @@ -1,3 +1,4 @@ +import {expiryWarnings} from '../wallet/expiry.js'; import {activeRunner,runningInfo,type Runner} from './lifecycle.js'; import type {Subscriptions} from './subscriptions.js'; import {events} from './events.js'; @@ -11,7 +12,7 @@ export async function runnerHealth(){if(activeRunner) return health(activeRunner export function startHeartbeat(runner:Runner,subscriptions:Subscriptions){ runner.subscriptions=subscriptions;let queue=Promise.resolve(); const tick=()=>{queue=queue.then(async()=>{ - await events.append('runner_state',health(runner)); + await events.append('runner_state',health(runner));await expiryWarnings(runner.config); if(runner.config.masterAddress){const state=await hlInfo<{marginSummary:{accountValue:string}}>(runner.config.network,'clearinghouseState',{user:runner.config.masterAddress});await pollDeposits(runner.config.network,runner.config.masterAddress,state.marginSummary.accountValue,{emit:async deposit=>{await events.append('deposit',{id:deposit.id,state:deposit.state});}});} }).catch(async error=>{await events.append('error',normalizeError(error).toJSON());});}; tick();const timer=setInterval(tick,30_000);runner.onShutdown(async()=>{clearInterval(timer);await queue;}); diff --git a/src/wallet/expiry.ts b/src/wallet/expiry.ts new file mode 100644 index 0000000..a0fd94b --- /dev/null +++ b/src/wallet/expiry.ts @@ -0,0 +1,15 @@ +import {readFile} from 'node:fs/promises'; +import {join} from 'node:path'; +import {atomicWrite,type Config,stateDir} from '../core/config.js'; +import {AgentCliError} from '../errors/index.js'; +import {events} from '../runner/events.js'; +export function assertAgentValid(config:Config,now=Date.now()){ + if(!config.agentExpiry||config.agentExpiry<=now) throw new AgentCliError('AGENT_EXPIRED','Rotate expired agent'); +} +export async function expiryWarnings(config:Config,options:{now?:number;path?:string;emit?:(data:unknown)=>Promise}={}){ + if(!config.agentExpiry) return [];const now=options.now??Date.now();const path=options.path??join(stateDir(),'expiry-warnings.json');let emitted:string[]=[]; + try{emitted=JSON.parse(await readFile(path,'utf8'));}catch(error){if((error as NodeJS.ErrnoException).code!=='ENOENT') throw error;} + const days=(config.agentExpiry-now)/86400000;const warnings=[]; + for(const threshold of config.expiryWarningDays){const key=`${config.agentAddress}:${config.agentExpiry}:${threshold}`;if(days<=threshold&&!emitted.includes(key)){const warning={agentAddress:config.agentAddress,expiry:config.agentExpiry,daysRemaining:String(Math.max(0,days)),threshold:String(threshold)};await (options.emit??(async data=>{await events.append('agent_expiry_warning',data);}))(warning);emitted.push(key);warnings.push(warning);}} + if(warnings.length) await atomicWrite(path,JSON.stringify(emitted));return warnings; +} diff --git a/test/expiry.test.ts b/test/expiry.test.ts new file mode 100644 index 0000000..8489c1a --- /dev/null +++ b/test/expiry.test.ts @@ -0,0 +1,10 @@ +import {expect,test} from 'vitest'; +import {mkdtemp,rm} from 'node:fs/promises'; +import {tmpdir} from 'node:os'; +import {join} from 'node:path'; +import {assertAgentValid,expiryWarnings} from '../src/wallet/expiry.js'; +import {migrate} from '../src/core/config.js'; +test('mocked clock blocks expiry and emits each threshold once',async()=>{ + const dir=await mkdtemp(join(tmpdir(),'bloxwap-expiry-'));const config=migrate({agentExpiry:15*86400000});const data:unknown[]=[]; + try{const options={path:join(dir,'warnings.json'),now:12*86400000,emit:async(value:unknown)=>{data.push(value);}};expect(await expiryWarnings(config,options)).toHaveLength(2);expect(await expiryWarnings(config,options)).toHaveLength(0);assertAgentValid(config,14*86400000);expect(()=>assertAgentValid(config,15*86400000)).toThrow(expect.objectContaining({code:'AGENT_EXPIRED',retryable:false}));}finally{await rm(dir,{recursive:true,force:true});} +}); From b16971e3e2b585259cd3cde0899b2154861c1a2c Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:32:16 -0700 Subject: [PATCH 33/43] feat: rotate managed agents with verified signer handoff (#33) --- docs/PROGRESS.md | 1 + src/cli.ts | 1 + src/commands/init.ts | 2 +- src/commands/rotate.ts | 27 +++++++++++++++++++++++++++ src/core/config.ts | 2 +- src/runner/rpc.ts | 2 +- test/rotate.test.ts | 7 +++++++ 7 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 src/commands/rotate.ts create mode 100644 test/rotate.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 43de849..9461ef0 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -35,3 +35,4 @@ | #30 | done | JSON-line log replay/follow across rotated files, minimum-level/time/request filtering and redaction. | | #31 | done | Runner heartbeat/state events, PID/uptime/WS/signing health and stale detection; background deposit polling. | | #32 | done | Persisted once-per-expiry threshold warnings in runner events and nonretryable expiry trading block; mocked clock tests. | +| #33 | done | Persisted two-approval rotation, verified new agent before serialized runner hot-swap, old revocation only after handoff; rotation audit hook in #35. | diff --git a/src/cli.ts b/src/cli.ts index c95e03d..c944cd6 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -12,6 +12,7 @@ import './commands/leverage.js'; import './commands/run.js'; import './commands/events.js'; import './commands/logs.js'; +import './commands/rotate.js'; import {routeToRunner} from './runner/rpc.js'; setRouting(routeToRunner); let stdin=''; diff --git a/src/commands/init.ts b/src/commands/init.ts index 85a8fb2..0788e9f 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -17,7 +17,7 @@ register('init',{description:'Create agent and emit resumable master-wallet appr if(expiry<=Date.now()||expiry>Date.now()+180*86400000) throw new AgentCliError('CONFIG_INVALID','Expiry must be within 180 days');config.agentExpiry=expiry; const client=await privyClient(config);const policyId=await applyPolicy(config,client,context.configPath);await createAgent(config,client,context.configPath,policyId); const path=join(stateDir(),`approval-${config.agentAddress}.json`);let pending:{agent:Approval;builder?:Approval}; - try{pending=JSON.parse(await readFile(path,'utf8'));}catch(error){if((error as NodeJS.ErrnoException).code!=='ENOENT') throw error;pending={agent:approval(config.network,'approveAgent',{agentAddress:config.agentAddress,agentName:`bloxwap valid_until ${expiry}`})};if(config.builder.address) pending.builder=approval(config.network,'approveBuilderFee',{builder:config.builder.address,maxFeeRate:`${config.builder.fee/1000}%`},Date.now()+1);await atomicWrite(path,JSON.stringify(pending));} + try{pending=JSON.parse(await readFile(path,'utf8'));}catch(error){if((error as NodeJS.ErrnoException).code!=='ENOENT') throw error;pending={agent:approval(config.network,'approveAgent',{agentAddress:config.agentAddress,agentName:`${config.agentName} valid_until ${expiry}`})};if(config.builder.address) pending.builder=approval(config.network,'approveBuilderFee',{builder:config.builder.address,maxFeeRate:`${config.builder.fee/1000}%`},Date.now()+1);await atomicWrite(path,JSON.stringify(pending));} if(input.signature) await submitApproval(config.network,pending.agent,input.signature as string); if(input.builderSignature&&pending.builder) await submitApproval(config.network,pending.builder,input.builderSignature as string); const agents=await hlInfo<{address:string;validUntil:number}[]>(config.network,'extraAgents',{user:config.masterAddress}); diff --git a/src/commands/rotate.ts b/src/commands/rotate.ts new file mode 100644 index 0000000..0badf1b --- /dev/null +++ b/src/commands/rotate.ts @@ -0,0 +1,27 @@ +import {z} from 'zod'; +import {join} from 'node:path'; +import {readFile} from 'node:fs/promises'; +import {register} from '../core/router.js'; +import {commandConfig} from './read.js'; +import {atomicWrite,saveConfig,stateDir,type Config} from '../core/config.js'; +import {privyClient,createAgent,privySigner} from '../wallet/privy.js'; +import {approval,submitApproval,type Approval} from '../wallet/approval.js'; +import {hlInfo} from '../core/http.js'; +import {activeRunner} from '../runner/lifecycle.js'; +import {AgentCliError} from '../errors/index.js'; +import {events} from '../runner/events.js'; +export interface Rotation {old:Config;next:Config;approval:Approval;revoke:Approval;swapped:boolean;revoked:boolean;} +export async function rotationHandoff(rotation:Rotation,dependencies:{verify:()=>Promise;swap:(next:Config)=>Promise;persist:()=>Promise}){ + if(!await dependencies.verify()) throw new AgentCliError('AGENT_NOT_APPROVED','New agent approval not visible; old agent remains active'); + if(!rotation.swapped){await dependencies.swap(rotation.next);rotation.swapped=true;await dependencies.persist();}return rotation.revoke; +} +register('rotate',{description:'Approve new agent, hot-swap signer, then revoke old agent',schema:z.object({signature:z.string().optional(),revokeSignature:z.string().optional()}),execute:async(input,context)=>{ + const current=await commandConfig(context);if(!current.masterAddress||!current.agentAddress) throw new AgentCliError('AGENT_NOT_APPROVED','Initialize first'); + const path=join(stateDir(),`rotation-${current.masterAddress}.json`);let rotation:Rotation;const client=await privyClient(current); + try{rotation=JSON.parse(await readFile(path,'utf8'));}catch(error){if((error as NodeJS.ErrnoException).code!=='ENOENT') throw error; + const next:Config={...structuredClone(current),privyWalletId:undefined,agentAddress:undefined,authorizationKeyRef:undefined,agentExpiry:Date.now()+179*86400000,approved:false,revoked:false};await createAgent(next,client,join(stateDir(),'rotation-next.json'),current.policyId);next.agentName=`blox-${next.agentAddress!.slice(-8)}`; + rotation={old:current,next,approval:approval(current.network,'approveAgent',{agentAddress:next.agentAddress,agentName:`${next.agentName} valid_until ${next.agentExpiry}`}),revoke:approval(current.network,'approveAgent',{agentAddress:'0x'+'0'.repeat(40),agentName:current.agentName}),swapped:false,revoked:false};await atomicWrite(path,JSON.stringify(rotation));} + if(input.signature){await submitApproval(current.network,rotation.approval,input.signature as string);await rotationHandoff(rotation,{verify:async()=>{const agents=await hlInfo<{address:string;validUntil:number}[]>(current.network,'extraAgents',{user:current.masterAddress});return agents.some(a=>a.address.toLowerCase()===rotation.next.agentAddress?.toLowerCase()&&a.validUntil>=rotation.next.agentExpiry!);},swap:async next=>{const signer=await privySigner(next,client);next.approved=true;await saveConfig(next,context.configPath);if(activeRunner){activeRunner.signer=signer;Object.assign(activeRunner.config,next);}await events.append('runner_state',{rotation:{old:rotation.old.agentAddress,next:next.agentAddress}});},persist:()=>atomicWrite(path,JSON.stringify(rotation))});} + if(input.revokeSignature){if(!rotation.swapped) throw new AgentCliError('POLICY_REJECTED','Approve and swap new agent before revoking old');await submitApproval(current.network,rotation.revoke,input.revokeSignature as string);rotation.revoked=true;await atomicWrite(path,JSON.stringify(rotation));} + return {oldAgent:rotation.old.agentAddress,newAgent:rotation.next.agentAddress,swapped:rotation.swapped,oldRevoked:rotation.revoked,requiresSignature:!rotation.swapped,approval:rotation.approval,...(rotation.swapped?{revokeApproval:rotation.revoke}:{})}; +}}); diff --git a/src/core/config.ts b/src/core/config.ts index ba85f00..f6f25b8 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -9,7 +9,7 @@ const address=z.string().regex(/^0x[0-9a-fA-F]{40}$/); const positive=z.number().positive().finite(); export const configSchema=z.object({ version:z.literal(1).default(1),network:z.enum(['mainnet','testnet']).default('testnet'), - masterAddress:address.optional(),agentAddress:address.optional(),agentExpiry:z.number().int().positive().optional(), + agentName:z.string().max(16).default('bloxwap'),masterAddress:address.optional(),agentAddress:address.optional(),agentExpiry:z.number().int().positive().optional(), privyAppId:z.string().optional(),privyWalletId:z.string().optional(),policyId:z.string().optional(),policyVersion:z.number().int().optional(), authorizationKeyRef:z.string().regex(/^env:[A-Z_][A-Z0-9_]*$|^file:.+/).optional(), privyAppSecretRef:z.string().regex(/^env:[A-Z_][A-Z0-9_]*$/).optional(), diff --git a/src/runner/rpc.ts b/src/runner/rpc.ts index 4567628..4816f77 100644 --- a/src/runner/rpc.ts +++ b/src/runner/rpc.ts @@ -9,7 +9,7 @@ import {commands,type Context} from '../core/router.js'; import type {Runner} from './lifecycle.js'; export const socketPath=(directory=stateDir())=>process.platform==='win32'?`\\\\.\\pipe\\bloxwap-${createHash('sha256').update(directory).digest('hex').slice(0,24)}`:join(directory,'run.sock'); const aliases:Record={'status.get':'status','balance.get':'balance','positions.get':'positions','orders.get':'orders','order.place':'order','order.cancel':'cancel','order.modify':'modify','leverage.set':'leverage','config.get':'config','runner.shutdown':'shutdown'}; -export const routedCommands=new Set(['status','balance','positions','orders','order','cancel','modify','leverage']); +export const routedCommands=new Set(['status','balance','positions','orders','order','cancel','modify','leverage','rotate']); const requestSchema=z.object({jsonrpc:z.literal('2.0'),id:z.union([z.string(),z.number(),z.null()]),method:z.string(),params:z.record(z.string(),z.unknown()).default({})}); export async function rpcServer(runner:Runner,context:Context,execute?: (name:string,input:Record)=>Promise):Promise{ const path=socketPath(runner.directory);let queue=Promise.resolve(); diff --git a/test/rotate.test.ts b/test/rotate.test.ts new file mode 100644 index 0000000..9586de6 --- /dev/null +++ b/test/rotate.test.ts @@ -0,0 +1,7 @@ +import {expect,test,vi} from 'vitest'; +import {rotationHandoff,type Rotation} from '../src/commands/rotate.js'; +test('rotation never exposes old revocation until approval verification and swap',async()=>{ + const rotation={next:{},swapped:false,revoke:{action:{type:'approveAgent'}}} as unknown as Rotation;const swap=vi.fn(async()=>{}),persist=vi.fn(async()=>{}); + await expect(rotationHandoff(rotation,{verify:async()=>false,swap,persist})).rejects.toMatchObject({code:'AGENT_NOT_APPROVED'});expect(swap).not.toHaveBeenCalled(); + expect(await rotationHandoff(rotation,{verify:async()=>true,swap,persist})).toBe(rotation.revoke);expect(rotation.swapped).toBe(true);expect(persist).toHaveBeenCalledOnce(); +}); From e1a48c041e344aee6662de6d02cfba75f49e1ea5 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:32:47 -0700 Subject: [PATCH 34/43] feat: add agent revocation with local trading block (#34) --- docs/PROGRESS.md | 1 + src/cli.ts | 1 + src/commands/revoke.ts | 20 ++++++++++++++++++++ src/runner/rpc.ts | 2 +- test/revoke.test.ts | 5 +++++ 5 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 src/commands/revoke.ts create mode 100644 test/revoke.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 9461ef0..aaafc10 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -36,3 +36,4 @@ | #31 | done | Runner heartbeat/state events, PID/uptime/WS/signing health and stale detection; background deposit polling. | | #32 | done | Persisted once-per-expiry threshold warnings in runner events and nonretryable expiry trading block; mocked clock tests. | | #33 | done | Persisted two-approval rotation, verified new agent before serialized runner hot-swap, old revocation only after handoff; rotation audit hook in #35. | +| #34 | done | Optional cancel-all, resumable master revocation payload, immediate local blocking and config/session clearing after exchange submission. | diff --git a/src/cli.ts b/src/cli.ts index c944cd6..28ee274 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -13,6 +13,7 @@ import './commands/run.js'; import './commands/events.js'; import './commands/logs.js'; import './commands/rotate.js'; +import './commands/revoke.js'; import {routeToRunner} from './runner/rpc.js'; setRouting(routeToRunner); let stdin=''; diff --git a/src/commands/revoke.ts b/src/commands/revoke.ts new file mode 100644 index 0000000..dcb8091 --- /dev/null +++ b/src/commands/revoke.ts @@ -0,0 +1,20 @@ +import {z} from 'zod'; +import {readFile} from 'node:fs/promises'; +import {join} from 'node:path'; +import {register} from '../core/router.js'; +import {commandConfig} from './read.js'; +import {cancelOrders,cancelSchema} from './cancel.js'; +import {atomicWrite,saveConfig,stateDir,type Config} from '../core/config.js'; +import {approval,submitApproval,type Approval} from '../wallet/approval.js'; +import {activeRunner} from '../runner/lifecycle.js'; +import {AgentCliError} from '../errors/index.js'; +export function markRevoked(config:Config){config.revoked=true;config.approved=false;return config;} +register('revoke',{description:'Disable local agent and prepare master-signed exchange revocation',schema:z.object({cancelOrders:z.boolean().default(false),signature:z.string().optional()}),execute:async(input,context)=>{ + const config=await commandConfig(context);if(!config.agentAddress) throw new AgentCliError('AGENT_NOT_APPROVED','No agent to revoke'); + if(input.cancelOrders&&!config.revoked) await cancelOrders(config,cancelSchema.parse({all:true})); + const path=join(stateDir(),`revoke-${config.agentAddress}.json`);let pending:Approval; + try{pending=JSON.parse(await readFile(path,'utf8'));}catch(error){if((error as NodeJS.ErrnoException).code!=='ENOENT') throw error;pending=approval(config.network,'approveAgent',{agentAddress:'0x'+'0'.repeat(40),agentName:config.agentName});await atomicWrite(path,JSON.stringify(pending));} + let exchangeRevoked=false;if(input.signature){await submitApproval(config.network,pending,input.signature as string);exchangeRevoked=true;config.authorizationKeyRef=undefined;config.privyWalletId=undefined;} + markRevoked(config);await saveConfig(config,context.configPath);if(activeRunner){activeRunner.signer=undefined;Object.assign(activeRunner.config,config);} + return {agentAddress:config.agentAddress,locallyRevoked:true,exchangeRevoked,requiresSignature:!exchangeRevoked,approval:pending}; +}}); diff --git a/src/runner/rpc.ts b/src/runner/rpc.ts index 4816f77..0b17da9 100644 --- a/src/runner/rpc.ts +++ b/src/runner/rpc.ts @@ -9,7 +9,7 @@ import {commands,type Context} from '../core/router.js'; import type {Runner} from './lifecycle.js'; export const socketPath=(directory=stateDir())=>process.platform==='win32'?`\\\\.\\pipe\\bloxwap-${createHash('sha256').update(directory).digest('hex').slice(0,24)}`:join(directory,'run.sock'); const aliases:Record={'status.get':'status','balance.get':'balance','positions.get':'positions','orders.get':'orders','order.place':'order','order.cancel':'cancel','order.modify':'modify','leverage.set':'leverage','config.get':'config','runner.shutdown':'shutdown'}; -export const routedCommands=new Set(['status','balance','positions','orders','order','cancel','modify','leverage','rotate']); +export const routedCommands=new Set(['status','balance','positions','orders','order','cancel','modify','leverage','rotate','revoke']); const requestSchema=z.object({jsonrpc:z.literal('2.0'),id:z.union([z.string(),z.number(),z.null()]),method:z.string(),params:z.record(z.string(),z.unknown()).default({})}); export async function rpcServer(runner:Runner,context:Context,execute?: (name:string,input:Record)=>Promise):Promise{ const path=socketPath(runner.directory);let queue=Promise.resolve(); diff --git a/test/revoke.test.ts b/test/revoke.test.ts new file mode 100644 index 0000000..d65fbc6 --- /dev/null +++ b/test/revoke.test.ts @@ -0,0 +1,5 @@ +import {expect,test} from 'vitest'; +import {markRevoked} from '../src/commands/revoke.js'; +import {assertCanTrade} from '../src/core/trading.js'; +import {migrate} from '../src/core/config.js'; +test('revocation immediately blocks every trading entrypoint',async()=>{const config=migrate({approved:true,agentExpiry:Date.now()+10000,masterAddress:'0x'+'a'.repeat(40)});markRevoked(config);await expect(assertCanTrade(config)).rejects.toMatchObject({code:'AGENT_NOT_APPROVED'});}); From 9b95695212af6a4319426c3d4f6dd606ce423c0e Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:33:49 -0700 Subject: [PATCH 35/43] feat: add linked tamper-evident trading audit (#35) --- docs/PROGRESS.md | 1 + src/cli.ts | 1 + src/commands/audit.ts | 4 ++++ src/core/audit.ts | 36 ++++++++++++++++++++++++++++++++++++ src/core/router.ts | 6 +++++- src/core/trading.ts | 3 ++- src/runner/events.ts | 2 ++ src/wallet/privy.ts | 3 ++- test/audit.test.ts | 8 ++++++++ 9 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 src/commands/audit.ts create mode 100644 src/core/audit.ts create mode 100644 test/audit.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index aaafc10..3c88d39 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -37,3 +37,4 @@ | #32 | done | Persisted once-per-expiry threshold warnings in runner events and nonretryable expiry trading block; mocked clock tests. | | #33 | done | Persisted two-approval rotation, verified new agent before serialized runner hot-swap, old revocation only after handoff; rotation audit hook in #35. | | #34 | done | Optional cancel-all, resumable master revocation payload, immediate local blocking and config/session clearing after exchange submission. | +| #35 | done | Cross-process locked fsynced hash-chain audit, intent/decision/signature digest/response/fill linkage, rotation/revoke records and filters; tamper/redaction tests. | diff --git a/src/cli.ts b/src/cli.ts index 28ee274..f9477e0 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -14,6 +14,7 @@ import './commands/events.js'; import './commands/logs.js'; import './commands/rotate.js'; import './commands/revoke.js'; +import './commands/audit.js'; import {routeToRunner} from './runner/rpc.js'; setRouting(routeToRunner); let stdin=''; diff --git a/src/commands/audit.ts b/src/commands/audit.ts new file mode 100644 index 0000000..8f867a3 --- /dev/null +++ b/src/commands/audit.ts @@ -0,0 +1,4 @@ +import {z} from 'zod'; +import {register} from '../core/router.js'; +import {readAudit,verifyAudit} from '../core/audit.js'; +register('audit',{description:'Verify and query the hash-chained trading audit',schema:z.object({intent:z.string().optional(),intentId:z.string().optional(),cloid:z.string().optional()}),execute:async input=>{const entries=await readAudit();verifyAudit(entries);return entries.filter(entry=>(!input.intent&&!input.intentId||entry.intentId===(input.intentId??input.intent))&&(!input.cloid||entry.cloid===input.cloid));}}); diff --git a/src/core/audit.ts b/src/core/audit.ts new file mode 100644 index 0000000..60bb857 --- /dev/null +++ b/src/core/audit.ts @@ -0,0 +1,36 @@ +import {AsyncLocalStorage} from 'node:async_hooks'; +import {createHash,randomUUID} from 'node:crypto'; +import {readFile,open,unlink,mkdir} from 'node:fs/promises'; +import {join,dirname} from 'node:path'; +import {stateDir} from './config.js'; +import {redact} from './logger.js'; +import {AgentCliError,normalizeError} from '../errors/index.js'; +export interface AuditEntry {seq:number;timestamp:string;intentId:string;cloid?:string;stage:string;data:unknown;previousHash:string;hash:string;} +const digest=(value:unknown)=>createHash('sha256').update(JSON.stringify(value)).digest('hex'); +const context=new AsyncLocalStorage<{intentId:string;cloid?:string}>(); +export async function readAudit(path=join(stateDir(),'audit.jsonl')):Promise{try{return (await readFile(path,'utf8')).split('\n').filter(Boolean).map(line=>JSON.parse(line));}catch(error){if((error as NodeJS.ErrnoException).code==='ENOENT') return [];throw error;}} +export function verifyAudit(entries:AuditEntry[]){let previousHash='0'.repeat(64);let seq=0;for(const entry of entries){const {hash,...payload}=entry;if(entry.seq!==++seq||entry.previousHash!==previousHash||digest(payload)!==hash) throw new AgentCliError('INTERNAL','Audit chain integrity failure');previousHash=hash;}return true;} +export class AuditStore { + private queue=Promise.resolve(); + constructor(readonly path=join(stateDir(),'audit.jsonl')){} + append(stage:string,data:unknown,metadata=context.getStore()??{intentId:randomUUID()}){ + const operation=this.queue.then(async()=>{await mkdir(dirname(this.path),{recursive:true,mode:0o700});const lock=this.path+'.lock';let handle; + for(let i=0;i<100;i++){try{handle=await open(lock,'wx',0o600);break;}catch(error){if((error as NodeJS.ErrnoException).code!=='EEXIST') throw error;await new Promise(resolve=>setTimeout(resolve,10));}} + if(!handle) throw new AgentCliError('INTERNAL','Audit lock busy; inspect stale lock'); + try{const entries=await readAudit(this.path);verifyAudit(entries);const last=entries.at(-1);const payload={seq:(last?.seq??0)+1,timestamp:new Date().toISOString(),...metadata,stage,data:redact(data),previousHash:last?.hash??'0'.repeat(64)};const entry={...payload,hash:digest(payload)};const file=await open(this.path,'a',0o600);try{await file.writeFile(JSON.stringify(entry)+'\n');await file.sync();}finally{await file.close();}return entry;}finally{await handle.close();await unlink(lock);} + });this.queue=operation.then(()=>{});return operation; + } + async flush(){await this.queue;} +} +export const audit=new AuditStore(); +export async function auditedAction(name:string,input:Record,operation:()=>Promise):Promise{ + return context.run({intentId:input.intentId as string??randomUUID(),cloid:input.cloid as string|undefined},async()=>{ + await audit.append('intent',{action:name,input});try{const result=await operation();await audit.append('response',{action:name,result});return result;}catch(error){await audit.append('decision',{allowed:false,error:normalizeError(error).toJSON()});throw error;} + }); +} +export async function signatureAudit(signature:string){if(context.getStore()) await audit.append('signature',{digest:createHash('sha256').update(signature).digest('hex')});} +export async function decisionAudit(data:unknown){if(context.getStore()) await audit.append('decision',data);} +export async function auditFill(fill:Record){ + const entries=await readAudit();const linked=[...entries].reverse().find(entry=>entry.cloid&&entry.cloid===fill.cloid||entry.stage==='response'&&JSON.stringify(entry.data).includes(`"oid":${fill.oid}`)); + await audit.append('fill',fill,{intentId:linked?.intentId??'external',cloid:linked?.cloid}); +} diff --git a/src/core/router.ts b/src/core/router.ts index 0f537c0..711b707 100644 --- a/src/core/router.ts +++ b/src/core/router.ts @@ -1,3 +1,4 @@ +import {auditedAction} from './audit.js'; import { z } from 'zod'; import { AgentCliError, normalizeError, success } from '../errors/index.js'; export interface Context { configPath?: string; network?: 'mainnet' | 'testnet'; pretty: boolean; runner?: boolean; streaming?:boolean; } @@ -5,7 +6,10 @@ export interface Command { description: string; schema: z.ZodType; execute: (inp export let routing:((name:string,input:Record,context:Context)=>Promise<{routed:boolean;data?:unknown}>)|undefined; export function setRouting(value:typeof routing){routing=value;} export const commands = new Map(); -export function register(name: string, command: Command) { commands.set(name, command); } +export function register(name: string, command: Command) { + if(['order','cancel','modify','leverage','rotate','revoke'].includes(name)){const original=command.execute;command={...command,schema:command.schema instanceof z.ZodObject?command.schema.extend({intentId:z.string().optional(),intent:z.string().optional()}):command.schema,execute:(input,context)=>auditedAction(name,input,()=>original(input,context))};} + commands.set(name,command); +} export function schemas(name: string) { const command = commands.get(name); if (!command) throw new AgentCliError('CONFIG_INVALID', `Unknown command: ${name}`); diff --git a/src/core/trading.ts b/src/core/trading.ts index 3f0e16c..4d0e9bb 100644 --- a/src/core/trading.ts +++ b/src/core/trading.ts @@ -1,3 +1,4 @@ +import {decisionAudit} from './audit.js'; import {assertAgentValid} from '../wallet/expiry.js'; import {activeRunner} from '../runner/lifecycle.js'; import {randomUUID} from 'node:crypto'; @@ -31,7 +32,7 @@ export async function checkRisk(config:Config,intent:RiskIntent,services:Trading try{return evaluateRisk(config,intent,{positionSize:position?.szi??'0',dailyLoss:String(Math.max(0,-state.assetPositions.reduce((sum,p)=>sum+Number(p.position.unrealizedPnl),0))),openOrders:open.length,marketMaxLeverage:market.maxLeverage,pendingSize:String(pending)});}catch(error){logger.log('warn','Risk rejected',{error:mapHyperliquidError(error).toJSON()});await logger.flush();throw error;} } export async function exchangeAction(action:string,operation:()=>Promise){ - assertTradeAction(action);try{ + assertTradeAction(action);await decisionAudit({allowed:true,action});try{ const response=await operation();const data=response as {status?:string;response?:{data?:{statuses?:unknown[]}}}; if(data.status==='err') throw mapHyperliquidError(response); for(const status of data.response?.data?.statuses??[]) if(status&&typeof status==='object'&&'error' in status) throw mapHyperliquidError((status as {error:string}).error); diff --git a/src/runner/events.ts b/src/runner/events.ts index 2beacae..492f6bf 100644 --- a/src/runner/events.ts +++ b/src/runner/events.ts @@ -1,3 +1,4 @@ +import {auditFill} from '../core/audit.js'; import {appendFile,mkdir,readFile,chmod} from 'node:fs/promises'; import {dirname,join} from 'node:path'; import {z} from 'zod'; @@ -15,6 +16,7 @@ export class EventStore { private queue=Promise.resolve(); constructor(readonly path=join(stateDir(),'events.jsonl')){} append(type:EventType,data:unknown){let event:RunnerEvent;const operation=this.queue.then(async()=>{ + if(type==='fill'&&data&&typeof data==='object') await auditFill(data as Record); const previous=(await readEvents(this.path)).at(-1)?.seq??0;event=eventSchema.parse({seq:previous+1,timestamp:new Date().toISOString(),type,data:redact(data)}); await mkdir(dirname(this.path),{recursive:true,mode:0o700});await appendFile(this.path,JSON.stringify(event)+'\n',{mode:0o600});await chmod(this.path,0o600);return event; });this.queue=operation.then(()=>{});return operation; diff --git a/src/wallet/privy.ts b/src/wallet/privy.ts index e2a8347..f3c9fd2 100644 --- a/src/wallet/privy.ts +++ b/src/wallet/privy.ts @@ -1,3 +1,4 @@ +import {signatureAudit} from '../core/audit.js'; import { PrivyClient, generateP256KeyPair } from '@privy-io/node'; import { readFile,stat } from 'node:fs/promises'; import { join } from 'node:path'; @@ -29,7 +30,7 @@ export async function privySigner(config:Config,client:PrivyClient):Promise{ try { const signed=await client.wallets().ethereum().signTypedData(config.privyWalletId!,{params:{typed_data:{domain:data.domain,types:Object.fromEntries(Object.entries(data.types).map(([key,fields])=>[key,[...fields]])),primary_type:data.primaryType,message:data.message}},authorization_context:{authorization_private_keys:[authorizationKey]}}); - return signed.signature as `0x${string}`; + await signatureAudit(signed.signature);return signed.signature as `0x${string}`; }catch(error){throw mapPrivyError(error);} }}; } diff --git a/test/audit.test.ts b/test/audit.test.ts new file mode 100644 index 0000000..1120fe9 --- /dev/null +++ b/test/audit.test.ts @@ -0,0 +1,8 @@ +import {expect,test} from 'vitest'; +import {mkdtemp,rm} from 'node:fs/promises'; +import {tmpdir} from 'node:os'; +import {join} from 'node:path'; +import {AuditStore,readAudit,verifyAudit} from '../src/core/audit.js'; +test('audit chain detects edits and excludes sensitive signatures',async()=>{ + const dir=await mkdtemp(join(tmpdir(),'bloxwap-audit-'));const path=join(dir,'audit.jsonl');try{const store=new AuditStore(path);await store.append('intent',{signature:'secret'},{intentId:'one',cloid:'client'});await store.append('decision',{allowed:true},{intentId:'one'});const entries=await readAudit(path);expect(verifyAudit(entries)).toBe(true);expect(JSON.stringify(entries)).not.toContain('secret');entries[0].data='tampered';expect(()=>verifyAudit(entries)).toThrow();}finally{await rm(dir,{recursive:true,force:true});} +}); From ec9905ea3827c71da2a47731538e3d76a1893665 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:34:43 -0700 Subject: [PATCH 36/43] feat: reject master secrets and document security model (#36) --- docs/PROGRESS.md | 1 + docs/security.md | 26 ++++++++++++++++++++++++++ package-lock.json | 23 +++++++++++++++++++++++ package.json | 1 + src/core/logger.ts | 5 ++++- src/core/router.ts | 3 ++- src/errors/index.ts | 3 ++- src/runner/rpc.ts | 3 ++- src/wallet/approval.ts | 7 ++----- src/wallet/safety.ts | 12 ++++++++++++ test/safety.test.ts | 10 ++++++++++ 11 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 docs/security.md create mode 100644 src/wallet/safety.ts create mode 100644 test/safety.test.ts diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 3c88d39..a7edb8a 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -38,3 +38,4 @@ | #33 | done | Persisted two-approval rotation, verified new agent before serialized runner hot-swap, old revocation only after handoff; rotation audit hook in #35. | | #34 | done | Optional cancel-all, resumable master revocation payload, immediate local blocking and config/session clearing after exchange submission. | | #35 | done | Cross-process locked fsynced hash-chain audit, intent/decision/signature digest/response/fill linkage, rotation/revoke records and filters; tamper/redaction tests. | +| #36 | done | Pre-schema recursive CLI/RPC secret detector, BIP39-like seed rejection, private authorization refs and env/log/error scrubbing; threat model docs. | diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000..c3ec951 --- /dev/null +++ b/docs/security.md @@ -0,0 +1,26 @@ +# Security and threat model + +State is always `~/.bloxwap`, directory 0700, config/auth keys/logs/events/audit 0600. +P-256 Privy authorization keys are PKCS8 base64 in private files or environment +references. The CLI does not import master keys, seeds or mnemonic phrases. +External master-wallet signatures are accepted only as 65-byte signatures. +Every CLI and RPC input is scanned before schema parsing so unknown fields cannot +smuggle key material. Hex 32-byte secrets and BIP39-like phrases are refused. + +Trust boundaries: master wallet → one-time Hyperliquid agent approval → Privy +managed wallet → restricted local signing runner. Read `custody.md` for the hash +preimage limitation of server policies. Anyone controlling this OS user can use +the authorization key and change limits. Socket/file modes exclude other users, +not same-user malware or root. Windows named pipes require current-user ACLs; +use direct mode in environments unable to enforce private IPC. + +Logs and error envelopes recursively scrub sensitive fields, long hex values, +Bearer tokens and known secret environment values. Signature hashes, never raw +signatures, appear in the audit chain. A hash chain detects edits inside a retained +journal; it cannot detect deletion of the entire journal without an external +checkpoint. Back up checkpoints separately for stronger evidence. + +Never put secrets in command args, repository files or issue reports. Environment +references avoid plaintext config but do not protect against privileged process +inspection. Revoke and rotate after suspected compromise. Unit tests use service +mocks; real signing tests require explicit testnet/sandbox environment gates. diff --git a/package-lock.json b/package-lock.json index 91125d3..357966c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@bloxwap/hyperliquid": "^0.6.0", "@privy-io/node": "^0.35.0", + "@scure/bip39": "^2.4.0", "qrcode": "^1.5.4", "ws": "^8.22.0", "zod": "^4.6.7" @@ -1160,6 +1161,28 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@scure/bip39": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.4.0.tgz", + "integrity": "sha512-82dxFbZUYboyOf0AXiydsQrFQ5Q4h9mX+O2UkE91ROYmsc0BKMGZLwDmy96Jpa2+vrtoxomjUhy1RPIgH/r2nA==", + "dependencies": { + "@noble/hashes": "2.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz", + "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@stablelib/base64": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", diff --git a/package.json b/package.json index c60413d..b22fee2 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "dependencies": { "@bloxwap/hyperliquid": "^0.6.0", "@privy-io/node": "^0.35.0", + "@scure/bip39": "^2.4.0", "qrcode": "^1.5.4", "ws": "^8.22.0", "zod": "^4.6.7" diff --git a/src/core/logger.ts b/src/core/logger.ts index a1316f5..0360e6c 100644 --- a/src/core/logger.ts +++ b/src/core/logger.ts @@ -5,7 +5,10 @@ import { stateDir } from './config.js'; export const levels={debug:10,info:20,warn:30,error:40}; export type Level=keyof typeof levels; export function redact(value: unknown): unknown { - if (typeof value==='string') return value.replace(/(?:0x)?[a-fA-F0-9]{64,}/g,'[REDACTED]').replace(/Bearer\s+\S+/gi,'Bearer [REDACTED]'); + if (typeof value==='string') { + let safe=value;for(const [key,secret] of Object.entries(process.env)) if(/secret|token|authorization|private.?key|password/i.test(key)&&secret&&secret.length>=4) safe=safe.split(secret).join('[REDACTED]'); + return safe.replace(/(?:0x)?[a-fA-F0-9]{64,}/g,'[REDACTED]').replace(/Bearer\s+\S+/gi,'Bearer [REDACTED]');} + if (Array.isArray(value)) return value.map(redact); if (value && typeof value==='object') return Object.fromEntries(Object.entries(value).map(([key,v])=>[key,/secret|signature|token|authorization|private.?key|seed|mnemonic|password|app.?key/i.test(key) ? '[REDACTED]' : redact(v)])); return value; diff --git a/src/core/router.ts b/src/core/router.ts index 711b707..0272fa3 100644 --- a/src/core/router.ts +++ b/src/core/router.ts @@ -1,3 +1,4 @@ +import {assertSafeInput} from '../wallet/safety.js'; import {auditedAction} from './audit.js'; import { z } from 'zod'; import { AgentCliError, normalizeError, success } from '../errors/index.js'; @@ -41,7 +42,7 @@ export async function dispatch(argv: string[], stdin = '') { if (name === 'schema') return {body:success(schemas(args[0])),exitCode:0,context}; const command=commands.get(name); if (!command) throw new AgentCliError('CONFIG_INVALID',`Unknown command: ${name}`); if (args.length) input.args=args; - const parsed=command.schema.safeParse(input); + assertSafeInput(input);const parsed=command.schema.safeParse(input); if (!parsed.success) throw new AgentCliError('CONFIG_INVALID','Invalid command input',parsed.error.flatten()); const route=await routing?.(name,parsed.data as Record,context); const data=route?.routed?route.data:await command.execute(parsed.data as Record,context); diff --git a/src/errors/index.ts b/src/errors/index.ts index aa0de68..b4ad26a 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -1,3 +1,4 @@ +import {redact} from '../core/logger.js'; export const errorDefaults = { INSUFFICIENT_MARGIN: false, POLICY_REJECTED: false, AGENT_EXPIRED: false, AGENT_NOT_APPROVED: false, DEPOSIT_PENDING: true, NONCE_COLLISION: true, @@ -11,7 +12,7 @@ export class AgentCliError extends Error { constructor(readonly code: ErrorCode, message: string, readonly details: unknown = null, retryable = errorDefaults[code] as boolean) { super(message); this.name = 'AgentCliError'; this.retryable = retryable; } - toJSON() { return { ok: false as const, code: this.code, message: this.message, retryable: this.retryable, details: this.details }; } + toJSON() { return { ok: false as const, code: this.code, message: String(redact(this.message)), retryable: this.retryable, details: redact(this.details) }; } get exitCode() { return this.code === 'CONFIG_INVALID' ? 2 : this.retryable ? 75 : 1; } } export function normalizeError(error: unknown): AgentCliError { diff --git a/src/runner/rpc.ts b/src/runner/rpc.ts index 0b17da9..f454b22 100644 --- a/src/runner/rpc.ts +++ b/src/runner/rpc.ts @@ -1,3 +1,4 @@ +import {assertSafeInput} from '../wallet/safety.js'; import {createServer,createConnection,type Server} from 'node:net'; import {chmod,unlink} from 'node:fs/promises'; import {join} from 'node:path'; @@ -23,7 +24,7 @@ export async function rpcServer(runner:Runner,context:Context,execute?: (name:st if(name==='shutdown'){socket.write(JSON.stringify({jsonrpc:'2.0',id,result:{stopping:true}})+'\n');setImmediate(()=>{void runner.shutdown();});return;} if(!routedCommands.has(name)&&name!=='config') throw new AgentCliError('CONFIG_INVALID','Unknown runner method'); const command=commands.get(name);if(!command&&!execute) throw new AgentCliError('CONFIG_INVALID','Unavailable method'); - const input=name==='config'?{args:['get']}:request.params;const parsed=command?.schema.safeParse(input);if(parsed&&!parsed.success) throw new AgentCliError('CONFIG_INVALID','Invalid runner input'); + assertSafeInput(request.params);const input=name==='config'?{args:['get']}:request.params;const parsed=command?.schema.safeParse(input);if(parsed&&!parsed.success) throw new AgentCliError('CONFIG_INVALID','Invalid runner input'); const data=execute?await execute(name,input):await command!.execute(parsed!.data as Record,{...context,runner:true}); socket.write(JSON.stringify({jsonrpc:'2.0',id,result:data})+'\n'); }catch(error){const normalized=error instanceof SyntaxError||error instanceof z.ZodError?new AgentCliError('CONFIG_INVALID','Invalid JSON-RPC request'):normalizeError(error);socket.write(JSON.stringify({jsonrpc:'2.0',id,error:{code:-32000,message:normalized.message,data:normalized.toJSON()}})+'\n');} diff --git a/src/wallet/approval.ts b/src/wallet/approval.ts index 2a2f44f..1e9ec1e 100644 --- a/src/wallet/approval.ts +++ b/src/wallet/approval.ts @@ -1,3 +1,4 @@ +import {assertSafeInput} from './safety.js'; import {ApproveAgentTypes} from '@bloxwap/hyperliquid/api/exchange/approveAgent'; import {ApproveBuilderFeeTypes} from '@bloxwap/hyperliquid/api/exchange/approveBuilderFee'; import {AgentCliError} from '../errors/index.js'; @@ -15,8 +16,4 @@ export async function submitApproval(network:'mainnet'|'testnet',pending:Approva const response=await jsonRequest(`${hlEndpoint(network)}/exchange`,{action:pending.action,nonce:pending.action.nonce,signature:{r:signature.slice(0,66),s:'0x'+signature.slice(66,130),v}}) as {status:string;response:unknown}; if(response.status!=='ok') throw new AgentCliError('UPSTREAM_ERROR','Approval failed');return response; } -export function rejectMasterKey(value:unknown){ - if(typeof value==='string'&&/^(?:0x)?[a-fA-F0-9]{64}$/.test(value.trim())) throw new AgentCliError('POLICY_REJECTED','Never supply a master private key'); - if(Array.isArray(value)) value.forEach(rejectMasterKey); - else if(value&&typeof value==='object') for(const [key,field] of Object.entries(value)){if(/private.?key|mnemonic|seedPhrase/i.test(key)) throw new AgentCliError('POLICY_REJECTED','Master keys are forbidden');rejectMasterKey(field);} -} +export const rejectMasterKey=assertSafeInput; diff --git a/src/wallet/safety.ts b/src/wallet/safety.ts new file mode 100644 index 0000000..b87e96d --- /dev/null +++ b/src/wallet/safety.ts @@ -0,0 +1,12 @@ +import {wordlist} from '@scure/bip39/wordlists/english.js'; +import {AgentCliError} from '../errors/index.js'; +const words=new Set(wordlist); +export function resemblesSecret(value:string){ + const trimmed=value.trim();if(/^(?:0x)?[0-9a-fA-F]{64}$/.test(trimmed)) return true; + const tokens=trimmed.toLowerCase().split(/\s+/);return [12,15,18,21,24].includes(tokens.length)&&tokens.every(word=>words.has(word)); +} +export function assertSafeInput(value:unknown):void{ + if(typeof value==='string'&&resemblesSecret(value)) throw new AgentCliError('POLICY_REJECTED','Private keys and seed phrases are forbidden; use external wallet signing'); + if(Array.isArray(value)) value.forEach(assertSafeInput); + else if(value&&typeof value==='object') for(const [key,field] of Object.entries(value)){if(/private.?key|seed.?phrase|mnemonic/i.test(key)) throw new AgentCliError('POLICY_REJECTED','Secret key input is forbidden');assertSafeInput(field);} +} diff --git a/test/safety.test.ts b/test/safety.test.ts new file mode 100644 index 0000000..9e2da66 --- /dev/null +++ b/test/safety.test.ts @@ -0,0 +1,10 @@ +import {expect,test} from 'vitest'; +import {assertSafeInput} from '../src/wallet/safety.js'; +import {dispatch} from '../src/core/router.js'; +import {redact} from '../src/core/logger.js'; +test('secret detector covers keys, seeds and nested ignored fields',()=>{ + for(const value of ['ab'.repeat(32),'0x'+'12'.repeat(32),'abandon '.repeat(11)+'about',{ignored:{seedPhrase:'secret'}},['abandon '.repeat(12).trim()]]) expect(()=>assertSafeInput(value)).toThrow();assertSafeInput({signature:'0x'+'12'.repeat(65),address:'0x'+'a'.repeat(40)}); +}); +test('environment secrets are scrubbed from arbitrary messages',async()=>{ + process.env.BLOXWAP_SECRET_TOKEN='sensitive-value';try{expect(redact('failure: sensitive-value')).toBe('failure: [REDACTED]');expect((await dispatch(['config','--json',JSON.stringify({privateKey:'ab'.repeat(32)})])).body).toMatchObject({ok:false});}finally{delete process.env.BLOXWAP_SECRET_TOKEN;} +}); From bd8183ace7b0e64bb6fa91d7ba696ec8f5bce8e0 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:42:55 -0700 Subject: [PATCH 37/43] feat: document agent-native trading skill (#37) --- SKILL.md | 106 +++++++++++++++++++++++++++++++++++++++++++++++ docs/PROGRESS.md | 1 + 2 files changed, 107 insertions(+) create mode 100644 SKILL.md diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..a568afe --- /dev/null +++ b/SKILL.md @@ -0,0 +1,106 @@ +--- +name: agent-cli +description: Operate the agent-cli Hyperliquid trading CLI, including managed-agent onboarding, funding, orders, event monitoring, rotation, and normalized error recovery. +metadata: + version: "0.1.0" +--- + +Use `agent-cli` for the user's authorized Hyperliquid account operations. Obtain +command schemas with `agent-cli schema COMMAND`; arguments can be flags or +`--json '{...}'`, or a JSON object on stdin. Ordinary commands print exactly one +JSON object: `{ok:true,data}` or `{ok:false,code,message,retryable,details}`. +Logs/QR appear on stderr. `events --follow` and `logs` explicitly stream JSON lines. +Use `--network testnet` during testing; never silently change to mainnet. + +## Onboarding sequence + +1. Configure Privy app ID and secret reference, e.g. `config set privyAppId APP` + and `config set privyAppSecretRef env:BLOXWAP_PRIVY_APP_SECRET`. The user sets + the environment secret outside the conversation. Configure published builder + address/fee if applicable; unset defaults charge no builder fee. +2. Run `init --master-address 0x... --network testnet`. Show the returned approval + payload/deeplink/QR. The user signs in their master wallet; submit only the + resulting signature with `init --signature 0x...`. Submit builder approval + separately if requested. Continue only after `approved:true`. +3. Run `fund --amount 10 --origin-chain 42161`. Show `qrAscii`/deposit address, + origin chain/token, minimum, fees, expected received amount and expiry. The user + transfers the quoted origin token. If `signatureSteps` require signatures, + show the exact payload and submit each via `deposit` in returned order. +4. Poll `status` with bounded backoff until each active deposit is `settled`. + Settlement requires Relay success and Hyperliquid balance receipt. Stop on + failed/refunded deposits and explain their status. Do not place orders while + any deposit is awaiting funds or pending. +5. Run `run --detach`, then monitor `events --follow --since SEQ`. Persist the + last handled sequence; replay after reconnect. A fill updates the trading + plan, an error follows the catalog below, and expiry warnings prompt rotation + using external master-wallet approval. + +## Commands and example envelopes + +Examples show representative `data`, not live balances or service promises. +Full inputs/outputs are discoverable with `schema`; amounts/prices are strings. + +| Input | Example output | +| --- | --- | +| `--help` | `{"ok":true,"data":{"commands":[{"name":"status","description":"Read account status"}],"globalFlags":["--json","--config","--network","--pretty"]}}` | +| `schema order` | `{"ok":true,"data":{"input":{"type":"object"},"output":{"oneOf":[]}}}` (actual schemas include properties) | +| `config get network` | `{"ok":true,"data":"testnet"}` | +| `config set limits.maxLeverage 5` | `{"ok":true,"data":{"version":1,"limits":{"maxLeverage":5}}}` | +| `config validate` | `{"ok":true,"data":{"valid":true,"version":1}}` | +| `init --master-address 0x...` | `{"ok":true,"data":{"approved":false,"requiresSignature":true,"approvals":{"agent":{"typedData":{}}},"qrAscii":"..."}}` | +| `fund --amount 10 --no-qr` | `{"ok":true,"data":{"depositId":"...","depositAddress":"0x...","expectedReceived":"100000000","builderFeeRate":"0%","state":"awaiting_funds","qrAscii":null}}` | +| `deposit --deposit-id ID` | `{"ok":true,"data":{"depositId":"ID","steps":[{"id":"step:0","requiresSignature":true,"payload":{}}]}}` | +| `deposit --deposit-id ID --step-id step:0 --signature 0x...` | `{"ok":true,"data":{"depositId":"ID","steps":[]}}` | +| `status` | `{"ok":true,"data":{"balance":{"accountValue":"100"},"agent":{"approved":true,"daysRemaining":"179"},"deposits":[],"policy":{"id":"...","version":"1"},"runner":{"pid":"123"}}}` | +| `balance` | `{"ok":true,"data":{"accountValue":"100","accountValueUnits":"10000000000","withdrawable":"96","marginUsed":"4"}}` | +| `positions` | `{"ok":true,"data":[{"coin":"BTC","size":"0.001","entryPrice":"20000","leverage":"5","marginMode":"cross"}]}` | +| `orders` | `{"ok":true,"data":[{"coin":"BTC","oid":"123","sz":"0.001","limitPx":"20000","side":"B"}]}` | +| `order --json '{"coin":"BTC","side":"buy","size":"0.001","price":"20000","tif":"Gtc","cloid":"0x11111111111111111111111111111111"}'` | `{"ok":true,"data":{"result":{"status":"ok","response":{"data":{"statuses":[{"resting":{"oid":123}}]}}},"builderFeeRate":"0%","cloid":"0x..."}}` | +| `order --json '{"coin":"BTC","side":"sell","size":"0.001","type":"market","slippage":0.01,"reduceOnly":true}'` | `{"ok":true,"data":{"result":{"status":"ok","response":{"data":{"statuses":[{"filled":{"oid":123}}]}}}}}` | +| `cancel --oid 123` or `cancel --all` | `{"ok":true,"data":{"cancelled":["123"],"result":{"status":"ok"}}}` | +| `modify --json '{"modifications":[{"oid":123,"price":"20100","size":"0.001"}]}'` | `{"ok":true,"data":{"status":"ok"}}` | +| `leverage --coin BTC --leverage 5 --mode isolated` | `{"ok":true,"data":{"status":"ok"}}` | +| `run --detach` | `{"ok":true,"data":{"pid":123,"startedAt":1700000000000,"network":"testnet"}}` | +| `events --since 12 --types '["fill"]'` | `{"ok":true,"data":[{"seq":13,"timestamp":"2026-01-01T00:00:00.000Z","type":"fill","data":{"tid":1}}]}` | +| `events --follow` | `{"seq":13,"timestamp":"2026-01-01T00:00:00.000Z","type":"fill","data":{"tid":1}}` (one line per event) | +| `logs --follow --level warn` | `{"level":"warn","requestId":"...","message":"Risk rejected","time":"..."}` (JSON lines) | +| `rotate` then `rotate --signature 0x...` | `{"ok":true,"data":{"oldAgent":"0x...","newAgent":"0x...","swapped":true,"oldRevoked":false,"revokeApproval":{}}}` | +| `rotate --revoke-signature 0x...` | `{"ok":true,"data":{"swapped":true,"oldRevoked":true}}` | +| `revoke --cancel-orders` | `{"ok":true,"data":{"locallyRevoked":true,"exchangeRevoked":false,"requiresSignature":true,"approval":{}}}` | +| `revoke --signature 0x...` | `{"ok":true,"data":{"locallyRevoked":true,"exchangeRevoked":true}}` | +| `audit --intent ID` | `{"ok":true,"data":[{"intentId":"ID","stage":"decision","data":{"allowed":true},"previousHash":"...","hash":"..."}]}` | +| `skill install --target codex` | `{"ok":true,"data":{"target":"codex","path":".../agent-cli/SKILL.md","version":"0.1.0"}}` | + +Use `--intent-id ID --intent "reason"` on trading actions to connect the user's +reasoning to the audit. Keep cloid stable when retrying a known order intent. +For TP/SL use `type:"tp"|"sl"` and `triggerPrice` with size/price/reduceOnly; +market orders use an explicit slippage-bounded IOC, not an unbounded market order. + +## Error recovery + +| Code | Action | +| --- | --- | +| RATE_LIMIT, NETWORK_ERROR | Bounded exponential backoff; stop after configured attempts | +| NONCE_COLLISION | Retry using a newly issued nonce; inspect a stale lock if persistent | +| DEPOSIT_PENDING | Poll status until settled; stop on terminal funding failure | +| RUNNER_NOT_RUNNING | Start runner or use configured direct fallback | +| INSUFFICIENT_MARGIN | Reduce exposure or request authorized funding | +| POLICY_REJECTED | Stop and revise intent within limits; do not bypass policy | +| AGENT_EXPIRED | Rotate with master-wallet approval | +| AGENT_NOT_APPROVED | Complete init/approval or explain revocation | +| INVALID_SIZE, INVALID_PRICE | Correct input using market precision | +| UNKNOWN_MARKET | Refresh metadata; use an allowed market | +| ORDER_NOT_FOUND | Refresh orders before changing intent | +| CONFIG_INVALID | Correct configuration/input; never insert secrets | +| UPSTREAM_ERROR, INTERNAL | Stop, inspect redacted details/logs and report failure | + +## Gotchas + +HyperCore perps USDC uses **8 decimals**; EVM USDC uses **6**. Preserve strings and +bigints. Some deposits require **two distinct signatures**; resume stored steps +rather than restarting the route. Agent expiry is at most **180 days**. Never +ask for, paste, log or import a master private key or seed. The managed agent is +trade-only; local limits include markets, size/notional, leverage, daily loss and +open orders. Privy sees an L1 action hash, so it cannot independently inspect +notional/action contents; Hyperliquid agent permissions and local checks enforce +those boundaries. State is always `~/.bloxwap`, env prefix `BLOXWAP_`. diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index a7edb8a..dc53ab5 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -39,3 +39,4 @@ | #34 | done | Optional cancel-all, resumable master revocation payload, immediate local blocking and config/session clearing after exchange submission. | | #35 | done | Cross-process locked fsynced hash-chain audit, intent/decision/signature digest/response/fill linkage, rotation/revoke records and filters; tamper/redaction tests. | | #36 | done | Pre-schema recursive CLI/RPC secret detector, BIP39-like seed rejection, private authorization refs and env/log/error scrubbing; threat model docs. | +| #37 | partial | Validated self-contained skill with every command example, ordered funding/custody flow and complete error table; real agent testnet execution requires credentials (#39 gate). | From ed8d4349ac4b54e7df0718372c26f63a344e758b Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:44:09 -0700 Subject: [PATCH 38/43] feat: generate command reference and documentation site (#38) --- .github/workflows/docs.yml | 23 + .gitignore | 1 + README.md | 64 +- docs/PROGRESS.md | 1 + docs/commands.md | 1405 ++++++++++++++++++++++++++++++++++++ docs/index.md | 9 + package-lock.json | 13 + package.json | 80 +- scripts/generate-docs.mjs | 10 + 9 files changed, 1564 insertions(+), 42 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 docs/commands.md create mode 100644 docs/index.md create mode 100644 scripts/generate-docs.mjs diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..69a3401 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,23 @@ +name: Documentation +on: + workflow_dispatch: +permissions: + contents: read + pages: write + id-token: write +jobs: + docs: + runs-on: ubuntu-latest + environment: github-pages + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run docs:generate + - uses: actions/upload-pages-artifact@v3 + with: + path: site + - uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 242599d..63d889f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ dist/ coverage/ .env *.tgz +site/ diff --git a/README.md b/README.md index d180633..3854160 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,65 @@ # agent-cli -Agent-native Hyperliquid trading CLI. Requires Node 20+; Bun supported. +An agent-native Hyperliquid trading CLI with JSON input/output, a persistent +signing runner, Privy managed agents, Relay funding, and a packaged agent skill. -Development: `npm ci`, then `npm run build`, `npm run lint`, `npm run typecheck`, `npm test`. +Install: `npm i -g @bloxwap/agent-cli` or `bunx @bloxwap/agent-cli --help`. +The requested Hyperliquid SDK declares Node **22.12+**; use that supported runtime +or current Bun. No postinstall script. Publishing is release-gated. -Local state lives in `~/.bloxwap`; configuration overrides use `BLOXWAP_`. +## Testnet quickstart + +```sh +npm ci +npm run build +node dist/cli.js --help +node dist/cli.js config set privyAppId YOUR_APP_ID +node dist/cli.js config set privyAppSecretRef env:BLOXWAP_PRIVY_APP_SECRET +# Set BLOXWAP_PRIVY_APP_SECRET securely outside the CLI/conversation. +node dist/cli.js init --network testnet --master-address 0xYOUR_MASTER_ADDRESS +# Sign returned EIP-712 approval externally, then submit its 65-byte signature: +node dist/cli.js init --network testnet --signature 0xYOUR_SIGNATURE +node dist/cli.js fund --network testnet --amount 10 --origin-chain 42161 +# Show QR/address and exact origin token/fees to user; complete any deposit steps. +node dist/cli.js status --network testnet +# Continue only when active deposits report settled. +node dist/cli.js run --network testnet --detach +node dist/cli.js order --json '{"coin":"BTC","side":"buy","size":"0.001","price":"20000","tif":"Gtc"}' +node dist/cli.js events --follow +``` + +Testnet onboarding/trades require Privy sandbox credentials, an externally signed +testnet master approval and testnet collateral. Relay HyperCore testnet routes may +be unavailable; the e2e suite mocks Relay. The placeholders above must be replaced +with addresses/signatures from your test environment. Never paste a private key. + +## Architecture and custody + +```mermaid +flowchart LR + Agent[Agent / CLI] -->|private JSON-RPC| Runner[Persistent runner] + Runner -->|P-256 authorization| Privy[Privy managed agent] + Privy -->|EIP-712 signature| HL[Hyperliquid exchange] + Agent --> Relay[Relay funding quote] + Master[External master wallet] -->|one-time approvals| HL + Runner --> Journal[Events / audit / logs] +``` + +Master custody stays external. Privy owns the separate agent key; the CLI holds +only an authorization key. Hyperliquid agent permissions deny withdrawal. +Privy policies restrict signing domains; local checks enforce configured risk +limits. Read [custody limits](docs/custody.md) before using a funded account. + +State: `~/.bloxwap`; config: `~/.bloxwap/config.json`; IPC: +`~/.bloxwap/run.sock`; env prefix: `BLOXWAP_`. All local state is private to the OS +user. Configure the official builder address/rate explicitly when available; +defaults charge zero fees instead of inventing a recipient. + +## Development and docs + +`npm run build`, `npm run lint`, `npm run typecheck`, `npm test` must pass before +committing. Unit tests mock services. Credential-dependent tests are gated. + +`npm run docs:generate` creates [command schemas](docs/commands.md) and a static +site in `site/`. [Docs index](docs/index.md), [security](docs/security.md), +[error catalog](docs/errors.md), and [SKILL.md](SKILL.md) explain operation. diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index dc53ab5..2de6fa9 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -40,3 +40,4 @@ | #35 | done | Cross-process locked fsynced hash-chain audit, intent/decision/signature digest/response/fill linkage, rotation/revoke records and filters; tamper/redaction tests. | | #36 | done | Pre-schema recursive CLI/RPC secret detector, BIP39-like seed rejection, private authorization refs and env/log/error scrubbing; threat model docs. | | #37 | partial | Validated self-contained skill with every command example, ordered funding/custody flow and complete error table; real agent testnet execution requires credentials (#39 gate). | +| #38 | done | Install/testnet README, custody architecture, schema-generated command reference and static docs site with gated Pages workflow. | diff --git a/docs/commands.md b/docs/commands.md new file mode 100644 index 0000000..88a3e72 --- /dev/null +++ b/docs/commands.md @@ -0,0 +1,1405 @@ +# Command reference + +Generated from `agent-cli schema `. Do not edit by hand. + +Global flags: `--config PATH`, `--network mainnet|testnet`, `--pretty`, `--json OBJECT`. + +## config + +Get, set or validate local configuration + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "args": { + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "args" + ], + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": {} + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## init + +Create agent and emit resumable master-wallet approvals + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "masterAddress": { + "type": "string", + "pattern": "^0x[0-9a-fA-F]{40}$" + }, + "validUntil": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "signature": { + "type": "string" + }, + "builderSignature": { + "type": "string" + }, + "qr": { + "default": true, + "type": "boolean" + } + }, + "required": [ + "qr" + ], + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": {} + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## fund + +Quote funding with address, QR and transparent fees + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "originChain": { + "default": 42161, + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "originCurrency": { + "default": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "type": "string" + }, + "originDecimals": { + "default": 6, + "type": "integer", + "minimum": 0, + "maximum": 30 + }, + "amount": { + "default": "10", + "type": "string" + }, + "qr": { + "default": true, + "type": "boolean" + }, + "qrPath": { + "type": "string" + } + }, + "required": [ + "originChain", + "originCurrency", + "originDecimals", + "amount", + "qr" + ], + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": {} + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## deposit + +Resume deposit steps or submit a master-wallet signature + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "depositId": { + "type": "string" + }, + "stepId": { + "type": "string" + }, + "signature": { + "type": "string" + } + }, + "required": [ + "depositId" + ], + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": {} + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## status + +Read account status + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": {} + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## balance + +Read account balance + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": {} + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## positions + +Read account positions + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": {} + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## orders + +Read account orders + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": {} + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## order + +Place limit, IOC market or TP/SL order + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "coin": { + "type": "string" + }, + "side": { + "type": "string", + "enum": [ + "buy", + "sell" + ] + }, + "size": { + "type": "string" + }, + "price": { + "type": "string" + }, + "type": { + "default": "limit", + "type": "string", + "enum": [ + "limit", + "market", + "tp", + "sl" + ] + }, + "tif": { + "default": "Gtc", + "type": "string", + "enum": [ + "Gtc", + "Ioc", + "Alo" + ] + }, + "reduceOnly": { + "default": false, + "type": "boolean" + }, + "cloid": { + "type": "string", + "pattern": "^0x[0-9a-fA-F]{32}$" + }, + "triggerPrice": { + "type": "string" + }, + "slippage": { + "default": 0.01, + "type": "number", + "minimum": 0, + "maximum": 0.1 + }, + "intent": { + "type": "string" + }, + "intentId": { + "type": "string" + } + }, + "required": [ + "coin", + "side", + "size", + "type", + "tif", + "reduceOnly", + "slippage" + ], + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": {} + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## cancel + +Cancel by oid/cloid, coin or all + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "oid": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "oids": { + "type": "array", + "items": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + }, + "cloid": { + "type": "string" + }, + "cloids": { + "type": "array", + "items": { + "type": "string" + } + }, + "coin": { + "type": "string" + }, + "all": { + "default": false, + "type": "boolean" + }, + "intent": { + "type": "string" + }, + "intentId": { + "type": "string" + } + }, + "required": [ + "all" + ], + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": {} + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## modify + +Modify price/size of single or batch resting orders + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "oid": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "cloid": { + "type": "string", + "pattern": "^0x[0-9a-fA-F]{32}$" + }, + "size": { + "type": "string" + }, + "price": { + "type": "string" + }, + "modifications": { + "type": "array", + "items": { + "type": "object", + "properties": { + "oid": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "cloid": { + "type": "string", + "pattern": "^0x[0-9a-fA-F]{32}$" + }, + "size": { + "type": "string" + }, + "price": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "intent": { + "type": "string" + }, + "intentId": { + "type": "string" + } + }, + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": {} + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## leverage + +Set market leverage and cross/isolated margin mode + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "coin": { + "type": "string" + }, + "leverage": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "mode": { + "default": "cross", + "type": "string", + "enum": [ + "cross", + "isolated" + ] + }, + "intent": { + "type": "string" + }, + "intentId": { + "type": "string" + } + }, + "required": [ + "coin", + "leverage", + "mode" + ], + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": {} + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## run + +Start persistent signing runner (foreground or detach) + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "detach": { + "default": false, + "type": "boolean" + }, + "daemon": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "detach", + "daemon" + ], + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": {} + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## events + +Replay or follow JSON-line runner events + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "follow": { + "default": false, + "type": "boolean" + }, + "since": { + "default": 0, + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "types": { + "default": [], + "type": "array", + "items": { + "type": "string", + "enum": [ + "fill", + "order_update", + "position_change", + "deposit", + "error", + "agent_expiry_warning", + "runner_state" + ] + } + } + }, + "required": [ + "follow", + "since", + "types" + ], + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": {} + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## logs + +Read/follow redacted JSON-line runner logs + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "follow": { + "default": false, + "type": "boolean" + }, + "level": { + "type": "string", + "enum": [ + "debug", + "info", + "warn", + "error" + ] + }, + "since": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + "requestId": { + "type": "string" + } + }, + "required": [ + "follow" + ], + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": {} + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## rotate + +Approve new agent, hot-swap signer, then revoke old agent + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "signature": { + "type": "string" + }, + "revokeSignature": { + "type": "string" + }, + "intentId": { + "type": "string" + }, + "intent": { + "type": "string" + } + }, + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": {} + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## revoke + +Disable local agent and prepare master-signed exchange revocation + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "cancelOrders": { + "default": false, + "type": "boolean" + }, + "signature": { + "type": "string" + }, + "intentId": { + "type": "string" + }, + "intent": { + "type": "string" + } + }, + "required": [ + "cancelOrders" + ], + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": {} + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## audit + +Verify and query the hash-chained trading audit + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "intent": { + "type": "string" + }, + "intentId": { + "type": "string" + }, + "cloid": { + "type": "string" + } + }, + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": {} + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..09f8315 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,9 @@ +# agent-cli documentation + +Use the [command reference](commands.md) for JSON schemas, [configuration](config.md) +for state/env settings, [funding](funding.md) for deposits/signatures, +[errors](errors.md) for recovery, [events](events.md) for streaming, +[custody](custody.md) and [security](security.md) for key boundaries. + +Development progress is recorded in [PROGRESS](PROGRESS.md). Documentation is +built with `npm run docs:generate` into `site/`; serve that directory locally. diff --git a/package-lock.json b/package-lock.json index 357966c..34002b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@types/qrcode": "^1.5.6", "@types/ws": "^8.18.2", "eslint": "^9.39.5", + "marked": "^18.1.1", "prettier": "^3.9.10", "tsup": "^8.5.1", "typescript": "^5.9.3", @@ -2547,6 +2548,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/marked": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.1.1.tgz", + "integrity": "sha512-zEGgrtUG8crk+TrMacJSVABU4DYDDkS8uqgU3ohrn/2fW9C2AEfUrO3m6IhpIpQpl7YcVpiXXC9cSMWDANT3dg==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", diff --git a/package.json b/package.json index b22fee2..1927d1f 100644 --- a/package.json +++ b/package.json @@ -1,41 +1,43 @@ { - "name": "@bloxwap/agent-cli", - "version": "0.1.0", - "type": "module", - "bin": { - "agent-cli": "dist/cli.js" - }, - "engines": { - "node": ">=20" - }, - "files": [ - "dist", - "SKILL.md", - "policies" - ], - "scripts": { - "build": "tsup src/cli.ts --format esm --clean", - "lint": "eslint .", - "typecheck": "tsc --noEmit", - "test": "vitest run" - }, - "devDependencies": { - "@types/node": "^26.6.5", - "@types/qrcode": "^1.5.6", - "@types/ws": "^8.18.2", - "eslint": "^9.39.5", - "prettier": "^3.9.10", - "tsup": "^8.5.1", - "typescript": "^5.9.3", - "typescript-eslint": "^8.71.2", - "vitest": "^3.2.7" - }, - "dependencies": { - "@bloxwap/hyperliquid": "^0.6.0", - "@privy-io/node": "^0.35.0", - "@scure/bip39": "^2.4.0", - "qrcode": "^1.5.4", - "ws": "^8.22.0", - "zod": "^4.6.7" - } + "name": "@bloxwap/agent-cli", + "version": "0.1.0", + "type": "module", + "bin": { + "agent-cli": "dist/cli.js" + }, + "engines": { + "node": ">=20" + }, + "files": [ + "dist", + "SKILL.md", + "policies" + ], + "scripts": { + "build": "tsup src/cli.ts --format esm --clean", + "lint": "eslint .", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "docs:generate": "npm run build && node scripts/generate-docs.mjs" + }, + "devDependencies": { + "@types/node": "^26.6.5", + "@types/qrcode": "^1.5.6", + "@types/ws": "^8.18.2", + "eslint": "^9.39.5", + "marked": "^18.1.1", + "prettier": "^3.9.10", + "tsup": "^8.5.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.71.2", + "vitest": "^3.2.7" + }, + "dependencies": { + "@bloxwap/hyperliquid": "^0.6.0", + "@privy-io/node": "^0.35.0", + "@scure/bip39": "^2.4.0", + "qrcode": "^1.5.4", + "ws": "^8.22.0", + "zod": "^4.6.7" + } } diff --git a/scripts/generate-docs.mjs b/scripts/generate-docs.mjs new file mode 100644 index 0000000..503cb53 --- /dev/null +++ b/scripts/generate-docs.mjs @@ -0,0 +1,10 @@ +import {execFileSync} from 'node:child_process'; +import {writeFileSync,readFileSync,mkdirSync,readdirSync} from 'node:fs'; +import {marked} from 'marked'; +const invoke=(args)=>JSON.parse(execFileSync(process.execPath,['dist/cli.js',...args],{input:'',encoding:'utf8'})).data; +const commands=invoke(['--help']).commands; +let reference='# Command reference\n\nGenerated from `agent-cli schema `. Do not edit by hand.\n\nGlobal flags: `--config PATH`, `--network mainnet|testnet`, `--pretty`, `--json OBJECT`.\n\n'; +for(const command of commands) reference+=`## ${command.name}\n\n${command.description}\n\n\`\`\`json\n${JSON.stringify(invoke(['schema',command.name]),null,2)}\n\`\`\`\n\n`; +writeFileSync('docs/commands.md',reference); +mkdirSync('site',{recursive:true}); +for(const name of readdirSync('docs').filter(name=>name.endsWith('.md'))){const content=marked(readFileSync(`docs/${name}`,'utf8')).replace(/href="([^"#]+)\.md/g,'href="$1.html');writeFileSync(`site/${name.replace('.md','.html')}`,`agent-cli${content}`);} From e13ffc3bb933f1a938b0da0c085af107f9abdc58 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:52:34 -0700 Subject: [PATCH 39/43] feat: add mocked and gated testnet lifecycle suites (#39) --- .github/workflows/e2e.yml | 31 + .gitignore | 2 + docs/PROGRESS.md | 1 + package-lock.json | 7788 +++++++++++++++++++----------------- package.json | 4 +- scripts/e2e-relay-hook.mjs | 10 + scripts/e2e-testnet.mjs | 31 + test/lifecycle.test.ts | 56 + 8 files changed, 4152 insertions(+), 3771 deletions(-) create mode 100644 .github/workflows/e2e.yml create mode 100644 scripts/e2e-relay-hook.mjs create mode 100644 scripts/e2e-testnet.mjs create mode 100644 test/lifecycle.test.ts diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..e3ca238 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,31 @@ +name: Testnet lifecycle +on: + schedule: + - cron: '23 4 * * *' + workflow_dispatch: +permissions: + contents: read +jobs: + testnet: + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: testnet + env: + BLOXWAP_E2E: '1' + BLOXWAP_TEST_MASTER_KEY: ${{ secrets.BLOXWAP_TEST_MASTER_KEY }} + BLOXWAP_PRIVY_APP_ID: ${{ secrets.BLOXWAP_PRIVY_APP_ID }} + BLOXWAP_PRIVY_APP_SECRET: ${{ secrets.BLOXWAP_PRIVY_APP_SECRET }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run build + - run: npm run test:e2e + - uses: actions/upload-artifact@v4 + if: always() + with: + name: testnet-journals + path: artifacts/ diff --git a/.gitignore b/.gitignore index 63d889f..0d564fa 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ coverage/ .env *.tgz site/ +artifacts/ +.bloxwap/ diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 2de6fa9..2acf254 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -41,3 +41,4 @@ | #36 | done | Pre-schema recursive CLI/RPC secret detector, BIP39-like seed rejection, private authorization refs and env/log/error scrubbing; threat model docs. | | #37 | partial | Validated self-contained skill with every command example, ordered funding/custody flow and complete error table; real agent testnet execution requires credentials (#39 gate). | | #38 | done | Install/testnet README, custody architecture, schema-generated command reference and static docs site with gated Pages workflow. | +| #39 | partial | Full mocked CLI/SDK/RPC lifecycle passes; opt-in real testnet lifecycle with mocked Relay, nightly secrets gate and journal artifacts. Real execution skipped without credentials. | diff --git a/package-lock.json b/package-lock.json index 34002b1..0d98842 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,3774 +1,4022 @@ { - "name": "@bloxwap/agent-cli", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@bloxwap/agent-cli", - "version": "0.1.0", - "dependencies": { - "@bloxwap/hyperliquid": "^0.6.0", - "@privy-io/node": "^0.35.0", - "@scure/bip39": "^2.4.0", - "qrcode": "^1.5.4", - "ws": "^8.22.0", - "zod": "^4.6.7" - }, - "bin": { - "agent-cli": "dist/cli.js" - }, - "devDependencies": { - "@types/node": "^26.6.5", - "@types/qrcode": "^1.5.6", - "@types/ws": "^8.18.2", - "eslint": "^9.39.5", - "marked": "^18.1.1", - "prettier": "^3.9.10", - "tsup": "^8.5.1", - "typescript": "^5.9.3", - "typescript-eslint": "^8.71.2", - "vitest": "^3.2.7" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@bloxwap/hyperliquid": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@bloxwap/hyperliquid/-/hyperliquid-0.6.0.tgz", - "integrity": "sha512-ryQya36sx5tD0xg9USFg+6krpMvPlHvc55p42FXJlCN6RTWO9obA6QknZpM1RRTPAl5w04Jj16kQxLcesEWl0Q==", - "dependencies": { - "@noble/hashes": "2.2.0", - "valibot": "1.4.2" - }, - "engines": { - "node": ">=22.12.0" - }, - "optionalDependencies": { - "hash-wasm": "4.12.0", - "tiny-secp256k1": "2.2.4" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", - "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", - "dev": true, - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.5" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", - "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", - "dev": true, - "dependencies": { - "ajv": "^6.14.0", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.3.2", - "minimatch": "^3.1.5", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.5", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", - "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@hpke/chacha20poly1305": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@hpke/chacha20poly1305/-/chacha20poly1305-1.8.0.tgz", - "integrity": "sha512-FcBfAQ+Y99vMNJP2yrZ9wpL8V0GOwp1+zMyzvc6alasrBygfFjFm1yeUtyADJCu/27C3Lm5mJzx6u7pwg+cX5w==", - "dependencies": { - "@hpke/common": "^1.10.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@hpke/common": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@hpke/common/-/common-1.10.1.tgz", - "integrity": "sha512-moJwhmtLtuxiUzzNp1jpfBfx8yefKoO9D/RCR9dmwrnc7qjJqId1rEtQz+lSlU5cabX8daToMSx/7HayXOiaFw==", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@hpke/core": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@hpke/core/-/core-1.9.0.tgz", - "integrity": "sha512-pFxWl1nNJeQCSUFs7+GAblHvXBCjn9EPN65vdKlYQil2aURaRxfGMO6vBKGqm1YHTKwiAxJQNEI70PbSowMP9Q==", - "dependencies": { - "@hpke/common": "^1.10.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", - "dev": true, - "dependencies": { - "@humanfs/types": "^0.15.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", - "dev": true, - "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", - "dev": true, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", - "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@napi-rs/lzma-linux-x64-gnu": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", - "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^22.20 || ^24.12 || >=25" - } - }, - "node_modules/@noble/curves": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", - "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", - "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/curves/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", - "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@privy-io/node": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@privy-io/node/-/node-0.35.0.tgz", - "integrity": "sha512-NhttAOmjgwtQzBoTmRCWzY5ZTTJV1WqPUmasyEbi9XfFd1LccpjHiNB+AUCeRXpY0FC5Bt/3Is7xJEmCX3LKNA==", - "dependencies": { - "@hpke/chacha20poly1305": "^1.7.1", - "@hpke/core": "^1.7.5", - "@noble/curves": "^1.9.7", - "@noble/hashes": "^1.8.0", - "@scure/base": "^1.2.5", - "canonicalize": "^2.1.0", - "jose": "^6.1.0", - "lru-cache": "^11.1.0", - "svix": "^1.92.2" - }, - "peerDependencies": { - "@solana/kit": "^5.1.0", - "@x402/evm": "^2.3.0", - "@x402/fetch": "^2.3.0", - "@x402/svm": "^2.3.0", - "viem": "^2.44.2" - }, - "peerDependenciesMeta": { - "@solana/kit": { - "optional": true - }, - "@x402/evm": { - "optional": true - }, - "@x402/fetch": { - "optional": true - }, - "@x402/svm": { - "optional": true - }, - "viem": { - "optional": true - } - } - }, - "node_modules/@privy-io/node/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.64.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.6.tgz", - "integrity": "sha512-FkO19Fowv/OQeJZPVfdetJaI3/VpbZmBsnmAlR/rQ+b3QwfiGQiOpVegpSRH331kelvHTVw9hXYG3DwjAaRzyQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.64.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.6.tgz", - "integrity": "sha512-si5/Etm6MphPc2xYHnk455Shls/hoMjTy3mhXge4OPmvCw7TwC4KP4fKokAlKLV4XaQvLvgSHfafPcblLMYYdQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.64.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.6.tgz", - "integrity": "sha512-PQCl3q7b0BgvVt72i2xrWM6I5KliwwVe4GpWW5QDGyUQawDS9aPZss/aGyFqBmxdRxlE8wDc9CaxiQ5DHRSp/Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.64.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.6.tgz", - "integrity": "sha512-l9DFl1Itb1YYdSrP1K3U8ih44AOHa/p+Q4WMMDfGvH5qVWp+ajfgS1DlYo4yCRh8wJ2ZJv0Fqccf+ULAmMFjmg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.64.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.6.tgz", - "integrity": "sha512-bR4GR3205N9+od0+l5Dd9/iZTDpJkIe6d6Ydp2qbcQZJLmKBdYQQRp8gbWpbMt92twKJY66o1vK4NkoKQODIrA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.64.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.6.tgz", - "integrity": "sha512-XTVNaDzcFl2TeBglvxdGgsCv9JGtAGRAoJENmGBR/XvDH2mA2K0IXc2aMfP0dR8mexSXSKifJ85FixoXX1wfTw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.64.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.6.tgz", - "integrity": "sha512-qWc6qbc+25ugG9vqPR9P422KJBbVfZ05mjcMggoq8tsfWP+eRfC+/tiDdIlRF5VL2pDVZWN9zAhWsmu7R1+7Dw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.64.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.6.tgz", - "integrity": "sha512-V4Nwkp8844X/Xnww3PI1LYS0YPei8XQEVG70R+5MPZGO7AhYoun4hCN7Svs4U3wbQLZbwiz+9xRFhUZZdRl8Ww==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.64.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.6.tgz", - "integrity": "sha512-xb72A1aPaaIsp+a88VtRae/ovIEXislbUMyp7b9AQQU7ZkP2m/+ik6DW9TQcJyw5R8/RIailF9zExYwkjR++UA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.64.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.6.tgz", - "integrity": "sha512-pbcs1BVBwC43hZkjhjlF8Rx8m5G0KnhLZs9Azy/gqccP6UsNSzJr0hKGIAVPqzAH1O/NdPxxC9GVGb4msQg/aw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.64.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.6.tgz", - "integrity": "sha512-EKOC8os6Vsg487ZLKUqzLF99Jsmnq6tu7O7mAELakNpcA+8KadktFf/JYdRuHe/DzCUXmN38pUKu4XBcKlqGbQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.64.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.6.tgz", - "integrity": "sha512-v/SQ68GnSlr/HyjB79sNXesGDwkOPu9JjbHfmWaymCiM+nZsQ9TstG1KfA4MuEvTr2d3PoY8PhFxj9bt0e32JA==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.64.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.6.tgz", - "integrity": "sha512-G3pmliMQ7r15zFFNwBlaT/xTdSLi4lfgI7CUrQueyKYheNVhrd8JrWCE6Ll8xf4AA1RZFqq+mNJYntiElsVOyA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.64.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.6.tgz", - "integrity": "sha512-XQTB/IPoB/wG6x/wu9uomAt8YH61IAr5iZTubMiad/lu88sotq2ocz9hJd556+Jz2kIkF7ZQZIsUuWE3OWqtcA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.64.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.6.tgz", - "integrity": "sha512-O5mpokth6+ydMuwiGtY8fIjvOBfH7u6SzmDNmZ3k6GK5C6iEAC2q2ZN/T5L9jYILx0IcCT0KnfMDihQ5yQFSBw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.64.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.6.tgz", - "integrity": "sha512-aIFxbAD9v3TvIaEDsKmukjcDg1JS4/tFXw8SHyQQgzOwzWCQ4cyDK7Ju1pQKqUXDFkEu/BZCFDCDATsKhQcB/Q==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.64.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.6.tgz", - "integrity": "sha512-fuNo1UwBc6BxnHDOxxguQL/InhheMN3RJcJMb0b4xP8r5Q3CNCrJbVg0eCIFuJ4e/zAfjR2gYbOfbzO/G7CIyw==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.64.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.6.tgz", - "integrity": "sha512-05v6wFKDKJPZs9xlytVrrRu6IbRuoSvlH9xNwEOafXsKyjBFZPvAcZz2VIAB/M+lblYeKa9iOYC4RDVdfcAAaw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.64.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.6.tgz", - "integrity": "sha512-wJXKcdMjvkc/FaavZXNP1yW4i0pfOmhZAIWRSLG2yJ0PB0TWi7dkLErhX0u0vptDcXKKO/m1Y/YTeuyvb2MXwg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.64.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.6.tgz", - "integrity": "sha512-cwg7/h0ThvJwy8CBkmACdiHAxRWpbsCmIsjOt+Zzx0hTnIcSUmKYEV03bGg+hD827OnwUsbKh1ZZDW754xWyBA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.64.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.6.tgz", - "integrity": "sha512-T0RkYIJbdYXJEQFw+tMBO09KDFdgmNxkHsof3wWAABrvnx15Yh8RytmrgYy9+q9cBeBNZF66MMQZC+5nzEz6vg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.64.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.6.tgz", - "integrity": "sha512-KlfpmF1AAIdAWxt8QQNClbpLFlAUfYOzqIuJ7IXEStMHpFRuH78RG6FjqPb3Rqhh0K89F5npvG+/i3Fjl6IveQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.64.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.6.tgz", - "integrity": "sha512-MrTLjd7Jlt9/ZavJZYG61KQv9Iy9bzbY+yXy73Xvwoa2uP66wvDShkyBr5Chz/yrE6lbD/zHMuTmKQVNQqBApg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.64.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.6.tgz", - "integrity": "sha512-8fWDH1CkPI8GdBKAxzHDza7UmgficsKo2+MVbvUgOGhB0629w5sHMg3Qu5y7TZsrYKHHWpz8fla4KvoT7WsO3Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.64.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.6.tgz", - "integrity": "sha512-9/c9PWdr5GCIHkkxiHM1HjNBQMVdftP4O8QTcoUAaRza2FSqJ4axbf8QAnBuE6BIGQDl71SaRPeDdDYid5ecTw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@scure/base": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", - "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.4.0.tgz", - "integrity": "sha512-82dxFbZUYboyOf0AXiydsQrFQ5Q4h9mX+O2UkE91ROYmsc0BKMGZLwDmy96Jpa2+vrtoxomjUhy1RPIgH/r2nA==", - "dependencies": { - "@noble/hashes": "2.4.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39/node_modules/@noble/hashes": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz", - "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@stablelib/base64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", - "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==" - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "26.6.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", - "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", - "dev": true, - "dependencies": { - "undici-types": "~8.9.0" - } - }, - "node_modules/@types/qrcode": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", - "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/ws": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz", - "integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.71.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.2.tgz", - "integrity": "sha512-K72/pvT7qpm8q3k8G5u93NCSN1RcLxX5h7Gw0bQpbMXviBHeY6Nx1WEwmk9wP+eKTSQ8ecxvcgt/ZX/rB938xQ==", - "dev": true, - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.71.2", - "@typescript-eslint/type-utils": "8.71.2", - "@typescript-eslint/utils": "8.71.2", - "@typescript-eslint/visitor-keys": "8.71.2", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.71.2", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.12.tgz", - "integrity": "sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.71.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.2.tgz", - "integrity": "sha512-aTtmZjp/x3gsGHkDs04/1I8kNogBJkpQaeGaN0PfiG43NzDEmW2n+OP5im9wfsvyX0yP1UZWBrAGtDBfH9jEVw==", - "dev": true, - "dependencies": { - "@typescript-eslint/scope-manager": "8.71.2", - "@typescript-eslint/types": "8.71.2", - "@typescript-eslint/typescript-estree": "8.71.2", - "@typescript-eslint/visitor-keys": "8.71.2", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.71.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.2.tgz", - "integrity": "sha512-Pd68GsLVh5ZXlcEccFPdhSOvtHZ/2WU0XsA8zmXOS3F158lVlcTLhfQ6qvtL1RzbMie0Kp8xtbVwAg4nejKPHw==", - "dev": true, - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.71.2", - "@typescript-eslint/types": "^8.71.2", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.71.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.2.tgz", - "integrity": "sha512-YF4wgg5iUX5Obywoac27B56FhozMNFhdJSbh11H5GBAwmBtfyRhmgTQqW9tRNNmHDvsZ2CX6UGzoz+YfYn2Leg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "8.71.2", - "@typescript-eslint/visitor-keys": "8.71.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.71.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.2.tgz", - "integrity": "sha512-jz49TAWokm/7yWD9/o837nYax9DyALxIPOImdImvVzo6canBCZYzin8zJyqNR5k1j9K1DE0bVvr0XYB+99uAdg==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.71.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.2.tgz", - "integrity": "sha512-cC8UaalCViJkb3rH/xLIvidEwoFRtMZMUqDjjJMGEKaTFwYRHYBcoczlEGHuvkHEO8svRDrgZ0yQgOArFmwang==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "8.71.2", - "@typescript-eslint/typescript-estree": "8.71.2", - "@typescript-eslint/utils": "8.71.2", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.71.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.2.tgz", - "integrity": "sha512-y5Ukl3oVp7e/VGxqIoEXoeb/Roj71PAJ4WG/N/RGj+eeDvKRLM15kGr9MijLbN0i03S52zxu31Z98E5gWChJKw==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.71.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.2.tgz", - "integrity": "sha512-DgNOEsl4pdYzwSL+N/Cl33tX1Hlw3FvId73LnKXfq7jnrF2viOEn7X+P7RBkkSWCVm92gnWd28GIC5vJuLREeA==", - "dev": true, - "dependencies": { - "@typescript-eslint/project-service": "8.71.2", - "@typescript-eslint/tsconfig-utils": "8.71.2", - "@typescript-eslint/types": "8.71.2", - "@typescript-eslint/visitor-keys": "8.71.2", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", - "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", - "dev": true, - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", - "dev": true, - "dependencies": { - "brace-expansion": "^5.0.8" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.71.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.2.tgz", - "integrity": "sha512-RyKa1G1M9rKHDhrk+HJtiF1MvLD5Ux2JW3TSkzw+02pRALdpJb+QzG1Y5em+LvCgzGwgg8H7mBRFKljNz7ImPw==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.71.2", - "@typescript-eslint/types": "8.71.2", - "@typescript-eslint/typescript-estree": "8.71.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.71.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.2.tgz", - "integrity": "sha512-zozlN0UA32bSG2zkKAgQyRYSbEISVoyU6vvkKYo2aSny74D8Jh4e+POkJ6Lk0dlezZi5x7TtD+88Xb06IB8nhg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "8.71.2", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@vitest/expect": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", - "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", - "dev": true, - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.7", - "@vitest/utils": "3.2.7", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", - "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", - "dev": true, - "dependencies": { - "@vitest/spy": "3.2.7", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", - "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", - "dev": true, - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", - "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", - "dev": true, - "dependencies": { - "@vitest/utils": "3.2.7", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", - "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", - "dev": true, - "dependencies": { - "@vitest/pretty-format": "3.2.7", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", - "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", - "dev": true, - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", - "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", - "dev": true, - "dependencies": { - "@vitest/pretty-format": "3.2.7", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/acorn": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", - "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", - "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/bundle-require": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", - "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", - "dev": true, - "dependencies": { - "load-tsconfig": "^0.2.3" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "peerDependencies": { - "esbuild": ">=0.18" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/canonicalize": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/canonicalize/-/canonicalize-2.1.0.tgz", - "integrity": "sha512-F705O3xrsUtgt98j7leetNhTWPe+5S72rlL5O4jA1pKqBVQ/dT1O1D6PFxmSXvc0SUOinWS57DKx0I3CHrXJHQ==", - "bin": { - "canonicalize": "bin/canonicalize.js" - } - }, - "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", - "dev": true, - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "engines": { - "node": ">= 16" - } - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "dev": true - }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "dev": true, - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/dijkstrajs": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", - "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true - }, - "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.39.5", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", - "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.2", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.6", - "@eslint/js": "9.39.5", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.5", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expect-type": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", - "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", - "dev": true, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fast-sha256": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", - "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/fix-dts-default-cjs-exports": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", - "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", - "dev": true, - "dependencies": { - "magic-string": "^0.30.17", - "mlly": "^1.7.4", - "rollup": "^4.34.8" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", - "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/hash-wasm": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.12.0.tgz", - "integrity": "sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ==", - "optional": true - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/jose": { - "version": "6.2.12", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", - "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true - }, - "node_modules/js-yaml": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", - "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "node_modules/load-tsconfig": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", - "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true - }, - "node_modules/lru-cache": { - "version": "11.5.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", - "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/marked": { - "version": "18.1.1", - "resolved": "https://registry.npmjs.org/marked/-/marked-18.1.1.tgz", - "integrity": "sha512-zEGgrtUG8crk+TrMacJSVABU4DYDDkS8uqgU3ohrn/2fW9C2AEfUrO3m6IhpIpQpl7YcVpiXXC9cSMWDANT3dg==", - "dev": true, - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mlly": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", - "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", - "dev": true, - "dependencies": { - "acorn": "^8.16.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.3" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.20", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", - "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true - }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "engines": { - "node": ">= 14.16" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true - }, - "node_modules/picomatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", - "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dev": true, - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, - "node_modules/pngjs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", - "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/postcss": { - "version": "8.5.30", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz", - "integrity": "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.20", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "lilconfig": "^3.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.9.10", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz", - "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==", - "dev": true, - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/qrcode": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", - "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", - "dependencies": { - "dijkstrajs": "^1.0.1", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "qrcode": "bin/qrcode" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/rollup": { - "version": "4.64.6", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.6.tgz", - "integrity": "sha512-fX1Vy+UNKdXn+xKDnu/HBXbHVr1AY8cSdQj9BU4kLv5aFCMqGpvrVbyUMmjN3DdzWhSAuTbcX8PCNEVaumthYA==", - "dev": true, - "dependencies": { - "@types/estree": "1.0.9" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@napi-rs/lzma-linux-x64-gnu": "1.5.1", - "@rollup/rollup-android-arm-eabi": "4.64.6", - "@rollup/rollup-android-arm64": "4.64.6", - "@rollup/rollup-darwin-arm64": "4.64.6", - "@rollup/rollup-darwin-x64": "4.64.6", - "@rollup/rollup-freebsd-arm64": "4.64.6", - "@rollup/rollup-freebsd-x64": "4.64.6", - "@rollup/rollup-linux-arm-gnueabihf": "4.64.6", - "@rollup/rollup-linux-arm-musleabihf": "4.64.6", - "@rollup/rollup-linux-arm64-gnu": "4.64.6", - "@rollup/rollup-linux-arm64-musl": "4.64.6", - "@rollup/rollup-linux-loong64-gnu": "4.64.6", - "@rollup/rollup-linux-loong64-musl": "4.64.6", - "@rollup/rollup-linux-ppc64-gnu": "4.64.6", - "@rollup/rollup-linux-ppc64-musl": "4.64.6", - "@rollup/rollup-linux-riscv64-gnu": "4.64.6", - "@rollup/rollup-linux-riscv64-musl": "4.64.6", - "@rollup/rollup-linux-s390x-gnu": "4.64.6", - "@rollup/rollup-linux-x64-gnu": "4.64.6", - "@rollup/rollup-linux-x64-musl": "4.64.6", - "@rollup/rollup-openbsd-x64": "4.64.6", - "@rollup/rollup-openharmony-arm64": "4.64.6", - "@rollup/rollup-win32-arm64-msvc": "4.64.6", - "@rollup/rollup-win32-ia32-msvc": "4.64.6", - "@rollup/rollup-win32-x64-gnu": "4.64.6", - "@rollup/rollup-win32-x64-msvc": "4.64.6", - "fsevents": "~2.3.2" - } - }, - "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true - }, - "node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/source-map-js": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", - "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true - }, - "node_modules/standardwebhooks": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", - "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", - "dependencies": { - "@stablelib/base64": "^1.0.0", - "fast-sha256": "^1.3.0" - } - }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-literal": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", - "dev": true, - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/svix": { - "version": "1.99.1", - "resolved": "https://registry.npmjs.org/svix/-/svix-1.99.1.tgz", - "integrity": "sha512-JfpvbAg5iT5NagDAfOq3e6Ci6NbOMbMm6C3DnfDBB60m2JDK+Z2hXPE9XNAmutb53DCdPoih1V70IbklZnX09A==", - "dependencies": { - "standardwebhooks": "1.0.0" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tiny-secp256k1": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.4.tgz", - "integrity": "sha512-FoDTcToPqZE454Q04hH9o2EhxWsm7pOSpicyHkgTwKhdKWdsTUuqfP5MLq3g+VjAtl2vSx6JpXGdwA2qpYkI0Q==", - "optional": true, - "dependencies": { - "uint8array-tools": "0.0.7" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true - }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", - "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "bin": { - "tree-kill": "cli.js" - } - }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true - }, - "node_modules/tsup": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", - "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", - "dev": true, - "dependencies": { - "bundle-require": "^5.1.0", - "cac": "^6.7.14", - "chokidar": "^4.0.3", - "consola": "^3.4.0", - "debug": "^4.4.0", - "esbuild": "^0.27.0", - "fix-dts-default-cjs-exports": "^1.0.0", - "joycon": "^3.1.1", - "picocolors": "^1.1.1", - "postcss-load-config": "^6.0.1", - "resolve-from": "^5.0.0", - "rollup": "^4.34.8", - "source-map": "^0.7.6", - "sucrase": "^3.35.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.11", - "tree-kill": "^1.2.2" - }, - "bin": { - "tsup": "dist/cli-default.js", - "tsup-node": "dist/cli-node.js" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@microsoft/api-extractor": "^7.36.0", - "@swc/core": "^1", - "postcss": "^8.4.12", - "typescript": ">=4.5.0" - }, - "peerDependenciesMeta": { - "@microsoft/api-extractor": { - "optional": true - }, - "@swc/core": { - "optional": true - }, - "postcss": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/tsup/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.71.2", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.71.2.tgz", - "integrity": "sha512-Rz4effbMy2g9wnvOJZG6SbVavX313Lap1vNEbk1RUochzickvzP8P4HB+bkNg5e50eVz1hQGjh9K1omI1xB4Bw==", - "dev": true, - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.71.2", - "@typescript-eslint/parser": "8.71.2", - "@typescript-eslint/typescript-estree": "8.71.2", - "@typescript-eslint/utils": "8.71.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/ufo": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", - "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", - "dev": true - }, - "node_modules/uint8array-tools": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.7.tgz", - "integrity": "sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ==", - "optional": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", - "dev": true - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/valibot": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.2.tgz", - "integrity": "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==", - "peerDependencies": { - "typescript": ">=5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/vite": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.7.tgz", - "integrity": "sha512-AHEzTORMbi7/7aXp7w7KZ5+DCUdSeLsJz0grY8xHpc9DHdYb0vQu6DqJfm2tOw3gDExNgPgnJTXxM6syYFHaCA==", - "dev": true, - "dependencies": { - "esbuild": "^0.27.0 || ^0.28.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vitest": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", - "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", - "dev": true, - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.7", - "@vitest/mocker": "3.2.7", - "@vitest/pretty-format": "^3.2.7", - "@vitest/runner": "3.2.7", - "@vitest/snapshot": "3.2.7", - "@vitest/spy": "3.2.7", - "@vitest/utils": "3.2.7", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", - "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.7", - "@vitest/ui": "3.2.7", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/debug": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ws": { - "version": "8.22.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", - "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true + "name": "@bloxwap/agent-cli", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@bloxwap/agent-cli", + "version": "0.1.0", + "dependencies": { + "@bloxwap/hyperliquid": "^0.6.0", + "@privy-io/node": "^0.35.0", + "@scure/bip39": "^2.4.0", + "qrcode": "^1.5.4", + "ws": "^8.22.0", + "zod": "^4.6.7" + }, + "bin": { + "agent-cli": "dist/cli.js" + }, + "devDependencies": { + "@types/node": "^26.6.5", + "@types/qrcode": "^1.5.6", + "@types/ws": "^8.18.2", + "eslint": "^9.39.5", + "marked": "^18.1.1", + "prettier": "^3.9.10", + "tsup": "^8.5.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.71.2", + "viem": "^2.57.5", + "vitest": "^3.2.7" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "devOptional": true + }, + "node_modules/@bloxwap/hyperliquid": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@bloxwap/hyperliquid/-/hyperliquid-0.6.0.tgz", + "integrity": "sha512-ryQya36sx5tD0xg9USFg+6krpMvPlHvc55p42FXJlCN6RTWO9obA6QknZpM1RRTPAl5w04Jj16kQxLcesEWl0Q==", + "dependencies": { + "@noble/hashes": "2.2.0", + "valibot": "1.4.2" + }, + "engines": { + "node": ">=22.12.0" + }, + "optionalDependencies": { + "hash-wasm": "4.12.0", + "tiny-secp256k1": "2.2.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", + "dev": true, + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@hpke/chacha20poly1305": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@hpke/chacha20poly1305/-/chacha20poly1305-1.8.0.tgz", + "integrity": "sha512-FcBfAQ+Y99vMNJP2yrZ9wpL8V0GOwp1+zMyzvc6alasrBygfFjFm1yeUtyADJCu/27C3Lm5mJzx6u7pwg+cX5w==", + "dependencies": { + "@hpke/common": "^1.10.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@hpke/common": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@hpke/common/-/common-1.10.1.tgz", + "integrity": "sha512-moJwhmtLtuxiUzzNp1jpfBfx8yefKoO9D/RCR9dmwrnc7qjJqId1rEtQz+lSlU5cabX8daToMSx/7HayXOiaFw==", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@hpke/core": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@hpke/core/-/core-1.9.0.tgz", + "integrity": "sha512-pFxWl1nNJeQCSUFs7+GAblHvXBCjn9EPN65vdKlYQil2aURaRxfGMO6vBKGqm1YHTKwiAxJQNEI70PbSowMP9Q==", + "dependencies": { + "@hpke/common": "^1.10.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "devOptional": true, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@privy-io/node": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@privy-io/node/-/node-0.35.0.tgz", + "integrity": "sha512-NhttAOmjgwtQzBoTmRCWzY5ZTTJV1WqPUmasyEbi9XfFd1LccpjHiNB+AUCeRXpY0FC5Bt/3Is7xJEmCX3LKNA==", + "dependencies": { + "@hpke/chacha20poly1305": "^1.7.1", + "@hpke/core": "^1.7.5", + "@noble/curves": "^1.9.7", + "@noble/hashes": "^1.8.0", + "@scure/base": "^1.2.5", + "canonicalize": "^2.1.0", + "jose": "^6.1.0", + "lru-cache": "^11.1.0", + "svix": "^1.92.2" + }, + "peerDependencies": { + "@solana/kit": "^5.1.0", + "@x402/evm": "^2.3.0", + "@x402/fetch": "^2.3.0", + "@x402/svm": "^2.3.0", + "viem": "^2.44.2" + }, + "peerDependenciesMeta": { + "@solana/kit": { + "optional": true + }, + "@x402/evm": { + "optional": true + }, + "@x402/fetch": { + "optional": true + }, + "@x402/svm": { + "optional": true + }, + "viem": { + "optional": true + } + } + }, + "node_modules/@privy-io/node/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.6.tgz", + "integrity": "sha512-FkO19Fowv/OQeJZPVfdetJaI3/VpbZmBsnmAlR/rQ+b3QwfiGQiOpVegpSRH331kelvHTVw9hXYG3DwjAaRzyQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.6.tgz", + "integrity": "sha512-si5/Etm6MphPc2xYHnk455Shls/hoMjTy3mhXge4OPmvCw7TwC4KP4fKokAlKLV4XaQvLvgSHfafPcblLMYYdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.6.tgz", + "integrity": "sha512-PQCl3q7b0BgvVt72i2xrWM6I5KliwwVe4GpWW5QDGyUQawDS9aPZss/aGyFqBmxdRxlE8wDc9CaxiQ5DHRSp/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.6.tgz", + "integrity": "sha512-l9DFl1Itb1YYdSrP1K3U8ih44AOHa/p+Q4WMMDfGvH5qVWp+ajfgS1DlYo4yCRh8wJ2ZJv0Fqccf+ULAmMFjmg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.6.tgz", + "integrity": "sha512-bR4GR3205N9+od0+l5Dd9/iZTDpJkIe6d6Ydp2qbcQZJLmKBdYQQRp8gbWpbMt92twKJY66o1vK4NkoKQODIrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.6.tgz", + "integrity": "sha512-XTVNaDzcFl2TeBglvxdGgsCv9JGtAGRAoJENmGBR/XvDH2mA2K0IXc2aMfP0dR8mexSXSKifJ85FixoXX1wfTw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.6.tgz", + "integrity": "sha512-qWc6qbc+25ugG9vqPR9P422KJBbVfZ05mjcMggoq8tsfWP+eRfC+/tiDdIlRF5VL2pDVZWN9zAhWsmu7R1+7Dw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.6.tgz", + "integrity": "sha512-V4Nwkp8844X/Xnww3PI1LYS0YPei8XQEVG70R+5MPZGO7AhYoun4hCN7Svs4U3wbQLZbwiz+9xRFhUZZdRl8Ww==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.6.tgz", + "integrity": "sha512-xb72A1aPaaIsp+a88VtRae/ovIEXislbUMyp7b9AQQU7ZkP2m/+ik6DW9TQcJyw5R8/RIailF9zExYwkjR++UA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.6.tgz", + "integrity": "sha512-pbcs1BVBwC43hZkjhjlF8Rx8m5G0KnhLZs9Azy/gqccP6UsNSzJr0hKGIAVPqzAH1O/NdPxxC9GVGb4msQg/aw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.6.tgz", + "integrity": "sha512-EKOC8os6Vsg487ZLKUqzLF99Jsmnq6tu7O7mAELakNpcA+8KadktFf/JYdRuHe/DzCUXmN38pUKu4XBcKlqGbQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.6.tgz", + "integrity": "sha512-v/SQ68GnSlr/HyjB79sNXesGDwkOPu9JjbHfmWaymCiM+nZsQ9TstG1KfA4MuEvTr2d3PoY8PhFxj9bt0e32JA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.6.tgz", + "integrity": "sha512-G3pmliMQ7r15zFFNwBlaT/xTdSLi4lfgI7CUrQueyKYheNVhrd8JrWCE6Ll8xf4AA1RZFqq+mNJYntiElsVOyA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.6.tgz", + "integrity": "sha512-XQTB/IPoB/wG6x/wu9uomAt8YH61IAr5iZTubMiad/lu88sotq2ocz9hJd556+Jz2kIkF7ZQZIsUuWE3OWqtcA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.6.tgz", + "integrity": "sha512-O5mpokth6+ydMuwiGtY8fIjvOBfH7u6SzmDNmZ3k6GK5C6iEAC2q2ZN/T5L9jYILx0IcCT0KnfMDihQ5yQFSBw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.6.tgz", + "integrity": "sha512-aIFxbAD9v3TvIaEDsKmukjcDg1JS4/tFXw8SHyQQgzOwzWCQ4cyDK7Ju1pQKqUXDFkEu/BZCFDCDATsKhQcB/Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.6.tgz", + "integrity": "sha512-fuNo1UwBc6BxnHDOxxguQL/InhheMN3RJcJMb0b4xP8r5Q3CNCrJbVg0eCIFuJ4e/zAfjR2gYbOfbzO/G7CIyw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.6.tgz", + "integrity": "sha512-05v6wFKDKJPZs9xlytVrrRu6IbRuoSvlH9xNwEOafXsKyjBFZPvAcZz2VIAB/M+lblYeKa9iOYC4RDVdfcAAaw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.6.tgz", + "integrity": "sha512-wJXKcdMjvkc/FaavZXNP1yW4i0pfOmhZAIWRSLG2yJ0PB0TWi7dkLErhX0u0vptDcXKKO/m1Y/YTeuyvb2MXwg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.6.tgz", + "integrity": "sha512-cwg7/h0ThvJwy8CBkmACdiHAxRWpbsCmIsjOt+Zzx0hTnIcSUmKYEV03bGg+hD827OnwUsbKh1ZZDW754xWyBA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.6.tgz", + "integrity": "sha512-T0RkYIJbdYXJEQFw+tMBO09KDFdgmNxkHsof3wWAABrvnx15Yh8RytmrgYy9+q9cBeBNZF66MMQZC+5nzEz6vg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.6.tgz", + "integrity": "sha512-KlfpmF1AAIdAWxt8QQNClbpLFlAUfYOzqIuJ7IXEStMHpFRuH78RG6FjqPb3Rqhh0K89F5npvG+/i3Fjl6IveQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.6.tgz", + "integrity": "sha512-MrTLjd7Jlt9/ZavJZYG61KQv9Iy9bzbY+yXy73Xvwoa2uP66wvDShkyBr5Chz/yrE6lbD/zHMuTmKQVNQqBApg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.6.tgz", + "integrity": "sha512-8fWDH1CkPI8GdBKAxzHDza7UmgficsKo2+MVbvUgOGhB0629w5sHMg3Qu5y7TZsrYKHHWpz8fla4KvoT7WsO3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.6.tgz", + "integrity": "sha512-9/c9PWdr5GCIHkkxiHM1HjNBQMVdftP4O8QTcoUAaRza2FSqJ4axbf8QAnBuE6BIGQDl71SaRPeDdDYid5ecTw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "devOptional": true, + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "devOptional": true, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.4.0.tgz", + "integrity": "sha512-82dxFbZUYboyOf0AXiydsQrFQ5Q4h9mX+O2UkE91ROYmsc0BKMGZLwDmy96Jpa2+vrtoxomjUhy1RPIgH/r2nA==", + "dependencies": { + "@noble/hashes": "2.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz", + "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "26.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", + "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", + "dev": true, + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.2.tgz", + "integrity": "sha512-K72/pvT7qpm8q3k8G5u93NCSN1RcLxX5h7Gw0bQpbMXviBHeY6Nx1WEwmk9wP+eKTSQ8ecxvcgt/ZX/rB938xQ==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/type-utils": "8.71.2", + "@typescript-eslint/utils": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.71.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.12.tgz", + "integrity": "sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.2.tgz", + "integrity": "sha512-aTtmZjp/x3gsGHkDs04/1I8kNogBJkpQaeGaN0PfiG43NzDEmW2n+OP5im9wfsvyX0yP1UZWBrAGtDBfH9jEVw==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.2.tgz", + "integrity": "sha512-Pd68GsLVh5ZXlcEccFPdhSOvtHZ/2WU0XsA8zmXOS3F158lVlcTLhfQ6qvtL1RzbMie0Kp8xtbVwAg4nejKPHw==", + "dev": true, + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.71.2", + "@typescript-eslint/types": "^8.71.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.2.tgz", + "integrity": "sha512-YF4wgg5iUX5Obywoac27B56FhozMNFhdJSbh11H5GBAwmBtfyRhmgTQqW9tRNNmHDvsZ2CX6UGzoz+YfYn2Leg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.2.tgz", + "integrity": "sha512-jz49TAWokm/7yWD9/o837nYax9DyALxIPOImdImvVzo6canBCZYzin8zJyqNR5k1j9K1DE0bVvr0XYB+99uAdg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.2.tgz", + "integrity": "sha512-cC8UaalCViJkb3rH/xLIvidEwoFRtMZMUqDjjJMGEKaTFwYRHYBcoczlEGHuvkHEO8svRDrgZ0yQgOArFmwang==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/utils": "8.71.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.2.tgz", + "integrity": "sha512-y5Ukl3oVp7e/VGxqIoEXoeb/Roj71PAJ4WG/N/RGj+eeDvKRLM15kGr9MijLbN0i03S52zxu31Z98E5gWChJKw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.2.tgz", + "integrity": "sha512-DgNOEsl4pdYzwSL+N/Cl33tX1Hlw3FvId73LnKXfq7jnrF2viOEn7X+P7RBkkSWCVm92gnWd28GIC5vJuLREeA==", + "dev": true, + "dependencies": { + "@typescript-eslint/project-service": "8.71.2", + "@typescript-eslint/tsconfig-utils": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.2.tgz", + "integrity": "sha512-RyKa1G1M9rKHDhrk+HJtiF1MvLD5Ux2JW3TSkzw+02pRALdpJb+QzG1Y5em+LvCgzGwgg8H7mBRFKljNz7ImPw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.2.tgz", + "integrity": "sha512-zozlN0UA32bSG2zkKAgQyRYSbEISVoyU6vvkKYo2aSny74D8Jh4e+POkJ6Lk0dlezZi5x7TtD+88Xb06IB8nhg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.71.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abitype": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", + "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", + "devOptional": true, + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/canonicalize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/canonicalize/-/canonicalize-2.1.0.tgz", + "integrity": "sha512-F705O3xrsUtgt98j7leetNhTWPe+5S72rlL5O4jA1pKqBVQ/dT1O1D6PFxmSXvc0SUOinWS57DKx0I3CHrXJHQ==", + "bin": { + "canonicalize": "bin/canonicalize.js" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "devOptional": true + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hash-wasm": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.12.0.tgz", + "integrity": "sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ==", + "optional": true + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/jose": { + "version": "6.2.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/marked": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.1.1.tgz", + "integrity": "sha512-zEGgrtUG8crk+TrMacJSVABU4DYDDkS8uqgU3ohrn/2fW9C2AEfUrO3m6IhpIpQpl7YcVpiXXC9cSMWDANT3dg==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ox": { + "version": "0.14.56", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.56.tgz", + "integrity": "sha512-Feinlj+deWEam6RIwurRE+eHZ2N3hoUb3AAjQ10BmxRCYIymfqNkv0eZ+4ln4GkgDmSGLD2rGtTMqsFYSVDC6w==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.2.3", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ox/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "devOptional": true, + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ox/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "devOptional": true, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ox/node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "devOptional": true, + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/postcss": { + "version": "8.5.30", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz", + "integrity": "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz", + "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.64.6", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.6.tgz", + "integrity": "sha512-fX1Vy+UNKdXn+xKDnu/HBXbHVr1AY8cSdQj9BU4kLv5aFCMqGpvrVbyUMmjN3DdzWhSAuTbcX8PCNEVaumthYA==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.64.6", + "@rollup/rollup-android-arm64": "4.64.6", + "@rollup/rollup-darwin-arm64": "4.64.6", + "@rollup/rollup-darwin-x64": "4.64.6", + "@rollup/rollup-freebsd-arm64": "4.64.6", + "@rollup/rollup-freebsd-x64": "4.64.6", + "@rollup/rollup-linux-arm-gnueabihf": "4.64.6", + "@rollup/rollup-linux-arm-musleabihf": "4.64.6", + "@rollup/rollup-linux-arm64-gnu": "4.64.6", + "@rollup/rollup-linux-arm64-musl": "4.64.6", + "@rollup/rollup-linux-loong64-gnu": "4.64.6", + "@rollup/rollup-linux-loong64-musl": "4.64.6", + "@rollup/rollup-linux-ppc64-gnu": "4.64.6", + "@rollup/rollup-linux-ppc64-musl": "4.64.6", + "@rollup/rollup-linux-riscv64-gnu": "4.64.6", + "@rollup/rollup-linux-riscv64-musl": "4.64.6", + "@rollup/rollup-linux-s390x-gnu": "4.64.6", + "@rollup/rollup-linux-x64-gnu": "4.64.6", + "@rollup/rollup-linux-x64-musl": "4.64.6", + "@rollup/rollup-openbsd-x64": "4.64.6", + "@rollup/rollup-openharmony-arm64": "4.64.6", + "@rollup/rollup-win32-arm64-msvc": "4.64.6", + "@rollup/rollup-win32-ia32-msvc": "4.64.6", + "@rollup/rollup-win32-x64-gnu": "4.64.6", + "@rollup/rollup-win32-x64-msvc": "4.64.6", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/svix": { + "version": "1.99.1", + "resolved": "https://registry.npmjs.org/svix/-/svix-1.99.1.tgz", + "integrity": "sha512-JfpvbAg5iT5NagDAfOq3e6Ci6NbOMbMm6C3DnfDBB60m2JDK+Z2hXPE9XNAmutb53DCdPoih1V70IbklZnX09A==", + "dependencies": { + "standardwebhooks": "1.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-secp256k1": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.4.tgz", + "integrity": "sha512-FoDTcToPqZE454Q04hH9o2EhxWsm7pOSpicyHkgTwKhdKWdsTUuqfP5MLq3g+VjAtl2vSx6JpXGdwA2qpYkI0Q==", + "optional": true, + "dependencies": { + "uint8array-tools": "0.0.7" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", + "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/tsup/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.71.2.tgz", + "integrity": "sha512-Rz4effbMy2g9wnvOJZG6SbVavX313Lap1vNEbk1RUochzickvzP8P4HB+bkNg5e50eVz1hQGjh9K1omI1xB4Bw==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.71.2", + "@typescript-eslint/parser": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/utils": "8.71.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true + }, + "node_modules/uint8array-tools": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.7.tgz", + "integrity": "sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ==", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "dev": true + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/valibot": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.2.tgz", + "integrity": "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem": { + "version": "2.57.5", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.57.5.tgz", + "integrity": "sha512-BaUdS5lJJTxr6hJX17K0AjHbgUhsolsqH8CCbTq1WMunW/I/Siwc9VrTwccCcNPppgqa/gY4TqK8Zl8LLmlTlw==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.2.3", + "isows": "1.0.7", + "ox": "0.14.56", + "ws": "8.21.0" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "devOptional": true, + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "devOptional": true, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "devOptional": true, + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "devOptional": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/vite": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.7.tgz", + "integrity": "sha512-AHEzTORMbi7/7aXp7w7KZ5+DCUdSeLsJz0grY8xHpc9DHdYb0vQu6DqJfm2tOw3gDExNgPgnJTXxM6syYFHaCA==", + "dev": true, + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } - } - }, - "node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" - }, - "node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yargs/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "4.6.7", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", - "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } } - } } diff --git a/package.json b/package.json index 1927d1f..9317e0b 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "lint": "eslint .", "typecheck": "tsc --noEmit", "test": "vitest run", - "docs:generate": "npm run build && node scripts/generate-docs.mjs" + "docs:generate": "npm run build && node scripts/generate-docs.mjs", + "test:e2e": "node scripts/e2e-testnet.mjs" }, "devDependencies": { "@types/node": "^26.6.5", @@ -30,6 +31,7 @@ "tsup": "^8.5.1", "typescript": "^5.9.3", "typescript-eslint": "^8.71.2", + "viem": "^2.57.5", "vitest": "^3.2.7" }, "dependencies": { diff --git a/scripts/e2e-relay-hook.mjs b/scripts/e2e-relay-hook.mjs new file mode 100644 index 0000000..91e4791 --- /dev/null +++ b/scripts/e2e-relay-hook.mjs @@ -0,0 +1,10 @@ +// Loaded only by the explicit testnet e2e harness, never by the packaged CLI. +const originalFetch=globalThis.fetch; +globalThis.fetch=async(url,init)=>{ + if(String(url).startsWith('https://api.testnets.relay.link/')){ + if(String(url).includes('/quote/')) return Response.json({steps:[{id:'deposit',kind:'transaction',requestId:'mock-testnet-deposit',depositAddress:'0x'+'b'.repeat(40),items:[{status:'incomplete',data:{}}]}],fees:{},details:{currencyOut:{amount:'0',currency:{decimals:8}},expiresAt:Date.now()+60000}}); + if(String(url).includes('/intents/status')) return Response.json({status:'success'}); + throw new Error('Unexpected Relay mock endpoint'); + } + return originalFetch(url,init); +}; diff --git a/scripts/e2e-testnet.mjs b/scripts/e2e-testnet.mjs new file mode 100644 index 0000000..e43a086 --- /dev/null +++ b/scripts/e2e-testnet.mjs @@ -0,0 +1,31 @@ +import {execFileSync} from 'node:child_process'; +import {mkdirSync,copyFileSync,existsSync,writeFileSync} from 'node:fs'; +import {resolve,join} from 'node:path'; +import {homedir} from 'node:os'; +import {privateKeyToAccount} from 'viem/accounts'; +if(process.env.BLOXWAP_E2E!=='1'||!process.env.BLOXWAP_TEST_MASTER_KEY||!process.env.BLOXWAP_PRIVY_APP_ID||!process.env.BLOXWAP_PRIVY_APP_SECRET){console.log('SKIP: set BLOXWAP_E2E=1 and dedicated testnet master/Privy credentials');process.exit(0);} +const account=privateKeyToAccount(process.env.BLOXWAP_TEST_MASTER_KEY); +const config=join(homedir(),'.bloxwap','e2e-config.json'); +if(existsSync(config)) throw new Error('Refusing to replace an existing e2e config; use a clean CI account'); +const hook=resolve('scripts/e2e-relay-hook.mjs');const env={...process.env,NODE_OPTIONS:`${process.env.NODE_OPTIONS??''} --import=${hook}`}; +const run=(name,input={})=>{const raw=execFileSync(process.execPath,['dist/cli.js',name,'--config',config,'--network','testnet','--json',JSON.stringify(input)],{input:'',encoding:'utf8',env});const result=JSON.parse(raw);if(!result.ok) throw new Error(JSON.stringify(result));return result.data;}; +const sign=payload=>account.signTypedData(payload.typedData); +const info=async(type,params={})=>(await fetch('https://api.hyperliquid-testnet.xyz/info',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({type,...params})})).json(); +try{ + run('config',{args:['set','privyAppId',process.env.BLOXWAP_PRIVY_APP_ID]}); + const init=run('init',{masterAddress:account.address,qr:false});if(!init.approved) run('init',{signature:await sign(init.approvals.agent),qr:false}); + const state=await info('clearinghouseState',{user:account.address});if(Number(state.withdrawable)<30) throw new Error('Dedicated testnet account needs at least 30 test USDC; no real funding is performed'); + run('fund',{amount:'10',qr:false});const status=run('status');if(status.deposits.some(d=>d.state!=='settled')) throw new Error('Mocked Relay funding did not settle'); + run('run',{detach:true}); + const meta=await info('meta');const market=meta.universe.find(m=>m.name==='BTC');const mids=await info('allMids');const mid=Number(mids.BTC);const size=(Math.ceil(20/mid*10**market.szDecimals)/10**market.szDecimals).toFixed(market.szDecimals);const cloid='0x'+Date.now().toString(16).padStart(32,'0'); + const order=run('order',{coin:'BTC',side:'buy',size,price:String(Math.floor(mid*0.7)),cloid,intentId:'nightly-lifecycle'}); + const oid=order.result.response.data.statuses[0].resting?.oid;if(!oid) throw new Error('Expected resting test order');run('modify',{oid,price:String(Math.floor(mid*0.69))});run('cancel',{oid});run('events',{since:0}); + const rotation=run('rotate');const swapped=run('rotate',{signature:await sign(rotation.approval)});run('rotate',{revokeSignature:await sign(swapped.revokeApproval)}); + const revoke=run('revoke',{cancelOrders:true});run('revoke',{signature:await sign(revoke.approval)});run('audit',{intent:'nightly-lifecycle'}); + console.log('PASS: testnet init/fund/run/order/modify/cancel/events/rotate/revoke'); +}finally{ + mkdirSync('artifacts',{recursive:true});for(const file of ['events.jsonl','audit.jsonl']) if(existsSync(join(homedir(),'.bloxwap',file))) copyFileSync(join(homedir(),'.bloxwap',file),join('artifacts',file)); + // A failed run records its stage via journals; credentials are never exported. + writeFileSync('artifacts/network.txt','testnet\n'); + const {createConnection}=await import('node:net');await new Promise(resolveStop=>{const socket=createConnection(join(homedir(),'.bloxwap','run.sock'));socket.on('connect',()=>socket.end(JSON.stringify({jsonrpc:'2.0',id:1,method:'shutdown',params:{}})+'\n'));socket.on('error',resolveStop);socket.on('close',resolveStop);socket.setTimeout(5000,()=>{socket.destroy();resolveStop();});}); +} diff --git a/test/lifecycle.test.ts b/test/lifecycle.test.ts new file mode 100644 index 0000000..db38b3e --- /dev/null +++ b/test/lifecycle.test.ts @@ -0,0 +1,56 @@ +import {afterEach,beforeEach,expect,test,vi} from 'vitest'; +import {mkdir,rm} from 'node:fs/promises'; +import {join} from 'node:path'; +const environment=vi.hoisted(()=>({home:`/tmp/bloxwap-e2e-${process.pid}-${Math.random().toString(16).slice(2)}`,wallets:0,orders:[] as {coin:string;oid:number;side:string;sz:string;limitPx:string}[],agents:[] as {address:string;validUntil:number}[],balance:'0'})); +vi.mock('node:os',async importOriginal=>({...await importOriginal(),homedir:()=>environment.home})); +vi.mock('../src/wallet/privy.js',async importOriginal=>{ + const original=await importOriginal();return {...original,privyClient:async()=>({}),createAgent:async(config:import('../src/core/config.js').Config,_client:unknown,path?:string)=>{const {saveConfig}=await import('../src/core/config.js');config.agentAddress='0x'+String(++environment.wallets).repeat(40);config.privyWalletId='wallet-'+environment.wallets;config.authorizationKeyRef='env:BLOXWAP_E2E_AUTH';await saveConfig(config,path);return {id:config.privyWalletId,address:config.agentAddress};},privySigner:async(config:import('../src/core/config.js').Config)=>({address:config.agentAddress,signTypedData:async()=>('0x'+'11'.repeat(64)+'1b')})}; +}); +vi.mock('../src/wallet/policy.js',async importOriginal=>({...await importOriginal(),applyPolicy:async(config:import('../src/core/config.js').Config)=>{config.policyId='policy';config.policyVersion=1;return 'policy';}})); +vi.mock('../src/runner/subscriptions.js',async importOriginal=>({...await importOriginal(),startSubscriptions:async()=>({state:'connected',lastMessage:Date.now(),lastFill:null})})); +import '../src/commands/init.js'; +import '../src/commands/fund.js'; +import '../src/commands/read.js'; +import '../src/commands/order.js'; +import '../src/commands/cancel.js'; +import '../src/commands/modify.js'; +import '../src/commands/leverage.js'; +import '../src/commands/run.js'; +import '../src/commands/rotate.js'; +import '../src/commands/revoke.js'; +import '../src/commands/events.js'; +import '../src/commands/audit.js'; +import {dispatch} from '../src/core/router.js'; +import {saveConfig,migrate} from '../src/core/config.js'; +import {activeRunner,setActiveRunner} from '../src/runner/lifecycle.js'; +import {rpcCall,socketPath} from '../src/runner/rpc.js'; +import {readAudit,verifyAudit} from '../src/core/audit.js'; +let path:string; +beforeEach(async()=>{ + await mkdir(environment.home,{recursive:true,mode:0o700});path=join(environment.home,'.bloxwap','config.json');environment.wallets=0;environment.orders=[];environment.agents=[];environment.balance='0'; + await saveConfig(migrate({privyAppId:'mock',masterAddress:'0x'+'a'.repeat(40)}),path); + vi.stubGlobal('fetch',vi.fn(async(url:unknown,options?:RequestInit)=>{ + const body=JSON.parse(String(options?.body??'{}'));const action=body.action; + if(String(url).includes('/quote/v2')) return Response.json({steps:[{id:'deposit',kind:'transaction',requestId:'deposit-1',depositAddress:'0x'+'b'.repeat(40),items:[{status:'incomplete',data:{}}]}],fees:{},details:{currencyOut:{amount:'1000000000',currency:{decimals:8}},expiresAt:Date.now()+60000}}); + if(String(url).includes('/intents/status')) return Response.json({status:'success'}); + if(action?.type==='approveAgent'){if(action.agentAddress==='0x'+'0'.repeat(40)) environment.agents=[];else environment.agents.push({address:action.agentAddress,validUntil:Number(action.agentName.match(/valid_until (\d+)/)?.[1])});return Response.json({status:'ok',response:{type:'default'}});} + if(action){const statuses=action.type==='order'?[{resting:{oid:1}}]:['success'];if(action.type==='order') environment.orders=[{coin:'BTC',oid:1,side:'B',sz:'0.001',limitPx:'20000'}];if(action.type==='cancel') environment.orders=[];return Response.json({status:'ok',response:{type:action.type==='batchModify'?'order':action.type,data:{statuses}}});} + if(body.type==='meta') return Response.json({universe:[{name:'BTC',szDecimals:5,maxLeverage:40}],marginTables:[]}); + if(body.type==='clearinghouseState') return Response.json({marginSummary:{accountValue:environment.balance,totalMarginUsed:'0',totalNtlPos:'0'},withdrawable:environment.balance,assetPositions:[]}); + if(body.type==='extraAgents') return Response.json(environment.agents); + if(body.type==='frontendOpenOrders') return Response.json(environment.orders); + if(body.type==='allMids') return Response.json({BTC:'20000'}); + throw new Error('Unexpected mocked request: '+JSON.stringify(body)); + })); +}); +afterEach(async()=>{await activeRunner?.shutdown();setActiveRunner(undefined);vi.unstubAllGlobals();await rm(environment.home,{recursive:true,force:true});}); +async function command(name:string,input:Record={}){const result=await dispatch([name,'--config',path,'--json',JSON.stringify(input)]);expect(result.body,JSON.stringify(result.body)).toMatchObject({ok:true});return (result.body as {data:Record}).data;} +test('mocked full account lifecycle through real CLI router, SDK and runner RPC',async()=>{ + const initialized=await command('init',{qr:false});expect(initialized.requiresSignature).toBe(true);await command('init',{qr:false,signature:'0x'+'11'.repeat(64)+'1b'}); + const deposit=await command('fund',{qr:false,amount:'10'});expect(deposit.depositId).toBe('deposit-1');environment.balance='10';const status=await command('status');expect(status.deposits).toEqual([{id:'deposit-1',state:'settled'}]); + await command('run');expect(await rpcCall('balance',{},socketPath())).toMatchObject({accountValue:'10'}); + await command('order',{coin:'BTC',side:'buy',size:'0.001',price:'20000',intentId:'lifecycle'});await command('modify',{oid:1,price:'20100'});await command('cancel',{all:true}); + const rotation=await command('rotate');expect(rotation.requiresSignature).toBe(true);expect((await command('rotate',{signature:'0x'+'11'.repeat(64)+'1b'})).swapped).toBe(true); + await command('revoke',{cancelOrders:true});const rejected=await dispatch(['order','--config',path,'--json','{"coin":"BTC","side":"buy","size":"0.001","price":"20000"}']);expect(rejected.body).toMatchObject({ok:false,code:'AGENT_NOT_APPROVED'}); + const entries=await readAudit(join(environment.home,'.bloxwap','audit.jsonl'));expect(entries.some(entry=>entry.intentId==='lifecycle')).toBe(true);expect(verifyAudit(entries)).toBe(true);expect(await command('events')).toBeDefined(); +},20000); From 1970104103332501aeec9345d4d5786e53994456 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:53:44 -0700 Subject: [PATCH 40/43] feat: package executable CLI for global npm and Bun installs (#40) --- .github/workflows/ci.yml | 2 +- .github/workflows/install.yml | 23 +++++++++++++++++++++++ .gitignore | 1 + docs/PROGRESS.md | 1 + package-lock.json | 3 ++- package.json | 13 ++++++++++--- scripts/build-binaries.mjs | 4 ++++ scripts/finalize-build.mjs | 2 ++ scripts/package-smoke.mjs | 15 +++++++++++++++ 9 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/install.yml create mode 100644 scripts/build-binaries.mjs create mode 100644 scripts/finalize-build.mjs create mode 100644 scripts/package-smoke.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d709e69..6d10f1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: npm - run: npm ci - run: npm run build diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml new file mode 100644 index 0000000..30b844e --- /dev/null +++ b/.github/workflows/install.yml @@ -0,0 +1,23 @@ +name: Install smoke +on: + pull_request: + push: + branches: [main] +jobs: + install: + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - uses: oven-sh/setup-bun@v2 + - run: npm ci + - run: npm run build + - run: npm run test:package + env: + BLOXWAP_SMOKE_BUN: '1' diff --git a/.gitignore b/.gitignore index 0d564fa..e50b39c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ coverage/ site/ artifacts/ .bloxwap/ +binaries/ diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 2acf254..02dba38 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -42,3 +42,4 @@ | #37 | partial | Validated self-contained skill with every command example, ordered funding/custody flow and complete error table; real agent testnet execution requires credentials (#39 gate). | | #38 | done | Install/testnet README, custody architecture, schema-generated command reference and static docs site with gated Pages workflow. | | #39 | partial | Full mocked CLI/SDK/RPC lifecycle passes; opt-in real testnet lifecycle with mocked Relay, nightly secrets gate and journal artifacts. Real execution skipped without credentials. | +| #40 | partial | Postinstall-free executable package, supported Node 22.12+ engine and global/Bun install smoke pass locally; macOS/Linux CI and optional compiled binaries. Registry install awaits publication. | diff --git a/package-lock.json b/package-lock.json index 0d98842..ea33cb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "@bloxwap/agent-cli", "version": "0.1.0", + "license": "MIT", "dependencies": { "@bloxwap/hyperliquid": "^0.6.0", "@privy-io/node": "^0.35.0", @@ -32,7 +33,7 @@ "vitest": "^3.2.7" }, "engines": { - "node": ">=20" + "node": ">=22.12.0" } }, "node_modules/@adraffy/ens-normalize": { diff --git a/package.json b/package.json index 9317e0b..7091a3b 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "agent-cli": "dist/cli.js" }, "engines": { - "node": ">=20" + "node": ">=22.12.0" }, "files": [ "dist", @@ -14,12 +14,14 @@ "policies" ], "scripts": { - "build": "tsup src/cli.ts --format esm --clean", + "build": "tsup src/cli.ts --format esm --clean && node scripts/finalize-build.mjs", "lint": "eslint .", "typecheck": "tsc --noEmit", "test": "vitest run", "docs:generate": "npm run build && node scripts/generate-docs.mjs", - "test:e2e": "node scripts/e2e-testnet.mjs" + "test:e2e": "node scripts/e2e-testnet.mjs", + "test:package": "node scripts/package-smoke.mjs", + "build:binaries": "node scripts/build-binaries.mjs" }, "devDependencies": { "@types/node": "^26.6.5", @@ -41,5 +43,10 @@ "qrcode": "^1.5.4", "ws": "^8.22.0", "zod": "^4.6.7" + }, + "description": "Agent-native Hyperliquid trading CLI with Privy custody and Relay funding", + "license": "MIT", + "publishConfig": { + "access": "public" } } diff --git a/scripts/build-binaries.mjs b/scripts/build-binaries.mjs new file mode 100644 index 0000000..fcc9ac3 --- /dev/null +++ b/scripts/build-binaries.mjs @@ -0,0 +1,4 @@ +import {execFileSync} from 'node:child_process'; +import {mkdirSync} from 'node:fs'; +mkdirSync('binaries',{recursive:true}); +const targets=process.argv.slice(2);for(const target of targets.length?targets:['bun-linux-x64','bun-darwin-arm64']) execFileSync('bun',['build','src/cli.ts','--compile',`--target=${target}`,'--outfile',`binaries/agent-cli-${target.replace('bun-','')}`],{stdio:'inherit'}); diff --git a/scripts/finalize-build.mjs b/scripts/finalize-build.mjs new file mode 100644 index 0000000..3e284f6 --- /dev/null +++ b/scripts/finalize-build.mjs @@ -0,0 +1,2 @@ +import {chmodSync} from 'node:fs'; +chmodSync('dist/cli.js',0o755); diff --git a/scripts/package-smoke.mjs b/scripts/package-smoke.mjs new file mode 100644 index 0000000..916d841 --- /dev/null +++ b/scripts/package-smoke.mjs @@ -0,0 +1,15 @@ +import {execFileSync} from 'node:child_process'; +import {mkdtempSync,rmSync} from 'node:fs'; +import {tmpdir} from 'node:os'; +import {join,resolve} from 'node:path'; +const directory=mkdtempSync(join(tmpdir(),'bloxwap-package-')); +try{ + const packed=JSON.parse(execFileSync('npm',['pack','--json','--pack-destination',directory],{encoding:'utf8'}))[0]; + if(!packed.files.some(file=>file.path==='SKILL.md')) throw new Error('Package missing skill'); + execFileSync('npm',['install','--global','--prefix',join(directory,'global'),join(directory,packed.filename),'--ignore-scripts'],{stdio:'pipe'}); + const bin=process.platform==='win32'?join(directory,'global','agent-cli.cmd'):join(directory,'global','bin','agent-cli'); + const help=JSON.parse(execFileSync(bin,['--help'],{input:'',encoding:'utf8'}));if(!help.ok||!help.data.commands.some(command=>command.name==='order')) throw new Error('Global CLI smoke failed'); + const schema=JSON.parse(execFileSync(bin,['schema','order'],{input:'',encoding:'utf8'}));if(!schema.ok) throw new Error('Schema smoke failed'); + if(process.env.BLOXWAP_SMOKE_BUN==='1'){const bun=JSON.parse(execFileSync('bun',[resolve('dist/cli.js'),'--help'],{input:'',encoding:'utf8'}));if(!bun.ok) throw new Error('Bun smoke failed');} + console.log('PASS: packed global install, CLI/schema and optional Bun smoke'); +}finally{rmSync(directory,{recursive:true,force:true});} From a04a6c783cb5c0b0948eab5dc06778d27fd43596 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:54:34 -0700 Subject: [PATCH 41/43] feat: add gated provenance release pipeline (#41) --- .changeset/config.json | 1 + .changeset/first-release.md | 6 + .github/workflows/release.yml | 42 ++++ .github/workflows/version.yml | 21 ++ CHANGELOG.md | 6 + docs/PROGRESS.md | 1 + docs/releases.md | 13 + package-lock.json | 448 +++++++++++++++++++++++++++++++++ package.json | 5 +- scripts/sync-skill-version.mjs | 4 + 10 files changed, 546 insertions(+), 1 deletion(-) create mode 100644 .changeset/config.json create mode 100644 .changeset/first-release.md create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/version.yml create mode 100644 CHANGELOG.md create mode 100644 docs/releases.md create mode 100644 scripts/sync-skill-version.mjs diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..5cd0c5c --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1 @@ +{"$schema":"https://unpkg.com/@changesets/config@3.1.1/schema.json","changelog":"@changesets/cli/changelog","commit":false,"fixed":[],"linked":[],"access":"public","baseBranch":"main","updateInternalDependencies":"patch","ignore":[]} diff --git a/.changeset/first-release.md b/.changeset/first-release.md new file mode 100644 index 0000000..a27363b --- /dev/null +++ b/.changeset/first-release.md @@ -0,0 +1,6 @@ +--- +"@bloxwap/agent-cli": minor +--- + +Add managed Hyperliquid agents, Relay funding, JSON commands, a persistent runner, +risk checks, durable events/audit journals, and the packaged trading skill. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..41f482b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,42 @@ +name: Release +on: + push: + tags: ['v*'] + workflow_dispatch: +permissions: + contents: write + id-token: write +jobs: + release: + if: vars.BLOXWAP_PUBLISH == '1' + runs-on: ubuntu-latest + environment: npm-release + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + registry-url: https://registry.npmjs.org + - uses: oven-sh/setup-bun@v2 + - run: npm ci + - name: Verify tag and package version + if: startsWith(github.ref, 'refs/tags/') + run: node -e 'const p=require("./package.json");if(process.env.GITHUB_REF_NAME!==`v${p.version}`)throw Error("Tag/version mismatch")' + - run: npm run build + - run: npm run lint + - run: npm run typecheck + - run: npm test + - run: npm run test:package + - run: npm run build:binaries + - run: npm publish --provenance --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + generate_release_notes: true + files: | + binaries/* + SKILL.md + CHANGELOG.md diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml new file mode 100644 index 0000000..389fe60 --- /dev/null +++ b/.github/workflows/version.yml @@ -0,0 +1,21 @@ +name: Version packages +on: + workflow_dispatch: +permissions: + contents: write + pull-requests: write +jobs: + version: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - uses: changesets/action@v1 + with: + version: npm run version:packages + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8c8e460 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# @bloxwap/agent-cli + +## 0.1.0 + +Initial implementation of managed-agent trading, funding and persistent runner. +Use Changesets for subsequent versioning and release notes. diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 02dba38..d355bf2 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -43,3 +43,4 @@ | #38 | done | Install/testnet README, custody architecture, schema-generated command reference and static docs site with gated Pages workflow. | | #39 | partial | Full mocked CLI/SDK/RPC lifecycle passes; opt-in real testnet lifecycle with mocked Relay, nightly secrets gate and journal artifacts. Real execution skipped without credentials. | | #40 | partial | Postinstall-free executable package, supported Node 22.12+ engine and global/Bun install smoke pass locally; macOS/Linux CI and optional compiled binaries. Registry install awaits publication. | +| #41 | partial | Changesets/version PR workflow, gated tag publish with npm provenance, generated release notes and binary/skill assets; publishing not run without registry credentials. | diff --git a/docs/releases.md b/docs/releases.md new file mode 100644 index 0000000..1d3a76a --- /dev/null +++ b/docs/releases.md @@ -0,0 +1,13 @@ +# Releases + +Add `.changeset/*.md` for user-visible changes. `npm run version:packages` applies +versioning/changelogs and synchronizes the packaged skill version. The version +workflow creates a version PR when manually invoked. + +A matching `vVERSION` tag builds/tests/packs and compiles Linux/macOS executables, +then publishes with npm provenance and attaches binaries, SKILL.md and changelog +to a generated-notes GitHub release. Enable repository variable +`BLOXWAP_PUBLISH=1` and configure `npm-release` environment plus `NPM_TOKEN` (or npm +trusted publishing) before using it. No publishing occurs without that gate. +Registry ownership, provenance and hosted CI execution need external credentials. +The implementation process does not push tags, publish packages or alter remotes. diff --git a/package-lock.json b/package-lock.json index ea33cb8..350f9fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "agent-cli": "dist/cli.js" }, "devDependencies": { + "@changesets/cli": "^3.0.3", "@types/node": "^26.6.5", "@types/qrcode": "^1.5.6", "@types/ws": "^8.18.2", @@ -58,6 +59,298 @@ "tiny-secp256k1": "2.2.4" } }, + "node_modules/@changesets/apply-release-plan": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-8.1.1.tgz", + "integrity": "sha512-nk2Hbq3M9NeHMdaIRBDvawKrQJzXB9sGyBUE5yWnIKtvdcNCPj75zBekJYr0gTT2ecJH3eFeTrc8eisPImcv0Q==", + "dev": true, + "dependencies": { + "@changesets/config": "^4.0.1", + "@changesets/format": "^0.1.2", + "@changesets/git": "^4.0.1", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "import-meta-resolve": "^4.2.0", + "jsonc-parser": "^3.3.1", + "semver": "^7.8.1" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/assemble-release-plan": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-7.0.0.tgz", + "integrity": "sha512-oEW8BxdA604kGGtDSCiHr5w9Tv4UWe9I2k61IBNZzCOE1kbYaJj4v+lFQNgcEZFkUc2pV/+hASErGDvpJOZCTg==", + "dev": true, + "dependencies": { + "@changesets/errors": "^1.0.0", + "@changesets/get-dependents-graph": "^3.0.0", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "semver": "^7.8.1" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/changelog-git": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-1.0.0.tgz", + "integrity": "sha512-3Dst2Ime2Op5nd4XmWJLPIgp11ZFqJqSkVug9izK6TDcIV4YlhPS4ECbEVR+eGI0bk0r1ItogD4j2Oli87bJrA==", + "dev": true, + "dependencies": { + "@changesets/types": "^7.0.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/cli": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-3.0.3.tgz", + "integrity": "sha512-L1i67bMfkK6iC/kDejuNO4pFgTRcWIObsD6Qof2woP1I4M8tj17DGlcpRaTzTv1DYSP5EMwWdEtR8X0Pj5Jg3Q==", + "dev": true, + "dependencies": { + "@changesets/apply-release-plan": "^8.1.1", + "@changesets/assemble-release-plan": "^7.0.0", + "@changesets/changelog-git": "^1.0.0", + "@changesets/config": "^4.0.1", + "@changesets/errors": "^1.0.0", + "@changesets/get-dependents-graph": "^3.0.0", + "@changesets/git": "^4.0.1", + "@changesets/pre": "^3.0.0", + "@changesets/read": "^1.0.1", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "@changesets/write": "^1.0.1", + "@clack/prompts": "^1.7.0", + "@manypkg/get-packages": "^3.1.0", + "@pnpm/deps.graph-sequencer": "^1100.0.1", + "cac": "^7.0.0", + "import-meta-resolve": "^4.2.0", + "launch-editor": "^2.14.1", + "package-manager-detector": "^1.6.0", + "semver": "^7.8.1", + "tinyexec": "^1.3.1" + }, + "bin": { + "changeset": "bin.js" + }, + "engines": { + "node": "^22.11 || ^24 || >=26", + "npm": ">=10.9.0", + "pnpm": ">=10.0.0", + "yarn": ">=4.5.2" + } + }, + "node_modules/@changesets/cli/node_modules/cac": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cac/-/cac-7.1.0.tgz", + "integrity": "sha512-LMDU/U2yA4IPIY8KT0NsNPWEYZvAMkbgyspxJ6btJaYV+WMDOHAZCmB0lsTObiHaRS1V/ztKAl7+TSkTqXyeoA==", + "dev": true, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@changesets/cli/node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@changesets/config": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@changesets/config/-/config-4.0.1.tgz", + "integrity": "sha512-PxpxSQLQSLDjceAgRq+4FWlU+HflDIw03mmPVipxoQx9xrXFUjmqTy3SE+n/2t+DqPdpPiLWVr5NRiLZvjyLwg==", + "dev": true, + "dependencies": { + "@changesets/get-dependents-graph": "^3.0.0", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "@manypkg/get-packages": "^3.1.0", + "picomatch": "^4.0.7" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/errors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/errors/-/errors-1.0.0.tgz", + "integrity": "sha512-ElN/mEzn6zmETgjwf5MclCMa9ef59sAR0lfO8VSYIsiRvbC2FbLB/92EoYw10Sl0kGixxHFiJZUSv7dA+YpR8g==", + "dev": true, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/format": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@changesets/format/-/format-0.1.2.tgz", + "integrity": "sha512-Caez5XtNXCFS/G5bwyav3wuXL0tMxVd2ZGbaumWbzN08tyzO21asCw7JZhNtVsAZDCvDRUzZN+Iit9SyRITSYA==", + "dev": true, + "dependencies": { + "package-manager-detector": "^1.8.0", + "tinyexec": "^1.3.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/format/node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@changesets/get-dependents-graph": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-3.0.0.tgz", + "integrity": "sha512-ji/t5wFA1zREKXRUePE6Qi+Qu2UgxCeSSGQrphezwvQZrp49B7sJ+8+wvM0tA7zPeSxYKCojDy3WWgrl+s+awg==", + "dev": true, + "dependencies": { + "@changesets/types": "^7.0.0", + "semver": "^7.8.1" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/git": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@changesets/git/-/git-4.0.1.tgz", + "integrity": "sha512-6vWpIAC4LkpmlqaIu37ViT5enyd9+uBwAiGqCGhASvkSHBgx4PrtTGXWTMFYpDmZbjgyonyVgMciPrW4MqtJsA==", + "dev": true, + "dependencies": { + "@changesets/errors": "^1.0.0", + "@changesets/types": "^7.0.0", + "@manypkg/get-packages": "^3.1.0", + "picomatch": "^4.0.4", + "tinyexec": "^1.3.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/git/node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@changesets/parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-1.0.0.tgz", + "integrity": "sha512-P0iaMb9p9CRYZiTgAllEIF9AUMQHIy1G72tKlcIqJp61icZSsKQNiOPdxAMZG8m/DvZwt/oz5xEbTpike//dWg==", + "dev": true, + "dependencies": { + "@changesets/types": "^7.0.0", + "yaml": "^2.9.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/pre": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-3.0.0.tgz", + "integrity": "sha512-Zm/6YliV/a2oeWTqHJf6KxLrQwgcK1i/BRDl2m0EKZvbnxV5fG9QRhwJJGshjsZTUTS6dkfURQ2K6aAvgNw/3Q==", + "dev": true, + "dependencies": { + "@changesets/errors": "^1.0.0", + "@changesets/types": "^7.0.0", + "@manypkg/get-packages": "^3.1.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/read": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@changesets/read/-/read-1.0.1.tgz", + "integrity": "sha512-nzvSC6RcTiWf/dsuwZFXpLzGGsRHYCL68U3uHV7srsulU93aVWY7u2GEJiDZ+4GIIElfaW5EqtKC0UHroY+LTQ==", + "dev": true, + "dependencies": { + "@changesets/git": "^4.0.1", + "@changesets/parse": "^1.0.0", + "@changesets/types": "^7.0.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/should-skip-package": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/should-skip-package/-/should-skip-package-1.0.0.tgz", + "integrity": "sha512-pwqoJmbONn1XgXmZXPEExgAaT+HdZLjALFTDgIm+PnS5KeO2nLtzA2/Q+4aMFY14kFMuXKG30MObhvDzWgzDgg==", + "dev": true, + "dependencies": { + "@changesets/types": "^7.0.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/types": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-7.0.0.tgz", + "integrity": "sha512-c5GoiQyt3pxiXjrWSNoP8/GRf4kG+VnKzovx1OQM8dYYALlSwgedmkPmJ+ZqGxqwg9D3Bkj85Uo4KLd5BN3A0w==", + "dev": true, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/write": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@changesets/write/-/write-1.0.1.tgz", + "integrity": "sha512-q/ThtP9gcnEP6xlv7LrY26C2mHqdGBti/MmOVngt/M/oIGYkssmQGxPK9WzBNt2juVcH/vml2WQ+ra8LXYOTaA==", + "dev": true, + "dependencies": { + "@changesets/format": "^0.1.1", + "@changesets/types": "^7.0.0", + "human-id": "^4.2.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@clack/core": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.5.1.tgz", + "integrity": "sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ==", + "dev": true, + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.8.1.tgz", + "integrity": "sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ==", + "dev": true, + "dependencies": { + "@clack/core": "1.5.1", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", @@ -734,6 +1027,45 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@manypkg/find-root": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-3.1.0.tgz", + "integrity": "sha512-BcSqCyKhBVZ5YkSzOiheMCV41kqAFptW6xGqYSTjkVTl9XQpr+pqHhwgGCOHQtjDCv7Is6EFyA14Sm5GVbVABA==", + "dev": true, + "dependencies": { + "@manypkg/tools": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@manypkg/get-packages": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-3.1.0.tgz", + "integrity": "sha512-0TbBVyvPrP7xGYBI/cP8UP+yl/z+HtbTttAD7FMAJgn/kXOTwh5/60TsqP9ZYY710forNfyV0N8P/IE/ujGZJg==", + "dev": true, + "dependencies": { + "@manypkg/find-root": "^3.1.0", + "@manypkg/tools": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@manypkg/tools": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@manypkg/tools/-/tools-2.1.2.tgz", + "integrity": "sha512-6QEf6yqFbETdwGITKq57aYoPfX/3K8XFNwsAlx0C1M7o8cb79sv1M3w+tWuWvIcSbNqrLF7OD7YpZMVVz335hQ==", + "dev": true, + "dependencies": { + "jju": "^1.4.0", + "tinyglobby": "^0.2.13", + "yaml": "^2.9.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@napi-rs/lzma-linux-x64-gnu": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", @@ -798,6 +1130,18 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@pnpm/deps.graph-sequencer": { + "version": "1100.0.1", + "resolved": "https://registry.npmjs.org/@pnpm/deps.graph-sequencer/-/deps.graph-sequencer-1100.0.1.tgz", + "integrity": "sha512-pOr5+q1fLYKwFN3LAJuGZEnfXDcQ73zqgDHMtGy+K+uIoUqyY+6MeDCWFwfu+4EFuq76I5EPFofoNAI+Bmmq4A==", + "dev": true, + "engines": { + "node": ">=22.13" + }, + "funding": { + "url": "https://opencollective.com/pnpm" + } + }, "node_modules/@privy-io/node": { "version": "0.35.0", "resolved": "https://registry.npmjs.org/@privy-io/node/-/node-0.35.0.tgz", @@ -2260,6 +2604,30 @@ "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==" }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2396,6 +2764,15 @@ "integrity": "sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ==", "optional": true }, + "node_modules/human-id": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.2.2.tgz", + "integrity": "sha512-Z+8unD6em/MJwyrkWQ2M3wygRGI1+MztbutiNwk1DCuEPXOZRWGjtcMPZYP1exY9iNZMhd7ZOSMQSo9WYtiQRw==", + "dev": true, + "bin": { + "human-id": "dist/cli.js" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2421,6 +2798,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -2480,6 +2867,12 @@ "ws": "*" } }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true + }, "node_modules/jose": { "version": "6.2.12", "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", @@ -2543,6 +2936,12 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2552,6 +2951,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/launch-editor": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.2.tgz", + "integrity": "sha512-NHJEb30MGBCkmZXLZwz25t37QR4aY03ZzPYUJXOJaOa92uWwExI9SBQUuIznBYc1xkD7cJNGX7oc3Nl3sq2kQQ==", + "dev": true, + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.10.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -2847,6 +3256,12 @@ "node": ">=6" } }, + "node_modules/package-manager-detector": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.9.0.tgz", + "integrity": "sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==", + "dev": true + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3174,12 +3589,30 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz", + "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, "node_modules/source-map": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", @@ -3918,6 +4351,21 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "15.4.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", diff --git a/package.json b/package.json index 7091a3b..cece521 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,12 @@ "docs:generate": "npm run build && node scripts/generate-docs.mjs", "test:e2e": "node scripts/e2e-testnet.mjs", "test:package": "node scripts/package-smoke.mjs", - "build:binaries": "node scripts/build-binaries.mjs" + "build:binaries": "node scripts/build-binaries.mjs", + "changeset": "changeset", + "version:packages": "changeset version && node scripts/sync-skill-version.mjs" }, "devDependencies": { + "@changesets/cli": "^3.0.3", "@types/node": "^26.6.5", "@types/qrcode": "^1.5.6", "@types/ws": "^8.18.2", diff --git a/scripts/sync-skill-version.mjs b/scripts/sync-skill-version.mjs new file mode 100644 index 0000000..14c96e6 --- /dev/null +++ b/scripts/sync-skill-version.mjs @@ -0,0 +1,4 @@ +import {readFileSync,writeFileSync} from 'node:fs'; +const {version}=JSON.parse(readFileSync('package.json','utf8')); +const skill=readFileSync('SKILL.md','utf8'); +writeFileSync('SKILL.md',skill.replace(/(metadata:\n version: )"[^"]+"/,`$1"${version}"`)); From 1885f8b01c26f34f33eed538da715f73c6a4ea78 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 13:55:34 -0700 Subject: [PATCH 42/43] feat: distribute and install version-matched trading skill (#42) --- .prettierignore | 8 + README.md | 3 +- SKILL.md | 21 +- docs/ACCEPTANCE.md | 26 + docs/PROGRESS.md | 9 +- docs/commands.md | 1182 +++++++++++++++++++++++++++- docs/config.md | 1 + docs/events.md | 1 + docs/funding.md | 4 +- eslint.config.js | 5 +- package-lock.json | 1271 ++++++++++++++++++++++--------- package.json | 12 +- scripts/build-binaries.mjs | 23 +- scripts/e2e-relay-hook.mjs | 33 +- scripts/e2e-testnet.mjs | 164 +++- scripts/finalize-build.mjs | 4 +- scripts/generate-docs.mjs | 37 +- scripts/generate-skill.mjs | 9 + scripts/package-smoke.mjs | 68 +- scripts/sync-skill-version.mjs | 11 +- src/cli.ts | 32 +- src/commands/audit.ts | 25 +- src/commands/cancel.ts | 85 ++- src/commands/config.ts | 57 +- src/commands/deposit.ts | 38 +- src/commands/events.ts | 49 +- src/commands/fund.ts | 152 +++- src/commands/init.ts | 152 +++- src/commands/leverage.ts | 71 +- src/commands/logs.ts | 96 ++- src/commands/market.ts | 33 + src/commands/modify.ts | 134 +++- src/commands/order.ts | 168 +++- src/commands/read.ts | 195 ++++- src/commands/revoke.ts | 94 ++- src/commands/rotate.ts | 196 ++++- src/commands/run.ts | 60 +- src/commands/skill.ts | 52 ++ src/core/account.ts | 13 + src/core/audit.ts | 195 ++++- src/core/builder.ts | 40 +- src/core/config.ts | 125 ++- src/core/hlclient.ts | 135 +++- src/core/http.ts | 42 +- src/core/logger.ts | 119 ++- src/core/nonce.ts | 63 +- src/core/output-schemas.ts | 247 ++++++ src/core/request-context.ts | 7 + src/core/retry.ts | 39 +- src/core/risk.ts | 120 ++- src/core/router.ts | 241 ++++-- src/core/skill-content.ts | 3 + src/core/trading.ts | 189 ++++- src/errors/index.ts | 55 +- src/errors/mappers.ts | 62 +- src/funding/amounts.ts | 49 +- src/funding/qr.ts | 37 +- src/funding/relay.ts | 109 ++- src/funding/signatures.ts | 101 ++- src/funding/status.ts | 108 ++- src/funding/store.ts | 36 +- src/runner/events.ts | 160 +++- src/runner/health.ts | 87 ++- src/runner/lifecycle.ts | 207 ++++- src/runner/rpc.ts | 358 +++++++-- src/runner/subscriptions.ts | 207 ++++- src/wallet/approval.ts | 99 ++- src/wallet/expiry.ts | 61 +- src/wallet/policy.ts | 58 +- src/wallet/privy.ts | 149 +++- src/wallet/safety.ts | 46 +- test/amounts.test.ts | 21 +- test/approval.test.ts | 22 +- test/audit.test.ts | 31 +- test/builder.test.ts | 21 +- test/cancel.test.ts | 29 +- test/config.test.ts | 24 +- test/daily-loss.test.ts | 22 + test/deposit-signatures.test.ts | 115 +++ test/deposit-status.test.ts | 45 +- test/errors.test.ts | 32 +- test/events.test.ts | 45 +- test/expiry.test.ts | 36 +- test/fixtures.ts | 64 ++ test/fund.test.ts | 46 +- test/health.test.ts | 27 +- test/hlclient.test.ts | 45 +- test/leverage.test.ts | 53 +- test/lifecycle.test.ts | 283 ++++++- test/logger.test.ts | 53 +- test/logs.test.ts | 49 +- test/mappers.test.ts | 33 +- test/modify.test.ts | 121 ++- test/order.test.ts | 82 +- test/policy.test.ts | 22 +- test/privy.integration.test.ts | 12 + test/privy.test.ts | 57 +- test/qr.test.ts | 20 +- test/read.test.ts | 25 +- test/relay.test.ts | 36 +- test/retry.test.ts | 57 +- test/revoke.test.ts | 20 +- test/risk.test.ts | 57 +- test/rotate.test.ts | 29 +- test/router.test.ts | 72 +- test/rpc.test.ts | 59 +- test/runner.test.ts | 65 +- test/safety.test.ts | 50 +- test/setup.ts | 20 + test/skill.test.ts | 42 + test/subscriptions.test.ts | 52 +- tsconfig.json | 14 +- vitest.config.ts | 7 + vitest.integration.config.ts | 4 + 114 files changed, 8509 insertions(+), 1658 deletions(-) create mode 100644 .prettierignore create mode 100644 docs/ACCEPTANCE.md create mode 100644 scripts/generate-skill.mjs create mode 100644 src/commands/market.ts create mode 100644 src/commands/skill.ts create mode 100644 src/core/account.ts create mode 100644 src/core/output-schemas.ts create mode 100644 src/core/request-context.ts create mode 100644 src/core/skill-content.ts create mode 100644 test/daily-loss.test.ts create mode 100644 test/deposit-signatures.test.ts create mode 100644 test/fixtures.ts create mode 100644 test/privy.integration.test.ts create mode 100644 test/setup.ts create mode 100644 test/skill.test.ts create mode 100644 vitest.config.ts create mode 100644 vitest.integration.config.ts diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..2ef7ba2 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +site/ +binaries/ +src/core/skill-content.ts +docs/issues/ +docs/commands.md +test/__snapshots__/ diff --git a/README.md b/README.md index 3854160..d63c8e1 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ node dist/cli.js fund --network testnet --amount 10 --origin-chain 42161 node dist/cli.js status --network testnet # Continue only when active deposits report settled. node dist/cli.js run --network testnet --detach +node dist/cli.js market --coin BTC node dist/cli.js order --json '{"coin":"BTC","side":"buy","size":"0.001","price":"20000","tif":"Gtc"}' node dist/cli.js events --follow ``` @@ -58,7 +59,7 @@ defaults charge zero fees instead of inventing a recipient. ## Development and docs `npm run build`, `npm run lint`, `npm run typecheck`, `npm test` must pass before -committing. Unit tests mock services. Credential-dependent tests are gated. +committing. Unit tests mock services. Credential-dependent tests are gated: `npm run test:sandbox` and `npm run test:e2e` skip without the required environment. The master-key testnet harness is CI-only; unit tests never access the network. `npm run docs:generate` creates [command schemas](docs/commands.md) and a static site in `site/`. [Docs index](docs/index.md), [security](docs/security.md), diff --git a/SKILL.md b/SKILL.md index a568afe..2c1db1c 100644 --- a/SKILL.md +++ b/SKILL.md @@ -23,15 +23,22 @@ Use `--network testnet` during testing; never silently change to mainnet. resulting signature with `init --signature 0x...`. Submit builder approval separately if requested. Continue only after `approved:true`. 3. Run `fund --amount 10 --origin-chain 42161`. Show `qrAscii`/deposit address, - origin chain/token, minimum, fees, expected received amount and expiry. The user + origin chain/token, minimum, fees, expected received amount and expiry. On + testnet, do not treat the default Arbitrum mainnet chain/token as a test route: + use an explicitly supported test chain/token and `originDecimals`, or the + credential-gated CI harness with mocked Relay and pre-funded test collateral. + If no test route is known, stop funding and report the missing route. The user transfers the quoted origin token. If `signatureSteps` require signatures, show the exact payload and submit each via `deposit` in returned order. -4. Poll `status` with bounded backoff until each active deposit is `settled`. +4. Poll `status` until each active deposit is `settled`, for at most 12 polls + or 5 minutes, whichever comes first. Start with 1-second delays, double to a + 15-second cap, and stop with an explicit pending report if the budget expires. Settlement requires Relay success and Hyperliquid balance receipt. Stop on failed/refunded deposits and explain their status. Do not place orders while any deposit is awaiting funds or pending. 5. Run `run --detach`, then monitor `events --follow --since SEQ`. Persist the - last handled sequence; replay after reconnect. A fill updates the trading + last handled sequence; replay after reconnect. An unfilled or rejected order + remains incomplete: do not claim the requested trade succeeded. A fill updates the trading plan, an error follows the catalog below, and expiry warnings prompt rotation using external master-wallet approval. @@ -53,6 +60,7 @@ Full inputs/outputs are discoverable with `schema`; amounts/prices are strings. | `deposit --deposit-id ID --step-id step:0 --signature 0x...` | `{"ok":true,"data":{"depositId":"ID","steps":[]}}` | | `status` | `{"ok":true,"data":{"balance":{"accountValue":"100"},"agent":{"approved":true,"daysRemaining":"179"},"deposits":[],"policy":{"id":"...","version":"1"},"runner":{"pid":"123"}}}` | | `balance` | `{"ok":true,"data":{"accountValue":"100","accountValueUnits":"10000000000","withdrawable":"96","marginUsed":"4"}}` | +| `market --coin BTC` | `{"ok":true,"data":{"coin":"BTC","asset":"0","szDecimals":"5","maxLeverage":"40","mid":"20000"}}` | | `positions` | `{"ok":true,"data":[{"coin":"BTC","size":"0.001","entryPrice":"20000","leverage":"5","marginMode":"cross"}]}` | | `orders` | `{"ok":true,"data":[{"coin":"BTC","oid":"123","sz":"0.001","limitPx":"20000","side":"B"}]}` | | `order --json '{"coin":"BTC","side":"buy","size":"0.001","price":"20000","tif":"Gtc","cloid":"0x11111111111111111111111111111111"}'` | `{"ok":true,"data":{"result":{"status":"ok","response":{"data":{"statuses":[{"resting":{"oid":123}}]}}},"builderFeeRate":"0%","cloid":"0x..."}}` | @@ -71,6 +79,11 @@ Full inputs/outputs are discoverable with `schema`; amounts/prices are strings. | `audit --intent ID` | `{"ok":true,"data":[{"intentId":"ID","stage":"decision","data":{"allowed":true},"previousHash":"...","hash":"..."}]}` | | `skill install --target codex` | `{"ok":true,"data":{"target":"codex","path":".../agent-cli/SKILL.md","version":"0.1.0"}}` | +Before an order, query `market --coin BTC` for `mid`, `szDecimals`, asset and +market leverage ceiling. Compute size/notional with decimal strings/bigints, +using the user-approved limit or the bounded IOC execution price. A valid schema +alone does not establish that an order is tradable or within the budget. + Use `--intent-id ID --intent "reason"` on trading actions to connect the user's reasoning to the audit. Keep cloid stable when retrying a known order intent. For TP/SL use `type:"tp"|"sl"` and `triggerPrice` with size/price/reduceOnly; @@ -80,7 +93,7 @@ market orders use an explicit slippage-bounded IOC, not an unbounded market orde | Code | Action | | --- | --- | -| RATE_LIMIT, NETWORK_ERROR | Bounded exponential backoff; stop after configured attempts | +| RATE_LIMIT, NETWORK_ERROR | At most 3 read retries with exponential backoff; for uncertain mutations, reconcile order state/cloid before retrying. Stop and report unresolved results | | NONCE_COLLISION | Retry using a newly issued nonce; inspect a stale lock if persistent | | DEPOSIT_PENDING | Poll status until settled; stop on terminal funding failure | | RUNNER_NOT_RUNNING | Start runner or use configured direct fallback | diff --git a/docs/ACCEPTANCE.md b/docs/ACCEPTANCE.md new file mode 100644 index 0000000..21c14e2 --- /dev/null +++ b/docs/ACCEPTANCE.md @@ -0,0 +1,26 @@ +# Acceptance review + +This records implementation evidence, not a claim that credential-dependent +services or hosted workflows ran. See PROGRESS.md for the remaining partial +items. The final local run uses the SDK-supported Node 22 runtime and Bun. + +| Issues | Reviewed artifacts and meaningful validation | +| --- | --- | +| #1 | package scripts/bin, strict ESM tsconfig, CI workflow; build/lint/typecheck/Vitest smoke pass. Hosted CI awaits a push, which was explicitly excluded. | +| #2 | docs/adr/0001-runner-interface.md compares all three transports, method names, private permissions and Windows strategy. | +| #3–#4 | Stable append-only error catalog and serialization/exit tests; strict flags/JSON/stdin parser, input/output schemas, typo and uncaught-error tests. Streaming exceptions are explicit. | +| #5–#6 | Config validation/migration/atomic private writes/corrupt-file tests; recursive field/env/PEM redaction, request correlation and size/day rotation. | +| #7–#9 | Requested SDK, network factories, shared metadata cache/refresh, exact rounding fixtures, service mapping fixtures, bounded retries and exclusive persisted nonce tests. | +| #10–#13 | P-256 managed-wallet creation/linking and typed-data adapter; versioned Privy domain/source policy and local forbidden-action tests; resumable external master approvals with persisted per-master nonces, expiry bounds and builder approval verification. Missing official builder defaults and hash-preimage policy limitations are explicit in PROGRESS/custody docs. Sandbox calls are separately gated. | +| #14–#19 | Relay quote/fee/address fixture tests, bigint round trips including signed values, terminal QR snapshot and file renderer, idempotent funding store, ordered/resumable signature tests, terminal deposit transitions and collateral receipt excluding marked PnL. | +| #20–#25 | Numeric-string account/order/market projections, approval/policy/deposit/expiry/runner status; every risk bound tested, opposing pending exposure and realized/marked daily PnL checked; SDK order fixture tests cover limit/IOC/slippage/trigger behavior, cancellation, batch modification preserving reduce-only/TIF/trigger semantics and leverage ceilings/loss. | +| #26–#28 | Private PID/socket lifecycle, stale dead-PID recovery/readiness, serialized RPC, cross-network/config refusal and direct fallback only on confirmed absence; simulated WS drop, SDK resubscribe, paginated info gap-fill, persisted fill dedupe. Windows ACL limitations are documented. | +| #29–#32 | Locked sequence journal across independent writers/restarts, replay/types/follow, exact fill fee units; log filtering/redaction; runner heartbeat/staleness and deposit polling; mocked-clock expiry blocking and once-per-threshold warnings. | +| #33–#36 | New approval verified before signer handoff/old revocation, resumable rotation and immediate local revocation tests; fsynced cross-process hash-chain with intent/decision/signature digest/response/fill linkage; tamper/redaction tests and linked full-lifecycle records. CLI/RPC pre-schema hex/mnemonic detector and threat model. | +| #37–#38 | Frontmatter validator, independent simulated skill forward-test (schema-validated actions, ordered signatures, pending/rejection/expiry handling), bounded polling and supported-test-route guidance; install/custody README, generated input/output command reference and static Pages site. Real agent/testnet validation is credential-gated. | +| #39 | Offline full CLI/SDK/RPC account lifecycle test; separate opt-in nightly testnet script signs only in CI, uses mocked Relay and funded test collateral, covers onboarding/orders/events/rotation/revoke and exports event/audit artifacts. Live execution skips without secrets. | +| #40–#42 | Packed global installation and Bun smoke, executable bin, engine declaration, no package postinstall; Linux/macOS binary build, changesets/tag/version/provenance/asset pipeline behind publish gate; packaged/embedded skill, Claude/Codex paths, CODEX_HOME, overwrite protection and matching versions. Publishing/registry verification awaits external credentials. | + +No push, tag creation, remote changes, package publication or live-account trading +was performed. Local acceptance deliberately leaves unsupported external claims +unproven instead of labelling skipped checks as successful live tests. diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index d355bf2..0defbec 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -9,7 +9,7 @@ | #4 | done | Validated JSON/flags/stdin router, JSON schemas, envelopes and exit codes. | | #5 | done | Versioned validated config, atomic private writes, migration, commands and env overrides. | | #6 | done | JSON logging, recursive redaction, correlation IDs and size/day rotation. | -| #7 | done | Requested SDK adapter, lazy WS factory, cached metadata, decimal rounding and remote signer abstraction. SDK requires Node 22.12+; runtime smoke checked on Node 20. | +| #7 | done | Requested SDK adapter, lazy WS factory, cached metadata, decimal rounding and remote signer abstraction. SDK requires Node 22.12+; final verification uses supported Node 22 and Bun. Shared metadata cache is exposed via market discovery. | | #8 | done | Service error adapters, redacted upstream detail, fixture-driven classification and retry defaults. | | #9 | done | Bounded jittered retries and per-signer persisted monotonic nonces guarded by exclusive locks. Stale locks fail closed. | | #10 | done | Privy server SDK create/link, P-256 owner, private authorization storage and typed-data signer; sandbox test env-gated. | @@ -28,7 +28,7 @@ | #23 | done | Batch cancellation by oid/cloid, coin or account; unknown order normalization and mocked tests. | | #24 | done | Single/batch modification by oid/cloid with rounding and pre-signing risk checks. | | #25 | done | Cross/isolated leverage updates enforce market and configured ceilings with mocked tests. | -| #26 | done | Private exclusive PID lock, managed signer session, foreground/detach, idempotency and graceful shutdown; persistent stores survive restart. Stale locks fail closed. | +| #26 | done | Private exclusive PID lock, managed signer session, foreground/detach, idempotency and graceful shutdown; persistent stores survive restart. Dead PID recovery and explicit readiness; stale startup locks fail closed. | | #27 | done | Bounded newline JSON-RPC socket with private mode, method aliases, serialized trading, normalized errors and configurable direct fallback. | | #28 | done | SDK reconnect/resubscribe + heartbeat transport, user/market subscriptions, info gap-fill and persisted tid dedupe; simulated drop tests. | | #29 | done | Typed persisted monotonic event journal, replay/filter/follow stream, subscription/deposit emission and shutdown flush. | @@ -39,8 +39,11 @@ | #34 | done | Optional cancel-all, resumable master revocation payload, immediate local blocking and config/session clearing after exchange submission. | | #35 | done | Cross-process locked fsynced hash-chain audit, intent/decision/signature digest/response/fill linkage, rotation/revoke records and filters; tamper/redaction tests. | | #36 | done | Pre-schema recursive CLI/RPC secret detector, BIP39-like seed rejection, private authorization refs and env/log/error scrubbing; threat model docs. | -| #37 | partial | Validated self-contained skill with every command example, ordered funding/custody flow and complete error table; real agent testnet execution requires credentials (#39 gate). | +| #37 | partial | Independently forward-tested, schema-validated self-contained skill with every command example, ordered funding/custody flow and complete error table; real agent testnet execution requires credentials (#39 gate). | | #38 | done | Install/testnet README, custody architecture, schema-generated command reference and static docs site with gated Pages workflow. | | #39 | partial | Full mocked CLI/SDK/RPC lifecycle passes; opt-in real testnet lifecycle with mocked Relay, nightly secrets gate and journal artifacts. Real execution skipped without credentials. | | #40 | partial | Postinstall-free executable package, supported Node 22.12+ engine and global/Bun install smoke pass locally; macOS/Linux CI and optional compiled binaries. Registry install awaits publication. | | #41 | partial | Changesets/version PR workflow, gated tag publish with npm provenance, generated release notes and binary/skill assets; publishing not run without registry credentials. | +| #42 | done | Final packaged flow audited and corrected (network/config isolation, exact loss/exposure/receipt checks, trigger preservation, readiness and stream errors). Skill shipped/embedded in npm and compiled CLI; Claude/Codex installation with CODEX_HOME, version synchronization, idempotency and overwrite protection. | + +Final local verification: supported Node 22, strict build/lint/typecheck and isolated offline tests. Sandbox/testnet scripts skip without credentials. npm audit reports zero vulnerabilities. Official builder defaults remain unspecified; fees stay disabled until configured. diff --git a/docs/commands.md b/docs/commands.md index 88a3e72..b1acd4a 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -56,7 +56,24 @@ Get, set or validate local configuration "const": false }, "code": { - "type": "string" + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] }, "message": { "type": "string" @@ -119,7 +136,46 @@ Create agent and emit resumable master-wallet approvals "ok": { "const": true }, - "data": {} + "data": { + "type": "object", + "properties": { + "masterAddress": { + "type": "string" + }, + "agentAddress": { + "type": "string" + }, + "expiry": { + "type": "number" + }, + "policyId": { + "type": "string" + }, + "approved": { + "type": "boolean" + }, + "requiresSignature": { + "type": "boolean" + }, + "approvals": {}, + "deeplink": { + "type": "string" + }, + "qrAscii": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "masterAddress", + "agentAddress", + "expiry", + "policyId", + "approved" + ] + } } }, { @@ -136,7 +192,24 @@ Create agent and emit resumable master-wallet approvals "const": false }, "code": { - "type": "string" + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] }, "message": { "type": "string" @@ -211,7 +284,67 @@ Quote funding with address, QR and transparent fees "ok": { "const": true }, - "data": {} + "data": { + "type": "object", + "properties": { + "depositId": { + "type": "string" + }, + "depositAddress": { + "type": "string" + }, + "originChain": { + "type": "number" + }, + "originCurrency": { + "type": "string" + }, + "minDeposit": { + "type": "string" + }, + "expectedReceived": { + "type": "string" + }, + "expectedReceivedHuman": { + "type": "string" + }, + "fees": {}, + "builderFeeRate": { + "type": "string" + }, + "expiresAt": { + "type": "number" + }, + "state": { + "type": "string" + }, + "signatureSteps": { + "type": "array", + "items": {} + }, + "qrAscii": { + "type": [ + "string", + "null" + ] + }, + "qrPath": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "depositId", + "depositAddress", + "expectedReceived", + "fees", + "builderFeeRate", + "expiresAt", + "qrAscii" + ] + } } }, { @@ -228,7 +361,24 @@ Quote funding with address, QR and transparent fees "const": false }, "code": { - "type": "string" + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] }, "message": { "type": "string" @@ -281,7 +431,22 @@ Resume deposit steps or submit a master-wallet signature "ok": { "const": true }, - "data": {} + "data": { + "type": "object", + "properties": { + "depositId": { + "type": "string" + }, + "steps": { + "type": "array", + "items": {} + }, + "complete": { + "type": "boolean" + } + }, + "required": [] + } } }, { @@ -298,7 +463,24 @@ Resume deposit steps or submit a master-wallet signature "const": false }, "code": { - "type": "string" + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] }, "message": { "type": "string" @@ -338,7 +520,161 @@ Read account status "ok": { "const": true }, - "data": {} + "data": { + "type": "object", + "properties": { + "balance": { + "type": "object", + "properties": { + "accountValue": { + "type": "string" + }, + "accountValueUnits": { + "type": "string" + }, + "withdrawable": { + "type": "string" + }, + "marginUsed": { + "type": "string" + } + }, + "required": [ + "accountValue", + "accountValueUnits", + "withdrawable", + "marginUsed" + ] + }, + "positions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "coin": { + "type": "string" + }, + "size": { + "type": "string" + }, + "entryPrice": { + "type": [ + "string", + "null" + ] + }, + "unrealizedPnl": { + "type": "string" + }, + "liquidationPrice": { + "type": [ + "string", + "null" + ] + }, + "leverage": { + "type": "string" + }, + "marginMode": { + "type": "string" + } + }, + "required": [ + "coin", + "size", + "leverage" + ] + } + }, + "agent": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "approved": { + "type": "boolean" + }, + "revoked": { + "type": "boolean" + }, + "expiry": { + "type": [ + "number", + "null" + ] + }, + "daysRemaining": { + "type": [ + "string", + "null" + ] + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [] + }, + "policy": { + "type": "object", + "properties": { + "id": { + "type": [ + "string", + "null" + ] + }, + "version": { + "type": "string" + } + }, + "required": [] + }, + "deposits": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "state": { + "enum": [ + "awaiting_funds", + "pending", + "settled", + "failed", + "refunded" + ] + } + }, + "required": [] + } + }, + "approvedMaxBuilderFee": { + "type": "string" + }, + "runner": { + "type": [ + "object", + "null" + ] + } + }, + "required": [ + "balance", + "positions", + "agent", + "policy", + "deposits", + "approvedMaxBuilderFee", + "runner" + ] + } } }, { @@ -355,7 +691,24 @@ Read account status "const": false }, "code": { - "type": "string" + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] }, "message": { "type": "string" @@ -395,7 +748,29 @@ Read account balance "ok": { "const": true }, - "data": {} + "data": { + "type": "object", + "properties": { + "accountValue": { + "type": "string" + }, + "accountValueUnits": { + "type": "string" + }, + "withdrawable": { + "type": "string" + }, + "marginUsed": { + "type": "string" + } + }, + "required": [ + "accountValue", + "accountValueUnits", + "withdrawable", + "marginUsed" + ] + } } }, { @@ -412,7 +787,24 @@ Read account balance "const": false }, "code": { - "type": "string" + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] }, "message": { "type": "string" @@ -452,7 +844,46 @@ Read account positions "ok": { "const": true }, - "data": {} + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "coin": { + "type": "string" + }, + "size": { + "type": "string" + }, + "entryPrice": { + "type": [ + "string", + "null" + ] + }, + "unrealizedPnl": { + "type": "string" + }, + "liquidationPrice": { + "type": [ + "string", + "null" + ] + }, + "leverage": { + "type": "string" + }, + "marginMode": { + "type": "string" + } + }, + "required": [ + "coin", + "size", + "leverage" + ] + } + } } }, { @@ -469,7 +900,24 @@ Read account positions "const": false }, "code": { - "type": "string" + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] }, "message": { "type": "string" @@ -509,7 +957,42 @@ Read account orders "ok": { "const": true }, - "data": {} + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "coin": { + "type": "string" + }, + "oid": { + "type": "string" + }, + "sz": { + "type": "string" + }, + "limitPx": { + "type": "string" + }, + "side": { + "enum": [ + "A", + "B" + ] + }, + "cloid": { + "type": "string" + } + }, + "required": [ + "coin", + "oid", + "sz", + "limitPx", + "side" + ] + } + } } }, { @@ -526,7 +1009,24 @@ Read account orders "const": false }, "code": { - "type": "string" + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] }, "message": { "type": "string" @@ -634,7 +1134,26 @@ Place limit, IOC market or TP/SL order "ok": { "const": true }, - "data": {} + "data": { + "type": "object", + "properties": { + "result": {}, + "builderFeeRate": { + "type": "string" + }, + "cloid": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "result", + "builderFeeRate", + "cloid" + ] + } } }, { @@ -651,7 +1170,24 @@ Place limit, IOC market or TP/SL order "const": false }, "code": { - "type": "string" + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] }, "message": { "type": "string" @@ -730,7 +1266,22 @@ Cancel by oid/cloid, coin or all "ok": { "const": true }, - "data": {} + "data": { + "type": "object", + "properties": { + "cancelled": { + "type": "array", + "items": { + "type": "string" + } + }, + "result": {} + }, + "required": [ + "cancelled", + "result" + ] + } } }, { @@ -747,7 +1298,24 @@ Cancel by oid/cloid, coin or all "const": false }, "code": { - "type": "string" + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] }, "message": { "type": "string" @@ -833,7 +1401,18 @@ Modify price/size of single or batch resting orders "ok": { "const": true }, - "data": {} + "data": { + "type": "object", + "properties": { + "status": { + "type": "string" + }, + "response": {} + }, + "required": [ + "status" + ] + } } }, { @@ -850,7 +1429,24 @@ Modify price/size of single or batch resting orders "const": false }, "code": { - "type": "string" + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] }, "message": { "type": "string" @@ -918,7 +1514,18 @@ Set market leverage and cross/isolated margin mode "ok": { "const": true }, - "data": {} + "data": { + "type": "object", + "properties": { + "status": { + "type": "string" + }, + "response": {} + }, + "required": [ + "status" + ] + } } }, { @@ -935,7 +1542,24 @@ Set market leverage and cross/isolated margin mode "const": false }, "code": { - "type": "string" + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] }, "message": { "type": "string" @@ -988,7 +1612,34 @@ Start persistent signing runner (foreground or detach) "ok": { "const": true }, - "data": {} + "data": { + "type": "object", + "properties": { + "pid": { + "type": "integer" + }, + "startedAt": { + "type": "number" + }, + "network": { + "enum": [ + "mainnet", + "testnet" + ] + }, + "ready": { + "type": "boolean" + }, + "existing": { + "type": "boolean" + } + }, + "required": [ + "pid", + "startedAt", + "network" + ] + } } }, { @@ -1005,7 +1656,24 @@ Start persistent signing runner (foreground or detach) "const": false }, "code": { - "type": "string" + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] }, "message": { "type": "string" @@ -1077,7 +1745,40 @@ Replay or follow JSON-line runner events "ok": { "const": true }, - "data": {} + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "seq": { + "type": "integer", + "minimum": 1 + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "type": { + "enum": [ + "fill", + "order_update", + "position_change", + "deposit", + "error", + "agent_expiry_warning", + "runner_state" + ] + }, + "data": {} + }, + "required": [ + "seq", + "timestamp", + "type", + "data" + ] + } + } } }, { @@ -1094,7 +1795,24 @@ Replay or follow JSON-line runner events "const": false }, "code": { - "type": "string" + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] }, "message": { "type": "string" @@ -1159,7 +1877,10 @@ Read/follow redacted JSON-line runner logs "ok": { "const": true }, - "data": {} + "data": { + "description": "JSON-line stream of redacted log objects", + "type": "object" + } } }, { @@ -1176,7 +1897,24 @@ Read/follow redacted JSON-line runner logs "const": false }, "code": { - "type": "string" + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] }, "message": { "type": "string" @@ -1229,7 +1967,34 @@ Approve new agent, hot-swap signer, then revoke old agent "ok": { "const": true }, - "data": {} + "data": { + "type": "object", + "properties": { + "oldAgent": { + "type": "string" + }, + "newAgent": { + "type": "string" + }, + "swapped": { + "type": "boolean" + }, + "oldRevoked": { + "type": "boolean" + }, + "requiresSignature": { + "type": "boolean" + }, + "approval": {}, + "revokeApproval": {} + }, + "required": [ + "oldAgent", + "newAgent", + "swapped", + "oldRevoked" + ] + } } }, { @@ -1246,7 +2011,24 @@ Approve new agent, hot-swap signer, then revoke old agent "const": false }, "code": { - "type": "string" + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] }, "message": { "type": "string" @@ -1303,7 +2085,31 @@ Disable local agent and prepare master-signed exchange revocation "ok": { "const": true }, - "data": {} + "data": { + "type": "object", + "properties": { + "agentAddress": { + "type": "string" + }, + "locallyRevoked": { + "type": "boolean" + }, + "exchangeRevoked": { + "type": "boolean" + }, + "requiresSignature": { + "type": "boolean" + }, + "approval": {} + }, + "required": [ + "agentAddress", + "locallyRevoked", + "exchangeRevoked", + "requiresSignature", + "approval" + ] + } } }, { @@ -1320,7 +2126,24 @@ Disable local agent and prepare master-signed exchange revocation "const": false }, "code": { - "type": "string" + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] }, "message": { "type": "string" @@ -1370,7 +2193,174 @@ Verify and query the hash-chained trading audit "ok": { "const": true }, - "data": {} + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "seq": { + "type": "integer" + }, + "timestamp": { + "type": "string" + }, + "intentId": { + "type": "string" + }, + "requestId": { + "type": "string" + }, + "cloid": { + "type": "string" + }, + "stage": { + "type": "string" + }, + "data": {}, + "previousHash": { + "type": "string" + }, + "hash": { + "type": "string" + } + }, + "required": [ + "seq", + "timestamp", + "intentId", + "stage", + "data", + "previousHash", + "hash" + ] + } + } + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## skill + +Install packaged version-matched skill for Claude Code or Codex + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "args": { + "default": [ + "install" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "target": { + "default": "codex", + "type": "string", + "enum": [ + "claude", + "codex" + ] + }, + "force": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "args", + "target", + "force" + ], + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": { + "type": "object", + "properties": { + "target": { + "enum": [ + "claude", + "codex" + ] + }, + "path": { + "type": "string" + }, + "version": { + "type": "string" + }, + "changed": { + "type": "boolean" + } + }, + "required": [ + "target", + "path", + "version", + "changed" + ] + } } }, { @@ -1387,8 +2377,132 @@ Verify and query the hash-chained trading audit "const": false }, "code": { + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] + }, + "message": { "type": "string" }, + "retryable": { + "type": "boolean" + }, + "details": {} + } + } + ] + } +} +``` + +## market + +Discover market asset, precision, leverage ceiling and current mid + +```json +{ + "input": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "coin": { + "type": "string" + } + }, + "required": [ + "coin" + ], + "additionalProperties": false + }, + "output": { + "oneOf": [ + { + "type": "object", + "required": [ + "ok", + "data" + ], + "properties": { + "ok": { + "const": true + }, + "data": { + "type": "object", + "properties": { + "coin": { + "type": "string" + }, + "asset": { + "type": "string" + }, + "szDecimals": { + "type": "string" + }, + "maxLeverage": { + "type": "string" + }, + "mid": { + "type": "string" + } + }, + "required": [ + "coin", + "asset", + "szDecimals", + "maxLeverage", + "mid" + ] + } + } + }, + { + "type": "object", + "required": [ + "ok", + "code", + "message", + "retryable", + "details" + ], + "properties": { + "ok": { + "const": false + }, + "code": { + "enum": [ + "INSUFFICIENT_MARGIN", + "POLICY_REJECTED", + "AGENT_EXPIRED", + "AGENT_NOT_APPROVED", + "DEPOSIT_PENDING", + "NONCE_COLLISION", + "RATE_LIMIT", + "INVALID_SIZE", + "INVALID_PRICE", + "UNKNOWN_MARKET", + "ORDER_NOT_FOUND", + "RUNNER_NOT_RUNNING", + "CONFIG_INVALID", + "NETWORK_ERROR", + "UPSTREAM_ERROR", + "INTERNAL" + ] + }, "message": { "type": "string" }, diff --git a/docs/config.md b/docs/config.md index e75bb74..eb38c35 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1,6 +1,7 @@ # Configuration Default path: `~/.bloxwap/config.json`. `--config` selects another file. +All command JSON objects reject unknown fields to catch intent-changing typos. Version 1 is validated on read and write; versionless config migrates to version 1. Unknown versions fail closed. Writes use a private temporary file and atomic rename. diff --git a/docs/events.md b/docs/events.md index 11925d7..a9c867a 100644 --- a/docs/events.md +++ b/docs/events.md @@ -2,6 +2,7 @@ Each JSON line is `{seq,timestamp,type,data}`. Sequence numbers strictly increase across restarts; timestamps are ISO UTC. Types: `fill` (upstream fill with tid), +Fill fees additionally have exact 8-decimal `feeUnits` strings. `order_update`, `position_change`, `deposit` (id/state), `error` (normalized failure), `agent_expiry_warning`, `runner_state`. Secret fields are redacted. diff --git a/docs/funding.md b/docs/funding.md index a52edad..e802d0f 100644 --- a/docs/funding.md +++ b/docs/funding.md @@ -9,7 +9,9 @@ and a Hyperliquid deposit authorization. The exact payloads come from the quote; never synthesize them or sign them using the trade-only agent. `fund` emits `signatureSteps` with payloads and `requiresSignature`. Query `deposit --deposit-id ID` to resume; submit each in order with `--step-id ID --signature 0x...`. Completed -steps are persisted immediately. Retrying a completed flow is harmless. +steps are persisted immediately. Relay signature submission keeps the quoted +POST body intact and adds the signature as a query parameter, per the +[step execution contract](https://docs.relay.link/references/api/api_core_concepts/step-execution). Retrying a completed flow is harmless. Poll `status` until the deposit is settled; a bridge status alone is insufficient without the corresponding Hyperliquid balance increase. Testnet uses Relay's diff --git a/eslint.config.js b/eslint.config.js index 6b2de08..5fed676 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,2 +1,5 @@ import tseslint from 'typescript-eslint'; -export default tseslint.config({ignores:['dist/**','node_modules/**']},...tseslint.configs.recommended); +export default tseslint.config( + { ignores: ['dist/**', 'node_modules/**'] }, + ...tseslint.configs.recommended, +); diff --git a/package-lock.json b/package-lock.json index 350f9fe..a0afefa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "typescript": "^5.9.3", "typescript-eslint": "^8.71.2", "viem": "^2.57.5", - "vitest": "^3.2.7" + "vitest": "^5.0.4" }, "engines": { "node": ">=22.12.0" @@ -352,13 +352,14 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" @@ -368,13 +369,14 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -384,13 +386,14 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -400,13 +403,14 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -416,13 +420,14 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -432,13 +437,14 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -448,13 +454,14 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -464,13 +471,14 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -480,13 +488,14 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -496,13 +505,14 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -512,13 +522,14 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -528,13 +539,14 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -544,13 +556,14 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -560,13 +573,14 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -576,13 +590,14 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -592,13 +607,14 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -608,13 +624,14 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -624,13 +641,14 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -640,13 +658,14 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -656,13 +675,14 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -672,13 +692,14 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -688,13 +709,14 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openharmony" @@ -704,13 +726,14 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" @@ -720,13 +743,14 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -736,13 +760,14 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -752,13 +777,14 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1130,6 +1156,17 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@oxc-project/types": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.153.0.tgz", + "integrity": "sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, "node_modules/@pnpm/deps.graph-sequencer": { "version": "1100.0.1", "resolved": "https://registry.npmjs.org/@pnpm/deps.graph-sequencer/-/deps.graph-sequencer-1100.0.1.tgz", @@ -1193,6 +1230,284 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.13.tgz", + "integrity": "sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.13.tgz", + "integrity": "sha512-6LEoIrGy5vvCd2boNnv2soPSUZj/PRvhp8R/t9KlX+mIZxThIE/ZqxrGXxmuNX0FuvQFvLdZr7S4HrYpYBVhEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.13.tgz", + "integrity": "sha512-CaJSRzlQ6Q+zkDMVA9OkVxBjVoPRCDEE3v0FSJ9kIQBgRjFApA3Si4ijhbNVUji7ZJt+PfO0e46qy4nPInHUPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.13.tgz", + "integrity": "sha512-e4r340I5hxSzJkD3YJBxgzq/rJ3wwxwdLLb2FPt1Zpyem20nrQPT11DDEEkazwxaHumH8wpglsp/Ozgj/wT6mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.13.tgz", + "integrity": "sha512-hoBWDZ4/K/mJKVMbsgUd1jfpA2DiOrJ54mpegObU9TBHBBrtX4FLIXJJQRDPZ/2Dn91GvPob+Z2hwKJzf4E5SQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.13.tgz", + "integrity": "sha512-oF9fZu3vTWnTiVAkDFxU9PJLt1nh20K9RRpuHsR7ZnE2L7XookBbDBVLojCVSLGbvtSlxeUNnjAj6DbNsKyzbg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.13.tgz", + "integrity": "sha512-DTpb/+jMiqWMPgphn4ksW1zW+XLJ6dJJ9FMvnIk9I291LhCctMPhMYJrP5t9p6C9qa3KXadDdUEZHBaPh3kNxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.13.tgz", + "integrity": "sha512-xU+Kvv9N2A3SY35zFh1FWWNjxbadE/BKeqnkXe7vScU4GZsw757BlVmDLITZEq8DPpk6m7uoDYkK7P+x/uU5Ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.13.tgz", + "integrity": "sha512-XMk/hnhh25oQ756Lclb1Q11h4ll0xC0Esa5w4Vzp+mAE2S3AGJgeM0SonrIgY8fHhjarb6hcoTuwoFG0VVP2ig==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.13.tgz", + "integrity": "sha512-U1BoTHYmlywE2n3HUWjzZ+8EQsKr+OwpxYuDWaubx42tUL8QSL7nOqRz/NVHanibbFP3M1Rxuprz/wNcbMKjqw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.13.tgz", + "integrity": "sha512-J4cm80Pc6wEwV5AZ42o62sIQgDk+Y7R2s646jIwuvO+4V46QB58N4vVuMPWCBaNgursXLpmUWJV//WQAkPvHBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.13.tgz", + "integrity": "sha512-W0euZS40U4V6JkmEnVCIX9mcsL6cbXQ4X8c2w+MMOIjFg68TXUW+u3RT+2e1t1pv9mg/EP3pDtK3POXdQ7Rxyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.13.tgz", + "integrity": "sha512-do/AF/4n9MRshia3a5aXoy8xM2c6mgfT6D7H/dY6my5z+hfCIR4ab7mk4IRj++nsEQpKy39kd0DQnDq+r692tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.13.tgz", + "integrity": "sha512-6FtUf2cJZobutJdxnGYX8z0vtbgqU0b2v8vfBhkXb9wp2MppMFtkunE+IaOntyi01rkIQoPW2GHABtfzsC0BRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.13.tgz", + "integrity": "sha512-UKhFaZTeTcbKrb3iU82m17jVcPEAoqDkCN5MBqgPyZSwp/cQmib21crTS0DdH7T1B7wZRWEI89YQAOJMet5yhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.64.6", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.6.tgz", @@ -1584,6 +1899,7 @@ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, + "license": "MIT", "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" @@ -1593,7 +1909,8 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.9", @@ -1901,38 +2218,24 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@vitest/expect": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", - "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", - "dev": true, - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.7", - "@vitest/utils": "3.2.7", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@vitest/mocker": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", - "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.4.tgz", + "integrity": "sha512-tqAlrYWce4iUxhr3ncNohejtCTOcHeE8XWrSNZY3OhPoDn01yeGv+gnaBy64ATp7o6pb03eIbBAjzmibpc9vIQ==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.7", + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.4", "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" + "magic-string": "^1.4.3" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + "msw": "^2.4.9 || ^3.0.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -1943,68 +2246,22 @@ } } }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", - "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "node_modules/@vitest/mocker/node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", "dev": true, + "license": "MIT", "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "@jridgewell/sourcemap-codec": "^1.6.0" } }, - "node_modules/@vitest/runner": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", - "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "node_modules/@vitest/spy": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.4.tgz", + "integrity": "sha512-MZKk/gSkm2q+ICobMxCpH1mA+cCAWm8WV7UhxlqGjq/sJ3AGc1BqFOs5CRVXxH78wQtme9q/NAAgBquj4AcyMw==", "dev": true, - "dependencies": { - "@vitest/utils": "3.2.7", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", - "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", - "dev": true, - "dependencies": { - "@vitest/pretty-format": "3.2.7", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", - "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", - "dev": true, - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", - "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", - "dev": true, - "dependencies": { - "@vitest/pretty-format": "3.2.7", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" - }, + "license": "MIT", "funding": { "url": "https://opencollective.com/vitest" } @@ -2106,6 +2363,7 @@ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" } @@ -2176,17 +2434,11 @@ } }, "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", "dev": true, - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, + "license": "MIT", "engines": { "node": ">=18" } @@ -2207,15 +2459,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "engines": { - "node": ">= 16" - } - }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -2326,21 +2569,23 @@ "node": ">=0.10.0" } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/dijkstrajs": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", @@ -2352,17 +2597,19 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" }, "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -2370,32 +2617,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, "node_modules/escape-string-regexp": { @@ -2553,6 +2800,7 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" } @@ -2890,12 +3138,6 @@ "node": ">=10" } }, - "node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true - }, "node_modules/js-yaml": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", @@ -2974,6 +3216,279 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -3022,12 +3537,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true - }, "node_modules/lru-cache": { "version": "11.5.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", @@ -3109,6 +3618,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -3131,6 +3641,20 @@ "node": ">=0.10.0" } }, + "node_modules/obug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3297,15 +3821,6 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "engines": { - "node": ">= 14.16" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3371,6 +3886,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.20", "picocolors": "^1.1.1", @@ -3506,6 +4022,41 @@ "node": ">=4" } }, + "node_modules/rolldown": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.13.tgz", + "integrity": "sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.153.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.13", + "@rolldown/binding-android-arm64": "1.2.13", + "@rolldown/binding-darwin-arm64": "1.2.13", + "@rolldown/binding-darwin-x64": "1.2.13", + "@rolldown/binding-freebsd-x64": "1.2.13", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.13", + "@rolldown/binding-linux-arm64-gnu": "1.2.13", + "@rolldown/binding-linux-arm64-musl": "1.2.13", + "@rolldown/binding-linux-ppc64-gnu": "1.2.13", + "@rolldown/binding-linux-s390x-gnu": "1.2.13", + "@rolldown/binding-linux-x64-gnu": "1.2.13", + "@rolldown/binding-linux-x64-musl": "1.2.13", + "@rolldown/binding-openharmony-arm64": "1.2.13", + "@rolldown/binding-win32-arm64-msvc": "1.2.13", + "@rolldown/binding-win32-x64-msvc": "1.2.13" + } + }, "node_modules/rollup": { "version": "4.64.6", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.6.tgz", @@ -3601,12 +4152,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true - }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -3627,16 +4172,11 @@ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, + "peer": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true - }, "node_modules/standardwebhooks": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", @@ -3647,10 +4187,11 @@ } }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true, + "license": "MIT" }, "node_modules/string-width": { "version": "4.2.3", @@ -3688,18 +4229,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", - "dev": true, - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -3776,10 +4305,14 @@ } }, "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.2.tgz", + "integrity": "sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } }, "node_modules/tinyexec": { "version": "0.3.2", @@ -3803,33 +4336,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", - "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -4101,17 +4607,18 @@ } }, "node_modules/vite": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.7.tgz", - "integrity": "sha512-AHEzTORMbi7/7aXp7w7KZ5+DCUdSeLsJz0grY8xHpc9DHdYb0vQu6DqJfm2tOw3gDExNgPgnJTXxM6syYFHaCA==", + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz", + "integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "esbuild": "^0.27.0 || ^0.28.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.29", + "rolldown": "~1.2.12", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -4127,9 +4634,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -4142,13 +4650,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { "optional": true }, - "lightningcss": { + "jiti": { + "optional": true + }, + "less": { "optional": true }, "sass": { @@ -4174,87 +4685,73 @@ } } }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/vitest": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", - "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.4.tgz", + "integrity": "sha512-z0WxZRV2Sg48wF0FSgMq3TFG8JS38/Qr96vZVZdX6+Em9raQTvnXQvdTFJe2m3rfyJnEHeHQ/snceIdi8D1pVg==", "dev": true, + "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.7", - "@vitest/mocker": "3.2.7", - "@vitest/pretty-format": "^3.2.7", - "@vitest/runner": "3.2.7", - "@vitest/snapshot": "3.2.7", - "@vitest/spy": "3.2.7", - "@vitest/utils": "3.2.7", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", - "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", - "why-is-node-running": "^2.3.0" + "@vitest/mocker": "5.0.4", + "chai": "^6.3.0", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.4.3", + "obug": "^3.0.0", + "picomatch": "^4.0.7", + "std-env": "^4.3.0", + "tinybench": "^6.2.2", + "tinyexec": "^1.3.1", + "tinyglobby": "^0.2.17", + "why-is-node-running": "3.2.1" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.7", - "@vitest/ui": "3.2.7", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.4", + "@vitest/browser-preview": "5.0.4", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.4", + "@vitest/coverage-v8": "5.0.4", + "@vitest/ui": "5.0.4", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, - "@types/debug": { + "@opentelemetry/api": { "optional": true }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { "optional": true }, "@vitest/ui": { @@ -4265,9 +4762,32 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, + "node_modules/vitest/node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4289,19 +4809,16 @@ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" }, "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", "dev": true, - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, + "license": "MIT", "bin": { "why-is-node-running": "cli.js" }, "engines": { - "node": ">=8" + "node": ">=20.11" } }, "node_modules/word-wrap": { diff --git a/package.json b/package.json index cece521..a508694 100644 --- a/package.json +++ b/package.json @@ -14,16 +14,17 @@ "policies" ], "scripts": { - "build": "tsup src/cli.ts --format esm --clean && node scripts/finalize-build.mjs", + "build": "node scripts/generate-skill.mjs && tsup src/cli.ts --format esm --clean && node scripts/finalize-build.mjs", "lint": "eslint .", "typecheck": "tsc --noEmit", "test": "vitest run", "docs:generate": "npm run build && node scripts/generate-docs.mjs", "test:e2e": "node scripts/e2e-testnet.mjs", "test:package": "node scripts/package-smoke.mjs", - "build:binaries": "node scripts/build-binaries.mjs", + "build:binaries": "node scripts/generate-skill.mjs && node scripts/build-binaries.mjs", "changeset": "changeset", - "version:packages": "changeset version && node scripts/sync-skill-version.mjs" + "version:packages": "changeset version && node scripts/sync-skill-version.mjs && node scripts/generate-skill.mjs", + "test:sandbox": "vitest run --config vitest.integration.config.ts" }, "devDependencies": { "@changesets/cli": "^3.0.3", @@ -37,7 +38,7 @@ "typescript": "^5.9.3", "typescript-eslint": "^8.71.2", "viem": "^2.57.5", - "vitest": "^3.2.7" + "vitest": "^5.0.4" }, "dependencies": { "@bloxwap/hyperliquid": "^0.6.0", @@ -51,5 +52,8 @@ "license": "MIT", "publishConfig": { "access": "public" + }, + "overrides": { + "esbuild": "^0.28.1" } } diff --git a/scripts/build-binaries.mjs b/scripts/build-binaries.mjs index fcc9ac3..1a6b8aa 100644 --- a/scripts/build-binaries.mjs +++ b/scripts/build-binaries.mjs @@ -1,4 +1,19 @@ -import {execFileSync} from 'node:child_process'; -import {mkdirSync} from 'node:fs'; -mkdirSync('binaries',{recursive:true}); -const targets=process.argv.slice(2);for(const target of targets.length?targets:['bun-linux-x64','bun-darwin-arm64']) execFileSync('bun',['build','src/cli.ts','--compile',`--target=${target}`,'--outfile',`binaries/agent-cli-${target.replace('bun-','')}`],{stdio:'inherit'}); +import { execFileSync } from 'node:child_process'; +import { mkdirSync } from 'node:fs'; +mkdirSync('binaries', { recursive: true }); +const targets = process.argv.slice(2); +for (const target of targets.length + ? targets + : ['bun-linux-x64', 'bun-darwin-arm64']) + execFileSync( + 'bun', + [ + 'build', + 'src/cli.ts', + '--compile', + `--target=${target}`, + '--outfile', + `binaries/agent-cli-${target.replace('bun-', '')}`, + ], + { stdio: 'inherit' }, + ); diff --git a/scripts/e2e-relay-hook.mjs b/scripts/e2e-relay-hook.mjs index 91e4791..da1f9f0 100644 --- a/scripts/e2e-relay-hook.mjs +++ b/scripts/e2e-relay-hook.mjs @@ -1,10 +1,27 @@ // Loaded only by the explicit testnet e2e harness, never by the packaged CLI. -const originalFetch=globalThis.fetch; -globalThis.fetch=async(url,init)=>{ - if(String(url).startsWith('https://api.testnets.relay.link/')){ - if(String(url).includes('/quote/')) return Response.json({steps:[{id:'deposit',kind:'transaction',requestId:'mock-testnet-deposit',depositAddress:'0x'+'b'.repeat(40),items:[{status:'incomplete',data:{}}]}],fees:{},details:{currencyOut:{amount:'0',currency:{decimals:8}},expiresAt:Date.now()+60000}}); - if(String(url).includes('/intents/status')) return Response.json({status:'success'}); - throw new Error('Unexpected Relay mock endpoint'); - } - return originalFetch(url,init); +const originalFetch = globalThis.fetch; +globalThis.fetch = async (url, init) => { + if (String(url).startsWith('https://api.testnets.relay.link/')) { + if (String(url).includes('/quote/')) + return Response.json({ + steps: [ + { + id: 'deposit', + kind: 'transaction', + requestId: 'mock-testnet-deposit', + depositAddress: '0x' + 'b'.repeat(40), + items: [{ status: 'incomplete', data: {} }], + }, + ], + fees: {}, + details: { + currencyOut: { amount: '0', currency: { decimals: 8 } }, + expiresAt: Date.now() + 60000, + }, + }); + if (String(url).includes('/intents/status')) + return Response.json({ status: 'success' }); + throw new Error('Unexpected Relay mock endpoint'); + } + return originalFetch(url, init); }; diff --git a/scripts/e2e-testnet.mjs b/scripts/e2e-testnet.mjs index e43a086..b332c55 100644 --- a/scripts/e2e-testnet.mjs +++ b/scripts/e2e-testnet.mjs @@ -1,31 +1,135 @@ -import {execFileSync} from 'node:child_process'; -import {mkdirSync,copyFileSync,existsSync,writeFileSync} from 'node:fs'; -import {resolve,join} from 'node:path'; -import {homedir} from 'node:os'; -import {privateKeyToAccount} from 'viem/accounts'; -if(process.env.BLOXWAP_E2E!=='1'||!process.env.BLOXWAP_TEST_MASTER_KEY||!process.env.BLOXWAP_PRIVY_APP_ID||!process.env.BLOXWAP_PRIVY_APP_SECRET){console.log('SKIP: set BLOXWAP_E2E=1 and dedicated testnet master/Privy credentials');process.exit(0);} -const account=privateKeyToAccount(process.env.BLOXWAP_TEST_MASTER_KEY); -const config=join(homedir(),'.bloxwap','e2e-config.json'); -if(existsSync(config)) throw new Error('Refusing to replace an existing e2e config; use a clean CI account'); -const hook=resolve('scripts/e2e-relay-hook.mjs');const env={...process.env,NODE_OPTIONS:`${process.env.NODE_OPTIONS??''} --import=${hook}`}; -const run=(name,input={})=>{const raw=execFileSync(process.execPath,['dist/cli.js',name,'--config',config,'--network','testnet','--json',JSON.stringify(input)],{input:'',encoding:'utf8',env});const result=JSON.parse(raw);if(!result.ok) throw new Error(JSON.stringify(result));return result.data;}; -const sign=payload=>account.signTypedData(payload.typedData); -const info=async(type,params={})=>(await fetch('https://api.hyperliquid-testnet.xyz/info',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({type,...params})})).json(); -try{ - run('config',{args:['set','privyAppId',process.env.BLOXWAP_PRIVY_APP_ID]}); - const init=run('init',{masterAddress:account.address,qr:false});if(!init.approved) run('init',{signature:await sign(init.approvals.agent),qr:false}); - const state=await info('clearinghouseState',{user:account.address});if(Number(state.withdrawable)<30) throw new Error('Dedicated testnet account needs at least 30 test USDC; no real funding is performed'); - run('fund',{amount:'10',qr:false});const status=run('status');if(status.deposits.some(d=>d.state!=='settled')) throw new Error('Mocked Relay funding did not settle'); - run('run',{detach:true}); - const meta=await info('meta');const market=meta.universe.find(m=>m.name==='BTC');const mids=await info('allMids');const mid=Number(mids.BTC);const size=(Math.ceil(20/mid*10**market.szDecimals)/10**market.szDecimals).toFixed(market.szDecimals);const cloid='0x'+Date.now().toString(16).padStart(32,'0'); - const order=run('order',{coin:'BTC',side:'buy',size,price:String(Math.floor(mid*0.7)),cloid,intentId:'nightly-lifecycle'}); - const oid=order.result.response.data.statuses[0].resting?.oid;if(!oid) throw new Error('Expected resting test order');run('modify',{oid,price:String(Math.floor(mid*0.69))});run('cancel',{oid});run('events',{since:0}); - const rotation=run('rotate');const swapped=run('rotate',{signature:await sign(rotation.approval)});run('rotate',{revokeSignature:await sign(swapped.revokeApproval)}); - const revoke=run('revoke',{cancelOrders:true});run('revoke',{signature:await sign(revoke.approval)});run('audit',{intent:'nightly-lifecycle'}); - console.log('PASS: testnet init/fund/run/order/modify/cancel/events/rotate/revoke'); -}finally{ - mkdirSync('artifacts',{recursive:true});for(const file of ['events.jsonl','audit.jsonl']) if(existsSync(join(homedir(),'.bloxwap',file))) copyFileSync(join(homedir(),'.bloxwap',file),join('artifacts',file)); - // A failed run records its stage via journals; credentials are never exported. - writeFileSync('artifacts/network.txt','testnet\n'); - const {createConnection}=await import('node:net');await new Promise(resolveStop=>{const socket=createConnection(join(homedir(),'.bloxwap','run.sock'));socket.on('connect',()=>socket.end(JSON.stringify({jsonrpc:'2.0',id:1,method:'shutdown',params:{}})+'\n'));socket.on('error',resolveStop);socket.on('close',resolveStop);socket.setTimeout(5000,()=>{socket.destroy();resolveStop();});}); +import { execFileSync } from 'node:child_process'; +import { mkdirSync, copyFileSync, existsSync, writeFileSync } from 'node:fs'; +import { resolve, join } from 'node:path'; +import { homedir } from 'node:os'; +import { privateKeyToAccount } from 'viem/accounts'; +if ( + process.env.BLOXWAP_E2E !== '1' || + !process.env.BLOXWAP_TEST_MASTER_KEY || + !process.env.BLOXWAP_PRIVY_APP_ID || + !process.env.BLOXWAP_PRIVY_APP_SECRET +) { + console.log( + 'SKIP: set BLOXWAP_E2E=1 and dedicated testnet master/Privy credentials', + ); + process.exit(0); +} +if (!['true', '1'].includes(process.env.CI ?? '')) + throw new Error( + 'Master-key e2e signing is restricted to a dedicated CI account', + ); +const account = privateKeyToAccount(process.env.BLOXWAP_TEST_MASTER_KEY); +const config = join(homedir(), '.bloxwap', 'e2e-config.json'); +if (existsSync(config)) + throw new Error( + 'Refusing to replace an existing e2e config; use a clean CI account', + ); +const hook = resolve('scripts/e2e-relay-hook.mjs'); +const env = { + ...process.env, + NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ''} --import=${hook}`, +}; +const run = (name, input = {}) => { + const raw = execFileSync( + process.execPath, + [ + 'dist/cli.js', + name, + '--config', + config, + '--network', + 'testnet', + '--json', + JSON.stringify(input), + ], + { input: '', encoding: 'utf8', env }, + ); + const result = JSON.parse(raw); + if (!result.ok) throw new Error(JSON.stringify(result)); + return result.data; +}; +const sign = (payload) => account.signTypedData(payload.typedData); +const info = async (type, params = {}) => + ( + await fetch('https://api.hyperliquid-testnet.xyz/info', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ type, ...params }), + }) + ).json(); +try { + run('config', { + args: ['set', 'privyAppId', process.env.BLOXWAP_PRIVY_APP_ID], + }); + const init = run('init', { masterAddress: account.address, qr: false }); + if (!init.approved) + run('init', { signature: await sign(init.approvals.agent), qr: false }); + const state = await info('clearinghouseState', { user: account.address }); + if (Number(state.withdrawable) < 30) + throw new Error( + 'Dedicated testnet account needs at least 30 test USDC; no real funding is performed', + ); + run('fund', { amount: '10', qr: false }); + const status = run('status'); + if (status.deposits.some((d) => d.state !== 'settled')) + throw new Error('Mocked Relay funding did not settle'); + run('run', { detach: true }); + const meta = await info('meta'); + const market = meta.universe.find((m) => m.name === 'BTC'); + const mids = await info('allMids'); + const mid = Number(mids.BTC); + const size = ( + Math.ceil((20 / mid) * 10 ** market.szDecimals) / + 10 ** market.szDecimals + ).toFixed(market.szDecimals); + const cloid = '0x' + Date.now().toString(16).padStart(32, '0'); + const order = run('order', { + coin: 'BTC', + side: 'buy', + size, + price: String(Math.floor(mid * 0.7)), + cloid, + intentId: 'nightly-lifecycle', + }); + const oid = order.result.response.data.statuses[0].resting?.oid; + if (!oid) throw new Error('Expected resting test order'); + run('modify', { oid, price: String(Math.floor(mid * 0.69)) }); + run('cancel', { oid }); + run('events', { since: 0 }); + const rotation = run('rotate'); + const swapped = run('rotate', { signature: await sign(rotation.approval) }); + run('rotate', { revokeSignature: await sign(swapped.revokeApproval) }); + const revoke = run('revoke', { cancelOrders: true }); + run('revoke', { signature: await sign(revoke.approval) }); + run('audit', { intent: 'nightly-lifecycle' }); + console.log( + 'PASS: testnet init/fund/run/order/modify/cancel/events/rotate/revoke', + ); +} finally { + mkdirSync('artifacts', { recursive: true }); + for (const file of ['events.jsonl', 'audit.jsonl']) + if (existsSync(join(homedir(), '.bloxwap', file))) + copyFileSync(join(homedir(), '.bloxwap', file), join('artifacts', file)); + // A failed run records its stage via journals; credentials are never exported. + writeFileSync('artifacts/network.txt', 'testnet\n'); + const { createConnection } = await import('node:net'); + await new Promise((resolveStop) => { + const socket = createConnection(join(homedir(), '.bloxwap', 'run.sock')); + socket.on('connect', () => + socket.end( + JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'shutdown', + params: {}, + }) + '\n', + ), + ); + socket.on('error', resolveStop); + socket.on('close', resolveStop); + socket.setTimeout(5000, () => { + socket.destroy(); + resolveStop(); + }); + }); } diff --git a/scripts/finalize-build.mjs b/scripts/finalize-build.mjs index 3e284f6..388c82f 100644 --- a/scripts/finalize-build.mjs +++ b/scripts/finalize-build.mjs @@ -1,2 +1,2 @@ -import {chmodSync} from 'node:fs'; -chmodSync('dist/cli.js',0o755); +import { chmodSync } from 'node:fs'; +chmodSync('dist/cli.js', 0o755); diff --git a/scripts/generate-docs.mjs b/scripts/generate-docs.mjs index 503cb53..1e1c339 100644 --- a/scripts/generate-docs.mjs +++ b/scripts/generate-docs.mjs @@ -1,10 +1,27 @@ -import {execFileSync} from 'node:child_process'; -import {writeFileSync,readFileSync,mkdirSync,readdirSync} from 'node:fs'; -import {marked} from 'marked'; -const invoke=(args)=>JSON.parse(execFileSync(process.execPath,['dist/cli.js',...args],{input:'',encoding:'utf8'})).data; -const commands=invoke(['--help']).commands; -let reference='# Command reference\n\nGenerated from `agent-cli schema `. Do not edit by hand.\n\nGlobal flags: `--config PATH`, `--network mainnet|testnet`, `--pretty`, `--json OBJECT`.\n\n'; -for(const command of commands) reference+=`## ${command.name}\n\n${command.description}\n\n\`\`\`json\n${JSON.stringify(invoke(['schema',command.name]),null,2)}\n\`\`\`\n\n`; -writeFileSync('docs/commands.md',reference); -mkdirSync('site',{recursive:true}); -for(const name of readdirSync('docs').filter(name=>name.endsWith('.md'))){const content=marked(readFileSync(`docs/${name}`,'utf8')).replace(/href="([^"#]+)\.md/g,'href="$1.html');writeFileSync(`site/${name.replace('.md','.html')}`,`agent-cli${content}`);} +import { execFileSync } from 'node:child_process'; +import { writeFileSync, readFileSync, mkdirSync, readdirSync } from 'node:fs'; +import { marked } from 'marked'; +const invoke = (args) => + JSON.parse( + execFileSync(process.execPath, ['dist/cli.js', ...args], { + input: '', + encoding: 'utf8', + }), + ).data; +const commands = invoke(['--help']).commands; +let reference = + '# Command reference\n\nGenerated from `agent-cli schema `. Do not edit by hand.\n\nGlobal flags: `--config PATH`, `--network mainnet|testnet`, `--pretty`, `--json OBJECT`.\n\n'; +for (const command of commands) + reference += `## ${command.name}\n\n${command.description}\n\n\`\`\`json\n${JSON.stringify(invoke(['schema', command.name]), null, 2)}\n\`\`\`\n\n`; +writeFileSync('docs/commands.md', reference); +mkdirSync('site', { recursive: true }); +for (const name of readdirSync('docs').filter((name) => name.endsWith('.md'))) { + const content = marked(readFileSync(`docs/${name}`, 'utf8')).replace( + /href="([^"#]+)\.md/g, + 'href="$1.html', + ); + writeFileSync( + `site/${name.replace('.md', '.html')}`, + `agent-cli${content}`, + ); +} diff --git a/scripts/generate-skill.mjs b/scripts/generate-skill.mjs new file mode 100644 index 0000000..da51c99 --- /dev/null +++ b/scripts/generate-skill.mjs @@ -0,0 +1,9 @@ +import { readFileSync, writeFileSync } from 'node:fs'; +const { version } = JSON.parse(readFileSync('package.json', 'utf8')); +const content = readFileSync('SKILL.md', 'utf8'); +if (!content.includes(` version: "${version}"`)) + throw new Error('Skill version differs from CLI'); +writeFileSync( + 'src/core/skill-content.ts', + `// Generated from SKILL.md and package.json.\nexport const cliVersion=${JSON.stringify(version)};\nexport const skillContent=${JSON.stringify(content)};\n`, +); diff --git a/scripts/package-smoke.mjs b/scripts/package-smoke.mjs index 916d841..9b21ffe 100644 --- a/scripts/package-smoke.mjs +++ b/scripts/package-smoke.mjs @@ -1,15 +1,53 @@ -import {execFileSync} from 'node:child_process'; -import {mkdtempSync,rmSync} from 'node:fs'; -import {tmpdir} from 'node:os'; -import {join,resolve} from 'node:path'; -const directory=mkdtempSync(join(tmpdir(),'bloxwap-package-')); -try{ - const packed=JSON.parse(execFileSync('npm',['pack','--json','--pack-destination',directory],{encoding:'utf8'}))[0]; - if(!packed.files.some(file=>file.path==='SKILL.md')) throw new Error('Package missing skill'); - execFileSync('npm',['install','--global','--prefix',join(directory,'global'),join(directory,packed.filename),'--ignore-scripts'],{stdio:'pipe'}); - const bin=process.platform==='win32'?join(directory,'global','agent-cli.cmd'):join(directory,'global','bin','agent-cli'); - const help=JSON.parse(execFileSync(bin,['--help'],{input:'',encoding:'utf8'}));if(!help.ok||!help.data.commands.some(command=>command.name==='order')) throw new Error('Global CLI smoke failed'); - const schema=JSON.parse(execFileSync(bin,['schema','order'],{input:'',encoding:'utf8'}));if(!schema.ok) throw new Error('Schema smoke failed'); - if(process.env.BLOXWAP_SMOKE_BUN==='1'){const bun=JSON.parse(execFileSync('bun',[resolve('dist/cli.js'),'--help'],{input:'',encoding:'utf8'}));if(!bun.ok) throw new Error('Bun smoke failed');} - console.log('PASS: packed global install, CLI/schema and optional Bun smoke'); -}finally{rmSync(directory,{recursive:true,force:true});} +import { execFileSync } from 'node:child_process'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +const directory = mkdtempSync(join(tmpdir(), 'bloxwap-package-')); +try { + const packed = JSON.parse( + execFileSync('npm', ['pack', '--json', '--pack-destination', directory], { + encoding: 'utf8', + }), + )[0]; + if (!packed.files.some((file) => file.path === 'SKILL.md')) + throw new Error('Package missing skill'); + execFileSync( + 'npm', + [ + 'install', + '--global', + '--prefix', + join(directory, 'global'), + join(directory, packed.filename), + ], + { stdio: 'pipe' }, + ); + const bin = + process.platform === 'win32' + ? join(directory, 'global', 'agent-cli.cmd') + : join(directory, 'global', 'bin', 'agent-cli'); + const help = JSON.parse( + execFileSync(bin, ['--help'], { input: '', encoding: 'utf8' }), + ); + if ( + !help.ok || + !help.data.commands.some((command) => command.name === 'order') + ) + throw new Error('Global CLI smoke failed'); + const schema = JSON.parse( + execFileSync(bin, ['schema', 'order'], { input: '', encoding: 'utf8' }), + ); + if (!schema.ok) throw new Error('Schema smoke failed'); + if (process.env.BLOXWAP_SMOKE_BUN === '1') { + const bun = JSON.parse( + execFileSync('bun', [resolve('dist/cli.js'), '--help'], { + input: '', + encoding: 'utf8', + }), + ); + if (!bun.ok) throw new Error('Bun smoke failed'); + } + console.log('PASS: packed global install, CLI/schema and optional Bun smoke'); +} finally { + rmSync(directory, { recursive: true, force: true }); +} diff --git a/scripts/sync-skill-version.mjs b/scripts/sync-skill-version.mjs index 14c96e6..6442d96 100644 --- a/scripts/sync-skill-version.mjs +++ b/scripts/sync-skill-version.mjs @@ -1,4 +1,7 @@ -import {readFileSync,writeFileSync} from 'node:fs'; -const {version}=JSON.parse(readFileSync('package.json','utf8')); -const skill=readFileSync('SKILL.md','utf8'); -writeFileSync('SKILL.md',skill.replace(/(metadata:\n version: )"[^"]+"/,`$1"${version}"`)); +import { readFileSync, writeFileSync } from 'node:fs'; +const { version } = JSON.parse(readFileSync('package.json', 'utf8')); +const skill = readFileSync('SKILL.md', 'utf8'); +writeFileSync( + 'SKILL.md', + skill.replace(/(metadata:\n version: )"[^"]+"/, `$1"${version}"`), +); diff --git a/src/cli.ts b/src/cli.ts index f9477e0..4f6ee99 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { dispatch,setRouting } from './core/router.js'; +import { dispatch, setRouting } from './core/router.js'; import './commands/config.js'; import './commands/init.js'; import './commands/fund.js'; @@ -15,10 +15,28 @@ import './commands/logs.js'; import './commands/rotate.js'; import './commands/revoke.js'; import './commands/audit.js'; -import {routeToRunner} from './runner/rpc.js'; +import './commands/skill.js'; +import './commands/market.js'; +import { routeToRunner } from './runner/rpc.js'; setRouting(routeToRunner); -let stdin=''; -if (!process.stdin.isTTY) for await (const chunk of process.stdin) stdin += String(chunk); -const result=await dispatch(process.argv.slice(2),stdin); -if(!result.context.streaming) process.stdout.write(JSON.stringify(result.body,null,result.context.pretty ? 2 : undefined)+'\n'); -process.exitCode=result.exitCode; +import { AgentCliError, normalizeError } from './errors/index.js'; +try { + let stdin = ''; + if (!process.stdin.isTTY) + for await (const chunk of process.stdin) { + stdin += String(chunk); + if (Buffer.byteLength(stdin) > 1_048_576) + throw new AgentCliError('CONFIG_INVALID', 'JSON input exceeds 1 MiB'); + } + const result = await dispatch(process.argv.slice(2), stdin); + if (!result.context.streaming || !result.body.ok) + process.stdout.write( + JSON.stringify(result.body, null, result.context.pretty ? 2 : undefined) + + '\n', + ); + process.exitCode = result.exitCode; +} catch (error) { + const normalized = normalizeError(error); + process.stdout.write(JSON.stringify(normalized.toJSON()) + '\n'); + process.exitCode = normalized.exitCode; +} diff --git a/src/commands/audit.ts b/src/commands/audit.ts index 8f867a3..1b2f693 100644 --- a/src/commands/audit.ts +++ b/src/commands/audit.ts @@ -1,4 +1,21 @@ -import {z} from 'zod'; -import {register} from '../core/router.js'; -import {readAudit,verifyAudit} from '../core/audit.js'; -register('audit',{description:'Verify and query the hash-chained trading audit',schema:z.object({intent:z.string().optional(),intentId:z.string().optional(),cloid:z.string().optional()}),execute:async input=>{const entries=await readAudit();verifyAudit(entries);return entries.filter(entry=>(!input.intent&&!input.intentId||entry.intentId===(input.intentId??input.intent))&&(!input.cloid||entry.cloid===input.cloid));}}); +import { z } from 'zod'; +import { register } from '../core/router.js'; +import { readAudit, verifyAudit } from '../core/audit.js'; +register('audit', { + description: 'Verify and query the hash-chained trading audit', + schema: z.object({ + intent: z.string().optional(), + intentId: z.string().optional(), + cloid: z.string().optional(), + }), + execute: async (input) => { + const entries = await readAudit(); + verifyAudit(entries); + return entries.filter( + (entry) => + ((!input.intent && !input.intentId) || + entry.intentId === (input.intentId ?? input.intent)) && + (!input.cloid || entry.cloid === input.cloid), + ); + }, +}); diff --git a/src/commands/cancel.ts b/src/commands/cancel.ts index f901688..49e7e17 100644 --- a/src/commands/cancel.ts +++ b/src/commands/cancel.ts @@ -1,18 +1,69 @@ -import {z} from 'zod'; -import {register} from '../core/router.js'; -import {commandConfig} from './read.js'; -import type {Config} from '../core/config.js'; -import {assertCanTrade,tradingServices,exchangeAction,type TradingServices} from '../core/trading.js'; -import {AgentCliError} from '../errors/index.js'; -export const cancelSchema=z.object({oid:z.number().int().positive().optional(),oids:z.array(z.number().int().positive()).optional(),cloid:z.string().optional(),cloids:z.array(z.string()).optional(),coin:z.string().optional(),all:z.boolean().default(false),intent:z.string().optional()}); -export async function cancelOrders(config:Config,input:z.infer,services?:TradingServices){ - await assertCanTrade(config);const clients=services??await tradingServices(config);const orders=await clients.orders(); - const oids=input.oids??(input.oid?[input.oid]:[]),cloids=input.cloids??(input.cloid?[input.cloid]:[]); - if(!input.all&&!input.coin&&!oids.length&&!cloids.length) throw new AgentCliError('CONFIG_INVALID','Select oid, cloid, coin or all'); - for(const oid of oids) if(!orders.some(o=>o.oid===oid)) throw new AgentCliError('ORDER_NOT_FOUND',`Order ${oid} absent`); - for(const cloid of cloids) if(!orders.some(o=>o.cloid===cloid)) throw new AgentCliError('ORDER_NOT_FOUND',`Order ${cloid} absent`); - const selected=orders.filter(o=>(!input.coin||o.coin===input.coin)&&(input.all||input.coin&&!oids.length&&!cloids.length||oids.includes(o.oid)||!!o.cloid&&cloids.includes(o.cloid))); - const cancels=await Promise.all(selected.map(async o=>({a:(await clients.market(o.coin)).asset,o:o.oid}))); - if(!cancels.length) return {cancelled:[],result:null};return {cancelled:selected.map(o=>String(o.oid)),result:await exchangeAction('cancel',()=>clients.exchange.cancel({cancels}))}; +import { z } from 'zod'; +import { register } from '../core/router.js'; +import { commandConfig } from './read.js'; +import type { Config } from '../core/config.js'; +import { + assertCanTrade, + tradingServices, + exchangeAction, + type TradingServices, +} from '../core/trading.js'; +import { AgentCliError } from '../errors/index.js'; +export const cancelSchema = z.object({ + oid: z.number().int().positive().optional(), + oids: z.array(z.number().int().positive()).optional(), + cloid: z.string().optional(), + cloids: z.array(z.string()).optional(), + coin: z.string().optional(), + all: z.boolean().default(false), + intent: z.string().optional(), +}); +export async function cancelOrders( + config: Config, + input: z.infer, + services?: TradingServices, +) { + await assertCanTrade(config); + const clients = services ?? (await tradingServices(config)); + const orders = await clients.orders(); + const oids = input.oids ?? (input.oid ? [input.oid] : []), + cloids = input.cloids ?? (input.cloid ? [input.cloid] : []); + if (!input.all && !input.coin && !oids.length && !cloids.length) + throw new AgentCliError('CONFIG_INVALID', 'Select oid, cloid, coin or all'); + for (const oid of oids) + if (!orders.some((o) => o.oid === oid)) + throw new AgentCliError('ORDER_NOT_FOUND', `Order ${oid} absent`); + for (const cloid of cloids) + if (!orders.some((o) => o.cloid === cloid)) + throw new AgentCliError('ORDER_NOT_FOUND', `Order ${cloid} absent`); + const selected = orders.filter( + (o) => + (!input.coin || o.coin === input.coin) && + (input.all || + (input.coin && !oids.length && !cloids.length) || + oids.includes(o.oid) || + (!!o.cloid && cloids.includes(o.cloid))), + ); + const cancels = await Promise.all( + selected.map(async (o) => ({ + a: (await clients.market(o.coin)).asset, + o: o.oid, + })), + ); + if (!cancels.length) return { cancelled: [], result: null }; + return { + cancelled: selected.map((o) => String(o.oid)), + result: await exchangeAction('cancel', () => + clients.exchange.cancel({ cancels }), + ), + }; } -register('cancel',{description:'Cancel by oid/cloid, coin or all',schema:cancelSchema,execute:async(input,context)=>cancelOrders(await commandConfig(context),input as z.infer)}); +register('cancel', { + description: 'Cancel by oid/cloid, coin or all', + schema: cancelSchema, + execute: async (input, context) => + cancelOrders( + await commandConfig(context), + input as z.infer, + ), +}); diff --git a/src/commands/config.ts b/src/commands/config.ts index 069fc19..dbaa960 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -2,14 +2,49 @@ import { z } from 'zod'; import { register } from '../core/router.js'; import { loadConfig, saveConfig, migrate } from '../core/config.js'; import { AgentCliError } from '../errors/index.js'; -register('config',{description:'Get, set or validate local configuration',schema:z.object({args:z.array(z.string()).default([])}),execute:async({args},context)=>{ - const [action='get',key,value]=args as string[]; const config=await loadConfig(context.configPath,{}); - if(action==='validate') return {valid:true,version:config.version}; - if(action==='get') return key ? key.split('.').reduce((v,k)=>(v as Record)?.[k],config) ?? null : config; - if(action!=='set' || !key || value===undefined) throw new AgentCliError('CONFIG_INVALID','Use config set key value'); - let parsed:unknown; try {parsed=JSON.parse(value);} catch {parsed=value;} - const updated=structuredClone(config) as Record; const parts=key.split('.'); let cursor=updated; - for(const part of parts.slice(0,-1)) {if (!Object.hasOwn(cursor,part) || !cursor[part] || typeof cursor[part]!=='object') throw new AgentCliError('CONFIG_INVALID','Unknown config key');cursor=cursor[part] as Record;} - if(['__proto__','constructor','prototype'].some(p=>parts.includes(p))) throw new AgentCliError('CONFIG_INVALID','Invalid key'); - cursor[parts.at(-1)!]=parsed; const validated=migrate(updated);await saveConfig(validated,context.configPath);return validated; -}}); +register('config', { + description: 'Get, set or validate local configuration', + schema: z.object({ args: z.array(z.string()).default([]) }), + execute: async ({ args }, context) => { + const [action = 'get', key, value] = args as string[]; + const config = await loadConfig(context.configPath, {}); + if (action === 'validate') return { valid: true, version: config.version }; + if (action === 'get') + return key + ? (key + .split('.') + .reduce( + (v, k) => (v as Record)?.[k], + config, + ) ?? null) + : config; + if (action !== 'set' || !key || value === undefined) + throw new AgentCliError('CONFIG_INVALID', 'Use config set key value'); + let parsed: unknown; + try { + parsed = JSON.parse(value); + } catch { + parsed = value; + } + const updated = structuredClone(config) as Record; + const parts = key.split('.'); + let cursor = updated; + for (const part of parts.slice(0, -1)) { + if ( + !Object.hasOwn(cursor, part) || + !cursor[part] || + typeof cursor[part] !== 'object' + ) + throw new AgentCliError('CONFIG_INVALID', 'Unknown config key'); + cursor = cursor[part] as Record; + } + if ( + ['__proto__', 'constructor', 'prototype'].some((p) => parts.includes(p)) + ) + throw new AgentCliError('CONFIG_INVALID', 'Invalid key'); + cursor[parts.at(-1)!] = parsed; + const validated = migrate(updated); + await saveConfig(validated, context.configPath); + return validated; + }, +}); diff --git a/src/commands/deposit.ts b/src/commands/deposit.ts index b4422a9..4cb227b 100644 --- a/src/commands/deposit.ts +++ b/src/commands/deposit.ts @@ -1,9 +1,29 @@ -import {z} from 'zod'; -import {register} from '../core/router.js'; -import {readDeposits} from '../funding/store.js'; -import {signatureSteps,submitDepositSignature} from '../funding/signatures.js'; -import {AgentCliError} from '../errors/index.js'; -register('deposit',{description:'Resume deposit steps or submit a master-wallet signature',schema:z.object({depositId:z.string(),stepId:z.string().optional(),signature:z.string().optional()}),execute:async(input)=>{ - if(input.signature&&input.stepId) return submitDepositSignature(input.depositId as string,input.stepId as string,input.signature as string); - const deposit=(await readDeposits()).find(d=>d.id===input.depositId);if(!deposit) throw new AgentCliError('CONFIG_INVALID','Unknown deposit');return {depositId:deposit.id,steps:signatureSteps(deposit)}; -}}); +import { z } from 'zod'; +import { register } from '../core/router.js'; +import { readDeposits } from '../funding/store.js'; +import { + signatureSteps, + submitDepositSignature, +} from '../funding/signatures.js'; +import { AgentCliError } from '../errors/index.js'; +register('deposit', { + description: 'Resume deposit steps or submit a master-wallet signature', + schema: z.object({ + depositId: z.string(), + stepId: z.string().optional(), + signature: z.string().optional(), + }), + execute: async (input) => { + if (input.signature && input.stepId) + return submitDepositSignature( + input.depositId as string, + input.stepId as string, + input.signature as string, + ); + const deposit = (await readDeposits()).find( + (d) => d.id === input.depositId, + ); + if (!deposit) throw new AgentCliError('CONFIG_INVALID', 'Unknown deposit'); + return { depositId: deposit.id, steps: signatureSteps(deposit) }; + }, +}); diff --git a/src/commands/events.ts b/src/commands/events.ts index afcebdb..7983178 100644 --- a/src/commands/events.ts +++ b/src/commands/events.ts @@ -1,8 +1,41 @@ -import {z} from 'zod'; -import {register} from '../core/router.js'; -import {readEvents,followEvents,eventTypes,type EventType} from '../runner/events.js'; -register('events',{description:'Replay or follow JSON-line runner events',schema:z.object({follow:z.boolean().default(false),since:z.number().int().nonnegative().default(0),types:z.array(z.enum(eventTypes)).default([])}),execute:async(input,context)=>{ - if(!input.follow) return readEvents(undefined,input.since as number,input.types as EventType[]); - context.streaming=true;const abort=new AbortController();const stop=()=>abort.abort();process.once('SIGINT',stop);process.once('SIGTERM',stop); - try{await followEvents({since:input.since as number,types:input.types as EventType[],signal:abort.signal,write:event=>process.stdout.write(JSON.stringify(event)+'\n')});}finally{process.off('SIGINT',stop);process.off('SIGTERM',stop);}return null; -}}); +import { z } from 'zod'; +import { register } from '../core/router.js'; +import { + readEvents, + followEvents, + eventTypes, + type EventType, +} from '../runner/events.js'; +register('events', { + description: 'Replay or follow JSON-line runner events', + schema: z.object({ + follow: z.boolean().default(false), + since: z.number().int().nonnegative().default(0), + types: z.array(z.enum(eventTypes)).default([]), + }), + execute: async (input, context) => { + if (!input.follow) + return readEvents( + undefined, + input.since as number, + input.types as EventType[], + ); + context.streaming = true; + const abort = new AbortController(); + const stop = () => abort.abort(); + process.once('SIGINT', stop); + process.once('SIGTERM', stop); + try { + await followEvents({ + since: input.since as number, + types: input.types as EventType[], + signal: abort.signal, + write: (event) => process.stdout.write(JSON.stringify(event) + '\n'), + }); + } finally { + process.off('SIGINT', stop); + process.off('SIGTERM', stop); + } + return null; + }, +}); diff --git a/src/commands/fund.ts b/src/commands/fund.ts index 3ea92cc..e84ecca 100644 --- a/src/commands/fund.ts +++ b/src/commands/fund.ts @@ -1,27 +1,127 @@ -import {z} from 'zod'; -import {register} from '../core/router.js'; -import {loadConfig,type Config} from '../core/config.js'; -import {hlInfo} from '../core/http.js'; -import {RelayClient,PERPS_USDC,parseQuote} from '../funding/relay.js'; -import {parseUsdc,formatUsdc} from '../funding/amounts.js'; -import {renderQr} from '../funding/qr.js'; -import {readDeposits,writeDeposits} from '../funding/store.js'; -import {signatureSteps} from '../funding/signatures.js'; -import {builderRate} from '../core/builder.js'; -import {AgentCliError} from '../errors/index.js'; -export const fundSchema=z.object({originChain:z.number().int().positive().default(42161),originCurrency:z.string().default('0xaf88d065e77c8cC2239327C5EDb3A432268e5831'),originDecimals:z.number().int().min(0).max(30).default(6),amount:z.string().default('10'),qr:z.boolean().default(true),qrPath:z.string().optional()}); -export async function fund(config:Config,input:z.infer,dependencies:{relay?:RelayClient;path?:string;baseline?:()=>Promise;qr?:typeof renderQr}={}){ - if(!config.masterAddress) throw new AgentCliError('AGENT_NOT_APPROVED','Initialize master address'); - const amount=parseUsdc(input.amount,input.originDecimals);if(amount<=0n) throw new AgentCliError('INVALID_SIZE','Positive funding amount required'); - const route=JSON.stringify([config.network,config.masterAddress,input.originChain,input.originCurrency,amount.toString()]); - const deposits=await readDeposits(dependencies.path);let deposit=deposits.find(d=>d.route===route&&d.expiresAt>Date.now()&&d.state==='awaiting_funds'); - if(!deposit){ - const quote=await (dependencies.relay??new RelayClient(config.network==='testnet'?'https://api.testnets.relay.link':'https://api.relay.link')).quote({user:config.masterAddress,recipient:config.masterAddress,originChainId:input.originChain,originCurrency:input.originCurrency,amount:amount.toString(),destinationChainId:1337,destinationCurrency:PERPS_USDC,useDepositAddress:true,tradeType:'EXACT_INPUT'}); - const parsed=parseQuote(quote);if(amount(config.network,'clearinghouseState',{user:config.masterAddress})).marginSummary.accountValue; - deposit={id:parsed.depositId,network:config.network,recipient:config.masterAddress,route,quote,expiresAt:parsed.expiresAt,state:'awaiting_funds',baseline,createdAt:Date.now(),completedSteps:[]};deposits.push(deposit);await writeDeposits(deposits,dependencies.path); - } - const parsed=parseQuote(deposit.quote);const received=formatUsdc(BigInt(parsed.expectedReceived),parsed.receivedDecimals); - return {...parsed,expectedReceived:parseUsdc(received,8).toString(),expectedReceivedHuman:received,originChain:input.originChain,originCurrency:input.originCurrency,minDeposit:formatUsdc(BigInt(parsed.minAmount),input.originDecimals),builderFeeRate:builderRate(config),state:deposit.state,signatureSteps:signatureSteps(deposit),...await (dependencies.qr??renderQr)(parsed.depositAddress,{enabled:input.qr,path:input.qrPath})}; +import { collateralBalance } from '../core/account.js'; +import { z } from 'zod'; +import { register } from '../core/router.js'; +import { loadConfig, type Config } from '../core/config.js'; +import { hlInfo } from '../core/http.js'; +import { RelayClient, PERPS_USDC, parseQuote } from '../funding/relay.js'; +import { parseUsdc, formatUsdc } from '../funding/amounts.js'; +import { renderQr } from '../funding/qr.js'; +import { readDeposits, writeDeposits } from '../funding/store.js'; +import { signatureSteps } from '../funding/signatures.js'; +import { builderRate } from '../core/builder.js'; +import { AgentCliError } from '../errors/index.js'; +export const fundSchema = z.object({ + originChain: z.number().int().positive().default(42161), + originCurrency: z + .string() + .default('0xaf88d065e77c8cC2239327C5EDb3A432268e5831'), + originDecimals: z.number().int().min(0).max(30).default(6), + amount: z.string().default('10'), + qr: z.boolean().default(true), + qrPath: z.string().optional(), +}); +export async function fund( + config: Config, + input: z.infer, + dependencies: { + relay?: RelayClient; + path?: string; + baseline?: () => Promise; + qr?: typeof renderQr; + } = {}, +) { + if (!config.masterAddress) + throw new AgentCliError('AGENT_NOT_APPROVED', 'Initialize master address'); + const amount = parseUsdc(input.amount, input.originDecimals); + if (amount <= 0n) + throw new AgentCliError('INVALID_SIZE', 'Positive funding amount required'); + const route = JSON.stringify([ + config.network, + config.masterAddress, + input.originChain, + input.originCurrency, + amount.toString(), + ]); + const deposits = await readDeposits(dependencies.path); + let deposit = deposits.find( + (d) => + d.route === route && + d.expiresAt > Date.now() && + d.state === 'awaiting_funds', + ); + if (!deposit) { + const quote = await ( + dependencies.relay ?? + new RelayClient( + config.network === 'testnet' + ? 'https://api.testnets.relay.link' + : 'https://api.relay.link', + ) + ).quote({ + user: config.masterAddress, + recipient: config.masterAddress, + originChainId: input.originChain, + originCurrency: input.originCurrency, + amount: amount.toString(), + destinationChainId: 1337, + destinationCurrency: PERPS_USDC, + useDepositAddress: true, + tradeType: 'EXACT_INPUT', + }); + const parsed = parseQuote(quote); + if (amount < BigInt(parsed.minAmount)) + throw new AgentCliError('INVALID_SIZE', 'Below minimum deposit', { + minimum: parsed.minAmount, + }); + const baseline = dependencies.baseline + ? await dependencies.baseline() + : collateralBalance( + await hlInfo(config.network, 'clearinghouseState', { + user: config.masterAddress, + }), + ); + deposit = { + id: parsed.depositId, + network: config.network, + recipient: config.masterAddress, + route, + quote, + expiresAt: parsed.expiresAt, + state: 'awaiting_funds', + baseline, + createdAt: Date.now(), + completedSteps: [], + }; + deposits.push(deposit); + await writeDeposits(deposits, dependencies.path); + } + const parsed = parseQuote(deposit.quote); + const received = formatUsdc( + BigInt(parsed.expectedReceived), + parsed.receivedDecimals, + ); + return { + ...parsed, + expectedReceived: parseUsdc(received, 8).toString(), + expectedReceivedHuman: received, + originChain: input.originChain, + originCurrency: input.originCurrency, + minDeposit: formatUsdc(BigInt(parsed.minAmount), input.originDecimals), + builderFeeRate: builderRate(config), + state: deposit.state, + signatureSteps: signatureSteps(deposit), + ...(await (dependencies.qr ?? renderQr)(parsed.depositAddress, { + enabled: input.qr, + path: input.qrPath, + })), + }; } -register('fund',{description:'Quote funding with address, QR and transparent fees',schema:fundSchema,execute:async(input,context)=>{const config=await loadConfig(context.configPath);if(context.network) config.network=context.network;return fund(config,input as z.infer);}}); +register('fund', { + description: 'Quote funding with address, QR and transparent fees', + schema: fundSchema, + execute: async (input, context) => { + const config = await loadConfig(context.configPath); + if (context.network) config.network = context.network; + return fund(config, input as z.infer); + }, +}); diff --git a/src/commands/init.ts b/src/commands/init.ts index 0788e9f..eddf838 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -1,29 +1,123 @@ -import {z} from 'zod'; -import {renderQr} from '../funding/qr.js'; -import {readFile} from 'node:fs/promises'; -import {join} from 'node:path'; -import {register} from '../core/router.js'; -import {loadConfig,saveConfig,atomicWrite,stateDir} from '../core/config.js'; -import {privyClient,createAgent} from '../wallet/privy.js'; -import {applyPolicy} from '../wallet/policy.js'; -import {approval,submitApproval,rejectMasterKey,type Approval} from '../wallet/approval.js'; -import {hlInfo} from '../core/http.js'; -import {AgentCliError} from '../errors/index.js'; -register('init',{description:'Create agent and emit resumable master-wallet approvals',schema:z.object({masterAddress:z.string().regex(/^0x[0-9a-fA-F]{40}$/).optional(),validUntil:z.number().int().optional(),signature:z.string().optional(),builderSignature:z.string().optional(),qr:z.boolean().default(true)}),execute:async(input,context)=>{ - rejectMasterKey(input);const config=await loadConfig(context.configPath);if(context.network) config.network=context.network; - if(config.approved&&!config.revoked) return config; - config.masterAddress=input.masterAddress as string??config.masterAddress;if(!config.masterAddress) throw new AgentCliError('CONFIG_INVALID','Provide masterAddress'); - const expiry=input.validUntil as number??config.agentExpiry??Date.now()+179*86400000; - if(expiry<=Date.now()||expiry>Date.now()+180*86400000) throw new AgentCliError('CONFIG_INVALID','Expiry must be within 180 days');config.agentExpiry=expiry; - const client=await privyClient(config);const policyId=await applyPolicy(config,client,context.configPath);await createAgent(config,client,context.configPath,policyId); - const path=join(stateDir(),`approval-${config.agentAddress}.json`);let pending:{agent:Approval;builder?:Approval}; - try{pending=JSON.parse(await readFile(path,'utf8'));}catch(error){if((error as NodeJS.ErrnoException).code!=='ENOENT') throw error;pending={agent:approval(config.network,'approveAgent',{agentAddress:config.agentAddress,agentName:`${config.agentName} valid_until ${expiry}`})};if(config.builder.address) pending.builder=approval(config.network,'approveBuilderFee',{builder:config.builder.address,maxFeeRate:`${config.builder.fee/1000}%`},Date.now()+1);await atomicWrite(path,JSON.stringify(pending));} - if(input.signature) await submitApproval(config.network,pending.agent,input.signature as string); - if(input.builderSignature&&pending.builder) await submitApproval(config.network,pending.builder,input.builderSignature as string); - const agents=await hlInfo<{address:string;validUntil:number}[]>(config.network,'extraAgents',{user:config.masterAddress}); - config.approved=agents.some(agent=>agent.address.toLowerCase()===config.agentAddress?.toLowerCase()&&agent.validUntil>=expiry); - if(config.builder.address){const max=await hlInfo(config.network,'maxBuilderFee',{user:config.masterAddress,builder:config.builder.address});config.approved=config.approved&&max>=config.builder.fee;} - config.revoked=false;await saveConfig(config,context.configPath); - const deeplink=`ethereum:${config.masterAddress}?request=${encodeURIComponent(JSON.stringify(pending.agent.typedData))}`; - return {masterAddress:config.masterAddress,agentAddress:config.agentAddress,expiry,policyId,approved:config.approved,requiresSignature:!config.approved,approvals:pending,deeplink,...await renderQr(deeplink,{enabled:input.qr as boolean})}; -}}); +import { z } from 'zod'; +import { renderQr } from '../funding/qr.js'; +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { register } from '../core/router.js'; +import { + loadConfig, + saveConfig, + atomicWrite, + stateDir, +} from '../core/config.js'; +import { privyClient, createAgent } from '../wallet/privy.js'; +import { applyPolicy } from '../wallet/policy.js'; +import { + newApproval, + submitApproval, + rejectMasterKey, + type Approval, +} from '../wallet/approval.js'; +import { hlInfo } from '../core/http.js'; +import { AgentCliError } from '../errors/index.js'; +register('init', { + description: 'Create agent and emit resumable master-wallet approvals', + schema: z.object({ + masterAddress: z + .string() + .regex(/^0x[0-9a-fA-F]{40}$/) + .optional(), + validUntil: z.number().int().optional(), + signature: z.string().optional(), + builderSignature: z.string().optional(), + qr: z.boolean().default(true), + }), + execute: async (input, context) => { + rejectMasterKey(input); + const config = await loadConfig(context.configPath); + if (context.network) config.network = context.network; + if (config.approved && !config.revoked) + return { ...config, expiry: config.agentExpiry }; + config.masterAddress = + (input.masterAddress as string) ?? config.masterAddress; + if (!config.masterAddress) + throw new AgentCliError('CONFIG_INVALID', 'Provide masterAddress'); + const expiry = + (input.validUntil as number) ?? + config.agentExpiry ?? + Date.now() + 179 * 86400000; + if (expiry <= Date.now() || expiry > Date.now() + 180 * 86400000) + throw new AgentCliError( + 'CONFIG_INVALID', + 'Expiry must be within 180 days', + ); + config.agentExpiry = expiry; + const client = await privyClient(config); + const policyId = await applyPolicy(config, client, context.configPath); + await createAgent(config, client, context.configPath, policyId); + const path = join( + stateDir(), + `approval-${config.network}-${config.agentAddress}.json`, + ); + let pending: { agent: Approval; builder?: Approval }; + try { + pending = JSON.parse(await readFile(path, 'utf8')); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + pending = { + agent: await newApproval(config, 'approveAgent', { + agentAddress: config.agentAddress, + agentName: `${config.agentName} valid_until ${expiry}`, + }), + }; + if (config.builder.address) + pending.builder = await newApproval(config, 'approveBuilderFee', { + builder: config.builder.address, + maxFeeRate: `${config.builder.fee / 1000}%`, + }); + await atomicWrite(path, JSON.stringify(pending)); + } + if (input.signature) + await submitApproval( + config.network, + pending.agent, + input.signature as string, + ); + if (input.builderSignature && pending.builder) + await submitApproval( + config.network, + pending.builder, + input.builderSignature as string, + ); + const agents = await hlInfo<{ address: string; validUntil: number }[]>( + config.network, + 'extraAgents', + { user: config.masterAddress }, + ); + config.approved = agents.some( + (agent) => + agent.address.toLowerCase() === config.agentAddress?.toLowerCase() && + agent.validUntil >= expiry, + ); + if (config.builder.address) { + const max = await hlInfo(config.network, 'maxBuilderFee', { + user: config.masterAddress, + builder: config.builder.address, + }); + config.approved = config.approved && max >= config.builder.fee; + } + config.revoked = false; + await saveConfig(config, context.configPath); + const deeplink = `ethereum:${config.masterAddress}?request=${encodeURIComponent(JSON.stringify(pending.agent.typedData))}`; + return { + masterAddress: config.masterAddress, + agentAddress: config.agentAddress, + expiry, + policyId, + approved: config.approved, + requiresSignature: !config.approved, + approvals: pending, + deeplink, + ...(await renderQr(deeplink, { enabled: input.qr as boolean })), + }; + }, +}); diff --git a/src/commands/leverage.ts b/src/commands/leverage.ts index 0948513..f4f8f09 100644 --- a/src/commands/leverage.ts +++ b/src/commands/leverage.ts @@ -1,13 +1,60 @@ -import {z} from 'zod'; -import {register} from '../core/router.js'; -import {commandConfig} from './read.js'; -import type {Config} from '../core/config.js'; -import {assertCanTrade,tradingServices,exchangeAction,type TradingServices} from '../core/trading.js'; -import {AgentCliError} from '../errors/index.js'; -export const leverageSchema=z.object({coin:z.string(),leverage:z.number().int().positive(),mode:z.enum(['cross','isolated']).default('cross'),intent:z.string().optional()}); -export async function setLeverage(config:Config,input:z.infer,services?:TradingServices){ - await assertCanTrade(config);const clients=services??await tradingServices(config);const market=await clients.market(input.coin); - if((config.allowedMarkets.length&&!config.allowedMarkets.includes(input.coin))||input.leverage>Math.min(market.maxLeverage,config.limits.maxLeverage??Infinity)) throw new AgentCliError('POLICY_REJECTED','Leverage or market exceeds configured limits'); - return exchangeAction('updateLeverage',()=>clients.exchange.updateLeverage({asset:market.asset,isCross:input.mode==='cross',leverage:input.leverage})); +import { z } from 'zod'; +import { register } from '../core/router.js'; +import { commandConfig } from './read.js'; +import type { Config } from '../core/config.js'; +import { + assertCanTrade, + tradingServices, + exchangeAction, + requiredDailyLoss, + type TradingServices, +} from '../core/trading.js'; +import { parseUsdc } from '../funding/amounts.js'; +import { AgentCliError } from '../errors/index.js'; +export const leverageSchema = z.object({ + coin: z.string(), + leverage: z.number().int().positive(), + mode: z.enum(['cross', 'isolated']).default('cross'), + intent: z.string().optional(), +}); +export async function setLeverage( + config: Config, + input: z.infer, + services?: TradingServices, +) { + await assertCanTrade(config); + const clients = services ?? (await tradingServices(config)); + const market = await clients.market(input.coin); + if ( + (config.allowedMarkets.length && + !config.allowedMarkets.includes(input.coin)) || + input.leverage > + Math.min(market.maxLeverage, config.limits.maxLeverage ?? Infinity) + ) + throw new AgentCliError( + 'POLICY_REJECTED', + 'Leverage or market exceeds configured limits', + ); + if ( + config.limits.dailyLoss !== undefined && + parseUsdc(await requiredDailyLoss(clients), 8) > + parseUsdc(String(config.limits.dailyLoss), 8) + ) + throw new AgentCliError('POLICY_REJECTED', 'Daily loss cap exceeded'); + return exchangeAction('updateLeverage', () => + clients.exchange.updateLeverage({ + asset: market.asset, + isCross: input.mode === 'cross', + leverage: input.leverage, + }), + ); } -register('leverage',{description:'Set market leverage and cross/isolated margin mode',schema:leverageSchema,execute:async(input,context)=>setLeverage(await commandConfig(context),input as z.infer)}); +register('leverage', { + description: 'Set market leverage and cross/isolated margin mode', + schema: leverageSchema, + execute: async (input, context) => + setLeverage( + await commandConfig(context), + input as z.infer, + ), +}); diff --git a/src/commands/logs.ts b/src/commands/logs.ts index 2c86d63..a480f63 100644 --- a/src/commands/logs.ts +++ b/src/commands/logs.ts @@ -1,15 +1,83 @@ -import {z} from 'zod'; -import {readdir,readFile} from 'node:fs/promises'; -import {join} from 'node:path'; -import {register} from '../core/router.js'; -import {stateDir} from '../core/config.js'; -import {levels,redact,type Level} from '../core/logger.js'; -export async function readLogs(options:{directory?:string;level?:Level;since?:string;requestId?:string}={}){ - const directory=options.directory??join(stateDir(),'logs');let files:string[];try{files=await readdir(directory);}catch(error){if((error as NodeJS.ErrnoException).code==='ENOENT') return [];throw error;} - const lines=(await Promise.all(files.filter(f=>/^runner.*\.jsonl$/.test(f)).map(f=>readFile(join(directory,f),'utf8')))).flatMap(content=>content.split('\n').filter(Boolean).map(line=>JSON.parse(line) as Record)); - return lines.filter(line=>(!options.level||levels[line.level as Level]>=levels[options.level])&&(!options.since||String(line.time)>=options.since)&&(!options.requestId||line.requestId===options.requestId)).sort((a,b)=>String(a.time).localeCompare(String(b.time))).map(redact); +import { z } from 'zod'; +import { readdir, readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { register } from '../core/router.js'; +import { stateDir } from '../core/config.js'; +import { levels, redact, type Level } from '../core/logger.js'; +export async function readLogs( + options: { + directory?: string; + level?: Level; + since?: string; + requestId?: string; + } = {}, +) { + const directory = options.directory ?? join(stateDir(), 'logs'); + let files: string[]; + try { + files = await readdir(directory); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []; + throw error; + } + const lines = ( + await Promise.all( + files + .filter((f) => /^runner.*\.jsonl$/.test(f)) + .map((f) => readFile(join(directory, f), 'utf8')), + ) + ).flatMap((content) => + content + .split('\n') + .slice(0, -1) + .filter(Boolean) + .map((line) => JSON.parse(line) as Record), + ); + return lines + .filter( + (line) => + (!options.level || + levels[line.level as Level] >= levels[options.level]) && + (!options.since || String(line.time) >= options.since) && + (!options.requestId || line.requestId === options.requestId), + ) + .sort((a, b) => String(a.time).localeCompare(String(b.time))) + .map(redact); } -register('logs',{description:'Read/follow redacted JSON-line runner logs',schema:z.object({follow:z.boolean().default(false),level:z.enum(['debug','info','warn','error']).optional(),since:z.string().datetime().optional(),requestId:z.string().optional()}),execute:async(input,context)=>{ - context.streaming=true;let cursor=0;const seen=new Set();const abort=new AbortController();const stop=()=>abort.abort();process.once('SIGINT',stop);process.once('SIGTERM',stop); - try{do{const logs=await readLogs(input);for(const log of logs){const key=JSON.stringify(log);if(!seen.has(key)){seen.add(key);process.stdout.write(key+'\n');cursor++;}}if(input.follow&&!abort.signal.aborted) await new Promise(resolve=>setTimeout(resolve,250));}while(input.follow&&!abort.signal.aborted);}finally{process.off('SIGINT',stop);process.off('SIGTERM',stop);}return {lines:cursor}; -}}); +register('logs', { + description: 'Read/follow redacted JSON-line runner logs', + schema: z.object({ + follow: z.boolean().default(false), + level: z.enum(['debug', 'info', 'warn', 'error']).optional(), + since: z.string().datetime().optional(), + requestId: z.string().optional(), + }), + execute: async (input, context) => { + context.streaming = true; + let cursor = 0; + const seen = new Set(); + const abort = new AbortController(); + const stop = () => abort.abort(); + process.once('SIGINT', stop); + process.once('SIGTERM', stop); + try { + do { + const logs = await readLogs(input); + for (const log of logs) { + const key = JSON.stringify(log); + if (!seen.has(key)) { + seen.add(key); + process.stdout.write(key + '\n'); + cursor++; + } + } + if (input.follow && !abort.signal.aborted) + await new Promise((resolve) => setTimeout(resolve, 250)); + } while (input.follow && !abort.signal.aborted); + } finally { + process.off('SIGINT', stop); + process.off('SIGTERM', stop); + } + return { lines: cursor }; + }, +}); diff --git a/src/commands/market.ts b/src/commands/market.ts new file mode 100644 index 0000000..f9f8291 --- /dev/null +++ b/src/commands/market.ts @@ -0,0 +1,33 @@ +import { z } from 'zod'; +import { register } from '../core/router.js'; +import { createClients } from '../core/hlclient.js'; +import { hlInfo } from '../core/http.js'; +import { commandConfig } from './read.js'; +import { AgentCliError } from '../errors/index.js'; +import { mapHyperliquidError } from '../errors/mappers.js'; +register('market', { + description: + 'Discover market asset, precision, leverage ceiling and current mid', + schema: z.object({ coin: z.string() }), + execute: async (input, context) => { + const config = await commandConfig(context); + try { + const [market, mids] = await Promise.all([ + createClients(config.network).meta.lookup(input.coin as string), + hlInfo>(config.network, 'allMids'), + ]); + const mid = mids[market.name]; + if (!mid) + throw new AgentCliError('UNKNOWN_MARKET', 'Market has no mid price'); + return { + coin: market.name, + asset: String(market.asset), + szDecimals: String(market.szDecimals), + maxLeverage: String(market.maxLeverage), + mid, + }; + } catch (error) { + throw mapHyperliquidError(error); + } + }, +}); diff --git a/src/commands/modify.ts b/src/commands/modify.ts index ab1b89f..b2f3df2 100644 --- a/src/commands/modify.ts +++ b/src/commands/modify.ts @@ -1,22 +1,114 @@ -import {z} from 'zod'; -import {register} from '../core/router.js'; -import {commandConfig} from './read.js'; -import type {Config} from '../core/config.js'; -import {assertCanTrade,tradingServices,checkRisk,exchangeAction,type TradingServices} from '../core/trading.js'; -import {roundSize,roundPrice} from '../core/hlclient.js'; -import {AgentCliError} from '../errors/index.js'; -const modification=z.object({oid:z.number().int().positive().optional(),cloid:z.string().regex(/^0x[0-9a-fA-F]{32}$/).optional(),size:z.string().optional(),price:z.string().optional()}); -export const modifySchema=modification.extend({modifications:z.array(modification).optional(),intent:z.string().optional()}); -export async function modifyOrders(config:Config,input:z.infer,services?:TradingServices){ - await assertCanTrade(config);const clients=services??await tradingServices(config);const orders=await clients.orders();const requests=input.modifications??[input];const modifies:Record[]=[]; - for(const request of requests){ - if(!request.oid&&!request.cloid) throw new AgentCliError('CONFIG_INVALID','Specify oid or cloid'); - const original=orders.find(o=>request.oid?o.oid===request.oid:o.cloid===request.cloid);if(!original) throw new AgentCliError('ORDER_NOT_FOUND','Resting order absent'); - const market=await clients.market(original.coin);const size=roundSize(request.size??original.sz,market.szDecimals);const price=roundPrice(request.price??original.limitPx,market.szDecimals); - const state=await clients.state();const leverage=state.assetPositions.find(p=>p.position.coin===original.coin)?.position.leverage.value??1; - await checkRisk(config,{coin:original.coin,size,price,side:original.side==='B'?'buy':'sell',leverage},clients,original.oid); - modifies.push({oid:request.cloid??original.oid,order:{a:market.asset,b:original.side==='B',p:price,s:size,r:false,t:{limit:{tif:'Gtc'}},...(original.cloid?{c:original.cloid}:{})}}); - } - return exchangeAction('batchModify',()=>clients.exchange.batchModify({modifies})); +import { z } from 'zod'; +import { register } from '../core/router.js'; +import { commandConfig } from './read.js'; +import type { Config } from '../core/config.js'; +import { + assertCanTrade, + tradingServices, + checkRisk, + exchangeAction, + type TradingServices, +} from '../core/trading.js'; +import { roundSize, roundPrice } from '../core/hlclient.js'; +import { AgentCliError } from '../errors/index.js'; +const modification = z.object({ + oid: z.number().int().positive().optional(), + cloid: z + .string() + .regex(/^0x[0-9a-fA-F]{32}$/) + .optional(), + size: z.string().optional(), + price: z.string().optional(), +}); +export const modifySchema = modification.extend({ + modifications: z.array(modification).optional(), + intent: z.string().optional(), +}); +export async function modifyOrders( + config: Config, + input: z.infer, + services?: TradingServices, +) { + await assertCanTrade(config); + const clients = services ?? (await tradingServices(config)); + const orders = await clients.orders(); + const staged = [...orders]; + const stagedServices = { ...clients, orders: async () => staged }; + const requests = input.modifications ?? [input]; + const modifies: Record[] = []; + for (const request of requests) { + if (!request.oid && !request.cloid) + throw new AgentCliError('CONFIG_INVALID', 'Specify oid or cloid'); + const original = orders.find((o) => + request.oid ? o.oid === request.oid : o.cloid === request.cloid, + ); + if (!original) + throw new AgentCliError('ORDER_NOT_FOUND', 'Resting order absent'); + if ( + original.isTrigger && + (!original.triggerPx || + !/take profit|stop/i.test(original.orderType ?? '')) + ) + throw new AgentCliError( + 'UPSTREAM_ERROR', + 'Trigger order metadata incomplete', + ); + const market = await clients.market(original.coin); + const size = roundSize(request.size ?? original.sz, market.szDecimals); + const price = roundPrice( + request.price ?? original.limitPx, + market.szDecimals, + ); + const state = await clients.state(); + const leverage = + state.assetPositions.find((p) => p.position.coin === original.coin) + ?.position.leverage.value ?? 1; + await checkRisk( + config, + { + coin: original.coin, + size, + price, + side: original.side === 'B' ? 'buy' : 'sell', + leverage, + reduceOnly: original.reduceOnly, + }, + stagedServices, + original.oid, + ); + const index = staged.findIndex((order) => order.oid === original.oid); + staged[index] = { ...original, sz: size, limitPx: price }; + modifies.push({ + oid: request.cloid ?? original.oid, + order: { + a: market.asset, + b: original.side === 'B', + p: price, + s: size, + r: original.reduceOnly ?? false, + t: original.isTrigger + ? { + trigger: { + triggerPx: roundPrice(original.triggerPx!, market.szDecimals), + isMarket: /market/i.test(original.orderType!), + tpsl: /take profit/i.test(original.orderType!) ? 'tp' : 'sl', + }, + } + : { limit: { tif: original.tif ?? 'Gtc' } }, + ...(original.cloid ? { c: original.cloid } : {}), + }, + }); + } + return exchangeAction('batchModify', () => + clients.exchange.batchModify({ modifies }), + ); } -register('modify',{description:'Modify price/size of single or batch resting orders',schema:modifySchema,execute:async(input,context)=>modifyOrders(await commandConfig(context),input as z.infer)}); +register('modify', { + description: 'Modify price/size of single or batch resting orders', + schema: modifySchema, + execute: async (input, context) => + modifyOrders( + await commandConfig(context), + input as z.infer, + ), +}); diff --git a/src/commands/order.ts b/src/commands/order.ts index 347042b..d852d59 100644 --- a/src/commands/order.ts +++ b/src/commands/order.ts @@ -1,25 +1,147 @@ -import {z} from 'zod'; -import {register} from '../core/router.js'; -import {commandConfig} from './read.js'; -import {type Config} from '../core/config.js'; -import {roundSize,roundPrice} from '../core/hlclient.js'; -import {approvedBuilder,builderRate} from '../core/builder.js'; -import {assertCanTrade,tradingServices,checkRisk,exchangeAction,type TradingServices} from '../core/trading.js'; -import {AgentCliError} from '../errors/index.js'; -export const orderSchema=z.object({coin:z.string(),side:z.enum(['buy','sell']),size:z.string(),price:z.string().optional(),type:z.enum(['limit','market','tp','sl']).default('limit'),tif:z.enum(['Gtc','Ioc','Alo']).default('Gtc'),reduceOnly:z.boolean().default(false),cloid:z.string().regex(/^0x[0-9a-fA-F]{32}$/).optional(),triggerPrice:z.string().optional(),slippage:z.number().min(0).max(0.1).default(0.01),intent:z.string().optional()}); -export type OrderInput=z.infer; -export async function prepareOrder(config:Config,input:OrderInput,services:TradingServices){ - const market=await services.market(input.coin);const size=roundSize(input.size,market.szDecimals); - let price=input.price;if(input.type==='market'){const mid=(await services.mids())[input.coin];if(!mid) throw new AgentCliError('UNKNOWN_MARKET','Missing market price');price=String(Number(mid)*(input.side==='buy'?1+input.slippage:1-input.slippage));} - if(!price) throw new AgentCliError('INVALID_PRICE','Provide limit price');price=roundPrice(price,market.szDecimals); - const state=await services.state();const leverage=state.assetPositions.find(p=>p.position.coin===input.coin)?.position.leverage.value??1; - await checkRisk(config,{coin:input.coin,size,price,side:input.side,leverage,reduceOnly:input.reduceOnly},services); - let type:unknown={limit:{tif:input.type==='market'?'Ioc':input.tif}}; - if(input.type==='tp'||input.type==='sl'){if(!input.triggerPrice) throw new AgentCliError('INVALID_PRICE','Trigger price required');type={trigger:{isMarket:!input.price,triggerPx:roundPrice(input.triggerPrice,market.szDecimals),tpsl:input.type}};} - return {a:market.asset,b:input.side==='buy',p:price,s:size,r:input.reduceOnly,t:type,...(input.cloid?{c:input.cloid}:{})}; +import { parseUsdc, formatUsdc } from '../funding/amounts.js'; +import { z } from 'zod'; +import { register } from '../core/router.js'; +import { commandConfig } from './read.js'; +import { type Config } from '../core/config.js'; +import { roundSize, roundPrice } from '../core/hlclient.js'; +import { approvedBuilder, builderRate } from '../core/builder.js'; +import { + assertCanTrade, + tradingServices, + checkRisk, + exchangeAction, + type TradingServices, +} from '../core/trading.js'; +import { AgentCliError } from '../errors/index.js'; +export const orderSchema = z.object({ + coin: z.string(), + side: z.enum(['buy', 'sell']), + size: z.string(), + price: z.string().optional(), + type: z.enum(['limit', 'market', 'tp', 'sl']).default('limit'), + tif: z.enum(['Gtc', 'Ioc', 'Alo']).default('Gtc'), + reduceOnly: z.boolean().default(false), + cloid: z + .string() + .regex(/^0x[0-9a-fA-F]{32}$/) + .optional(), + triggerPrice: z.string().optional(), + slippage: z.number().min(0).max(0.1).default(0.01), + intent: z.string().optional(), +}); +export type OrderInput = z.infer; +export async function prepareOrder( + config: Config, + input: OrderInput, + services: TradingServices, +) { + const market = await services.market(input.coin); + const size = roundSize(input.size, market.szDecimals); + let price = input.price; + if ( + input.type === 'market' || + ((input.type === 'tp' || input.type === 'sl') && !price) + ) { + const mid = (await services.mids())[input.coin]; + if (!mid) throw new AgentCliError('UNKNOWN_MARKET', 'Missing market price'); + price = boundedMarketPrice( + mid, + input.slippage, + input.side, + market.szDecimals, + ); + } + if (!price) throw new AgentCliError('INVALID_PRICE', 'Provide limit price'); + price = roundPrice(price, market.szDecimals); + const state = await services.state(); + const leverage = + state.assetPositions.find((p) => p.position.coin === input.coin)?.position + .leverage.value ?? 1; + await checkRisk( + config, + { + coin: input.coin, + size, + price, + side: input.side, + leverage, + reduceOnly: input.reduceOnly, + }, + services, + ); + let type: unknown = { + limit: { tif: input.type === 'market' ? 'Ioc' : input.tif }, + }; + if (input.type === 'tp' || input.type === 'sl') { + if (!input.triggerPrice) + throw new AgentCliError('INVALID_PRICE', 'Trigger price required'); + type = { + trigger: { + isMarket: !input.price, + triggerPx: roundPrice(input.triggerPrice, market.szDecimals), + tpsl: input.type, + }, + }; + } + return { + a: market.asset, + b: input.side === 'buy', + p: price, + s: size, + r: input.reduceOnly, + t: type, + ...(input.cloid ? { c: input.cloid } : {}), + }; } -export async function placeOrder(config:Config,input:OrderInput,services?:TradingServices){ - await assertCanTrade(config);const clients=services??await tradingServices(config);const order=await prepareOrder(config,input,clients);const builder=await approvedBuilder(config); - const result=await exchangeAction('order',()=>clients.exchange.order({orders:[order],grouping:'na',...(builder?{builder}:{})}));return {result,builderFeeRate:builderRate(config),cloid:input.cloid??null}; +export async function placeOrder( + config: Config, + input: OrderInput, + services?: TradingServices, +) { + await assertCanTrade(config); + const clients = services ?? (await tradingServices(config)); + const order = await prepareOrder(config, input, clients); + const builder = await approvedBuilder(config); + const result = await exchangeAction('order', () => + clients.exchange.order({ + orders: [order], + grouping: 'na', + ...(builder ? { builder } : {}), + }), + ); + return { + result, + builderFeeRate: builderRate(config), + cloid: input.cloid ?? null, + }; +} +register('order', { + description: 'Place limit, IOC market or TP/SL order', + schema: orderSchema, + execute: async (input, context) => + placeOrder(await commandConfig(context), input as OrderInput), +}); + +export function boundedMarketPrice( + mid: string, + slippage: number, + side: 'buy' | 'sell', + szDecimals: number, +) { + const units = parseUsdc(mid, 18); + const ratio = BigInt(Math.round(slippage * 1e9)); + const bound = + (units * (side === 'buy' ? 1000000000n + ratio : 1000000000n - ratio)) / + 1000000000n; + const rounded = roundPrice(formatUsdc(bound, 18), szDecimals); + if (side === 'buy' || parseUsdc(rounded, 18) >= bound) return rounded; + const precision = Math.max( + 0, + Math.min( + 6 - szDecimals, + 4 - Math.floor(Math.log10(Number(formatUsdc(bound, 18)))), + ), + ); + const tick = 10n ** BigInt(18 - precision); + return formatUsdc(parseUsdc(rounded, 18) + tick, 18); } -register('order',{description:'Place limit, IOC market or TP/SL order',schema:orderSchema,execute:async(input,context)=>placeOrder(await commandConfig(context),input as OrderInput)}); diff --git a/src/commands/read.ts b/src/commands/read.ts index 0c9792c..5fc86e5 100644 --- a/src/commands/read.ts +++ b/src/commands/read.ts @@ -1,28 +1,169 @@ -import {runnerHealth} from '../runner/health.js'; -import {events} from '../runner/events.js'; -import {activeRunner} from '../runner/lifecycle.js'; -import {z} from 'zod'; -import {register,type Context} from '../core/router.js'; -import {loadConfig,type Config} from '../core/config.js'; -import {hlInfo} from '../core/http.js'; -import {pollDeposits} from '../funding/status.js'; -import {parseUsdc} from '../funding/amounts.js'; -import {AgentCliError} from '../errors/index.js'; -import {mapHyperliquidError} from '../errors/mappers.js'; -export interface AccountState {marginSummary:{accountValue:string;totalMarginUsed:string;totalNtlPos:string};withdrawable:string;assetPositions:{position:{coin:string;szi:string;entryPx:string|null;unrealizedPnl:string;liquidationPx:string|null;leverage:{type:string;value:number}}}[];} -export interface OpenOrder {coin:string;oid:number;cloid?:string;sz:string;limitPx:string;side:'A'|'B';} -export function accountBalance(state:AccountState){return {accountValue:state.marginSummary.accountValue,accountValueUnits:parseUsdc(state.marginSummary.accountValue,8).toString(),withdrawable:state.withdrawable,marginUsed:state.marginSummary.totalMarginUsed};} -export function positions(state:AccountState){return state.assetPositions.map(({position:p})=>({coin:p.coin,size:p.szi,entryPrice:p.entryPx,unrealizedPnl:p.unrealizedPnl,liquidationPrice:p.liquidationPx,leverage:String(p.leverage.value),marginMode:p.leverage.type}));} -export function expiryStatus(config:Config,now=Date.now()){const days=config.agentExpiry===undefined?null:Math.max(0,(config.agentExpiry-now)/86400000);return {expiry:config.agentExpiry??null,daysRemaining:days===null?null:String(days),warnings:days===null?[]:config.expiryWarningDays.filter(threshold=>days<=threshold).map(threshold=>`Agent expires within ${threshold} days`)};} -export async function readCommand(name:string,config:Config){ - if(!config.masterAddress) throw new AgentCliError('AGENT_NOT_APPROVED','Initialize master address'); - try{ - if(name==='orders') return (await hlInfo(config.network,'frontendOpenOrders',{user:config.masterAddress})).map(order=>({...order,oid:String(order.oid)})); - const state=await hlInfo(config.network,'clearinghouseState',{user:config.masterAddress}); - if(name==='balance') return accountBalance(state);if(name==='positions') return positions(state); - const [agents,deposits,maxBuilderFee]=await Promise.all([hlInfo<{address:string;validUntil:number}[]>(config.network,'extraAgents',{user:config.masterAddress}),pollDeposits(config.network,config.masterAddress,state.marginSummary.accountValue,{emit:activeRunner?async deposit=>{await events.append('deposit',{id:deposit.id,state:deposit.state});}:undefined}),config.builder.address?hlInfo(config.network,'maxBuilderFee',{user:config.masterAddress,builder:config.builder.address}):Promise.resolve(0)]); - return {balance:accountBalance(state),positions:positions(state),agent:{address:config.agentAddress,approved:agents.some(a=>a.address.toLowerCase()===config.agentAddress?.toLowerCase()),revoked:config.revoked,...expiryStatus(config)},policy:{id:config.policyId??null,version:String(config.policyVersion??0)},deposits:deposits.map(d=>({id:d.id,state:d.state})),approvedMaxBuilderFee:String(maxBuilderFee),runner:await runnerHealth()}; - }catch(error){throw mapHyperliquidError(error);} +import { collateralBalance } from '../core/account.js'; +import { runnerHealth } from '../runner/health.js'; +import { events } from '../runner/events.js'; +import { activeRunner } from '../runner/lifecycle.js'; +import { z } from 'zod'; +import { register, type Context } from '../core/router.js'; +import { loadConfig, type Config } from '../core/config.js'; +import { hlInfo } from '../core/http.js'; +import { pollDeposits } from '../funding/status.js'; +import { parseUsdc } from '../funding/amounts.js'; +import { AgentCliError } from '../errors/index.js'; +import { mapHyperliquidError } from '../errors/mappers.js'; +export interface AccountState { + marginSummary: { + accountValue: string; + totalMarginUsed: string; + totalNtlPos: string; + }; + withdrawable: string; + assetPositions: { + position: { + coin: string; + szi: string; + entryPx: string | null; + unrealizedPnl: string; + liquidationPx: string | null; + leverage: { type: string; value: number }; + }; + }[]; } -export async function commandConfig(context:Context){const config=await loadConfig(context.configPath);if(context.network) config.network=context.network;return config;} -for(const name of ['status','balance','positions','orders']) register(name,{description:`Read account ${name}`,schema:z.object({}),execute:async(_input,context)=>readCommand(name,await commandConfig(context))}); +export interface OpenOrder { + coin: string; + oid: number; + cloid?: string; + sz: string; + limitPx: string; + side: 'A' | 'B'; + reduceOnly?: boolean; + isTrigger?: boolean; + triggerPx?: string; + triggerCondition?: string; + orderType?: string; + tif?: 'Gtc' | 'Ioc' | 'Alo'; +} +export function accountBalance(state: AccountState) { + return { + accountValue: state.marginSummary.accountValue, + accountValueUnits: parseUsdc( + state.marginSummary.accountValue, + 8, + ).toString(), + withdrawable: state.withdrawable, + marginUsed: state.marginSummary.totalMarginUsed, + }; +} +export function positions(state: AccountState) { + return state.assetPositions.map(({ position: p }) => ({ + coin: p.coin, + size: p.szi, + entryPrice: p.entryPx, + unrealizedPnl: p.unrealizedPnl, + liquidationPrice: p.liquidationPx, + leverage: String(p.leverage.value), + marginMode: p.leverage.type, + })); +} +export function expiryStatus(config: Config, now = Date.now()) { + const days = + config.agentExpiry === undefined + ? null + : Math.max(0, (config.agentExpiry - now) / 86400000); + return { + expiry: config.agentExpiry ?? null, + daysRemaining: days === null ? null : String(days), + warnings: + days === null + ? [] + : config.expiryWarningDays + .filter((threshold) => days <= threshold) + .map((threshold) => `Agent expires within ${threshold} days`), + }; +} +export async function readCommand(name: string, config: Config) { + if (!config.masterAddress) + throw new AgentCliError('AGENT_NOT_APPROVED', 'Initialize master address'); + try { + if (name === 'orders') + return ( + await hlInfo(config.network, 'frontendOpenOrders', { + user: config.masterAddress, + }) + ).map((order) => + Object.fromEntries( + Object.entries(order).map(([key, value]) => [ + key, + typeof value === 'number' ? String(value) : value, + ]), + ), + ); + const state = await hlInfo( + config.network, + 'clearinghouseState', + { user: config.masterAddress }, + ); + if (name === 'balance') return accountBalance(state); + if (name === 'positions') return positions(state); + const [agents, deposits, maxBuilderFee] = await Promise.all([ + hlInfo<{ address: string; validUntil: number }[]>( + config.network, + 'extraAgents', + { user: config.masterAddress }, + ), + pollDeposits( + config.network, + config.masterAddress, + collateralBalance(state), + { + emit: activeRunner + ? async (deposit) => { + await events.append('deposit', { + id: deposit.id, + state: deposit.state, + }); + } + : undefined, + }, + ), + config.builder.address + ? hlInfo(config.network, 'maxBuilderFee', { + user: config.masterAddress, + builder: config.builder.address, + }) + : Promise.resolve(0), + ]); + return { + balance: accountBalance(state), + positions: positions(state), + agent: { + address: config.agentAddress, + approved: agents.some( + (a) => a.address.toLowerCase() === config.agentAddress?.toLowerCase(), + ), + revoked: config.revoked, + ...expiryStatus(config), + }, + policy: { + id: config.policyId ?? null, + version: String(config.policyVersion ?? 0), + }, + deposits: deposits.map((d) => ({ id: d.id, state: d.state })), + approvedMaxBuilderFee: String(maxBuilderFee), + runner: await runnerHealth(), + }; + } catch (error) { + throw mapHyperliquidError(error); + } +} +export async function commandConfig(context: Context) { + const config = await loadConfig(context.configPath); + if (context.network) config.network = context.network; + return config; +} +for (const name of ['status', 'balance', 'positions', 'orders']) + register(name, { + description: `Read account ${name}`, + schema: z.object({}), + execute: async (_input, context) => + readCommand(name, await commandConfig(context)), + }); diff --git a/src/commands/revoke.ts b/src/commands/revoke.ts index dcb8091..4eabdce 100644 --- a/src/commands/revoke.ts +++ b/src/commands/revoke.ts @@ -1,20 +1,74 @@ -import {z} from 'zod'; -import {readFile} from 'node:fs/promises'; -import {join} from 'node:path'; -import {register} from '../core/router.js'; -import {commandConfig} from './read.js'; -import {cancelOrders,cancelSchema} from './cancel.js'; -import {atomicWrite,saveConfig,stateDir,type Config} from '../core/config.js'; -import {approval,submitApproval,type Approval} from '../wallet/approval.js'; -import {activeRunner} from '../runner/lifecycle.js'; -import {AgentCliError} from '../errors/index.js'; -export function markRevoked(config:Config){config.revoked=true;config.approved=false;return config;} -register('revoke',{description:'Disable local agent and prepare master-signed exchange revocation',schema:z.object({cancelOrders:z.boolean().default(false),signature:z.string().optional()}),execute:async(input,context)=>{ - const config=await commandConfig(context);if(!config.agentAddress) throw new AgentCliError('AGENT_NOT_APPROVED','No agent to revoke'); - if(input.cancelOrders&&!config.revoked) await cancelOrders(config,cancelSchema.parse({all:true})); - const path=join(stateDir(),`revoke-${config.agentAddress}.json`);let pending:Approval; - try{pending=JSON.parse(await readFile(path,'utf8'));}catch(error){if((error as NodeJS.ErrnoException).code!=='ENOENT') throw error;pending=approval(config.network,'approveAgent',{agentAddress:'0x'+'0'.repeat(40),agentName:config.agentName});await atomicWrite(path,JSON.stringify(pending));} - let exchangeRevoked=false;if(input.signature){await submitApproval(config.network,pending,input.signature as string);exchangeRevoked=true;config.authorizationKeyRef=undefined;config.privyWalletId=undefined;} - markRevoked(config);await saveConfig(config,context.configPath);if(activeRunner){activeRunner.signer=undefined;Object.assign(activeRunner.config,config);} - return {agentAddress:config.agentAddress,locallyRevoked:true,exchangeRevoked,requiresSignature:!exchangeRevoked,approval:pending}; -}}); +import { z } from 'zod'; +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { register } from '../core/router.js'; +import { commandConfig } from './read.js'; +import { cancelOrders, cancelSchema } from './cancel.js'; +import { + atomicWrite, + saveConfig, + stateDir, + type Config, +} from '../core/config.js'; +import { + newApproval, + submitApproval, + type Approval, +} from '../wallet/approval.js'; +import { activeRunner } from '../runner/lifecycle.js'; +import { AgentCliError } from '../errors/index.js'; +export function markRevoked(config: Config) { + config.revoked = true; + config.approved = false; + return config; +} +register('revoke', { + description: + 'Disable local agent and prepare master-signed exchange revocation', + schema: z.object({ + cancelOrders: z.boolean().default(false), + signature: z.string().optional(), + }), + execute: async (input, context) => { + const config = await commandConfig(context); + if (!config.agentAddress) + throw new AgentCliError('AGENT_NOT_APPROVED', 'No agent to revoke'); + if (input.cancelOrders && !config.revoked) + await cancelOrders(config, cancelSchema.parse({ all: true })); + const path = join( + stateDir(), + `revoke-${config.network}-${config.agentAddress}.json`, + ); + let pending: Approval; + try { + pending = JSON.parse(await readFile(path, 'utf8')); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + pending = await newApproval(config, 'approveAgent', { + agentAddress: '0x' + '0'.repeat(40), + agentName: config.agentName, + }); + await atomicWrite(path, JSON.stringify(pending)); + } + let exchangeRevoked = false; + if (input.signature) { + await submitApproval(config.network, pending, input.signature as string); + exchangeRevoked = true; + config.authorizationKeyRef = undefined; + config.privyWalletId = undefined; + } + markRevoked(config); + await saveConfig(config, context.configPath); + if (activeRunner) { + activeRunner.signer = undefined; + Object.assign(activeRunner.config, config); + } + return { + agentAddress: config.agentAddress, + locallyRevoked: true, + exchangeRevoked, + requiresSignature: !exchangeRevoked, + approval: pending, + }; + }, +}); diff --git a/src/commands/rotate.ts b/src/commands/rotate.ts index 0badf1b..efbcdc2 100644 --- a/src/commands/rotate.ts +++ b/src/commands/rotate.ts @@ -1,27 +1,171 @@ -import {z} from 'zod'; -import {join} from 'node:path'; -import {readFile} from 'node:fs/promises'; -import {register} from '../core/router.js'; -import {commandConfig} from './read.js'; -import {atomicWrite,saveConfig,stateDir,type Config} from '../core/config.js'; -import {privyClient,createAgent,privySigner} from '../wallet/privy.js'; -import {approval,submitApproval,type Approval} from '../wallet/approval.js'; -import {hlInfo} from '../core/http.js'; -import {activeRunner} from '../runner/lifecycle.js'; -import {AgentCliError} from '../errors/index.js'; -import {events} from '../runner/events.js'; -export interface Rotation {old:Config;next:Config;approval:Approval;revoke:Approval;swapped:boolean;revoked:boolean;} -export async function rotationHandoff(rotation:Rotation,dependencies:{verify:()=>Promise;swap:(next:Config)=>Promise;persist:()=>Promise}){ - if(!await dependencies.verify()) throw new AgentCliError('AGENT_NOT_APPROVED','New agent approval not visible; old agent remains active'); - if(!rotation.swapped){await dependencies.swap(rotation.next);rotation.swapped=true;await dependencies.persist();}return rotation.revoke; +import { z } from 'zod'; +import { join } from 'node:path'; +import { readFile } from 'node:fs/promises'; +import { register } from '../core/router.js'; +import { commandConfig } from './read.js'; +import { + atomicWrite, + saveConfig, + stateDir, + type Config, +} from '../core/config.js'; +import { privyClient, createAgent, privySigner } from '../wallet/privy.js'; +import { + newApproval, + submitApproval, + type Approval, +} from '../wallet/approval.js'; +import { hlInfo } from '../core/http.js'; +import { activeRunner } from '../runner/lifecycle.js'; +import { AgentCliError } from '../errors/index.js'; +import { events } from '../runner/events.js'; +export interface Rotation { + old: Config; + next: Config; + approval: Approval; + revoke: Approval; + swapped: boolean; + revoked: boolean; } -register('rotate',{description:'Approve new agent, hot-swap signer, then revoke old agent',schema:z.object({signature:z.string().optional(),revokeSignature:z.string().optional()}),execute:async(input,context)=>{ - const current=await commandConfig(context);if(!current.masterAddress||!current.agentAddress) throw new AgentCliError('AGENT_NOT_APPROVED','Initialize first'); - const path=join(stateDir(),`rotation-${current.masterAddress}.json`);let rotation:Rotation;const client=await privyClient(current); - try{rotation=JSON.parse(await readFile(path,'utf8'));}catch(error){if((error as NodeJS.ErrnoException).code!=='ENOENT') throw error; - const next:Config={...structuredClone(current),privyWalletId:undefined,agentAddress:undefined,authorizationKeyRef:undefined,agentExpiry:Date.now()+179*86400000,approved:false,revoked:false};await createAgent(next,client,join(stateDir(),'rotation-next.json'),current.policyId);next.agentName=`blox-${next.agentAddress!.slice(-8)}`; - rotation={old:current,next,approval:approval(current.network,'approveAgent',{agentAddress:next.agentAddress,agentName:`${next.agentName} valid_until ${next.agentExpiry}`}),revoke:approval(current.network,'approveAgent',{agentAddress:'0x'+'0'.repeat(40),agentName:current.agentName}),swapped:false,revoked:false};await atomicWrite(path,JSON.stringify(rotation));} - if(input.signature){await submitApproval(current.network,rotation.approval,input.signature as string);await rotationHandoff(rotation,{verify:async()=>{const agents=await hlInfo<{address:string;validUntil:number}[]>(current.network,'extraAgents',{user:current.masterAddress});return agents.some(a=>a.address.toLowerCase()===rotation.next.agentAddress?.toLowerCase()&&a.validUntil>=rotation.next.agentExpiry!);},swap:async next=>{const signer=await privySigner(next,client);next.approved=true;await saveConfig(next,context.configPath);if(activeRunner){activeRunner.signer=signer;Object.assign(activeRunner.config,next);}await events.append('runner_state',{rotation:{old:rotation.old.agentAddress,next:next.agentAddress}});},persist:()=>atomicWrite(path,JSON.stringify(rotation))});} - if(input.revokeSignature){if(!rotation.swapped) throw new AgentCliError('POLICY_REJECTED','Approve and swap new agent before revoking old');await submitApproval(current.network,rotation.revoke,input.revokeSignature as string);rotation.revoked=true;await atomicWrite(path,JSON.stringify(rotation));} - return {oldAgent:rotation.old.agentAddress,newAgent:rotation.next.agentAddress,swapped:rotation.swapped,oldRevoked:rotation.revoked,requiresSignature:!rotation.swapped,approval:rotation.approval,...(rotation.swapped?{revokeApproval:rotation.revoke}:{})}; -}}); +export async function rotationHandoff( + rotation: Rotation, + dependencies: { + verify: () => Promise; + swap: (next: Config) => Promise; + persist: () => Promise; + }, +) { + if (!(await dependencies.verify())) + throw new AgentCliError( + 'AGENT_NOT_APPROVED', + 'New agent approval not visible; old agent remains active', + ); + if (!rotation.swapped) { + await dependencies.swap(rotation.next); + rotation.swapped = true; + await dependencies.persist(); + } + return rotation.revoke; +} +register('rotate', { + description: 'Approve new agent, hot-swap signer, then revoke old agent', + schema: z.object({ + signature: z.string().optional(), + revokeSignature: z.string().optional(), + }), + execute: async (input, context) => { + const current = await commandConfig(context); + if (!current.masterAddress || !current.agentAddress) + throw new AgentCliError('AGENT_NOT_APPROVED', 'Initialize first'); + const path = join( + stateDir(), + `rotation-${current.network}-${current.masterAddress}.json`, + ); + let rotation: Rotation; + const client = await privyClient(current); + try { + rotation = JSON.parse(await readFile(path, 'utf8')); + if (rotation.revoked && !input.signature && !input.revokeSignature) { + await atomicWrite( + `${path}.${Date.now()}.previous`, + JSON.stringify(rotation), + ); + throw Object.assign(new Error('Begin next rotation'), { + code: 'ENOENT', + }); + } + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + const next: Config = { + ...structuredClone(current), + privyWalletId: undefined, + agentAddress: undefined, + authorizationKeyRef: undefined, + agentExpiry: Date.now() + 179 * 86400000, + approved: false, + revoked: false, + }; + await createAgent( + next, + client, + join(stateDir(), 'rotation-next.json'), + current.policyId, + ); + next.agentName = `blox-${next.agentAddress!.slice(-8)}`; + rotation = { + old: current, + next, + approval: await newApproval(current, 'approveAgent', { + agentAddress: next.agentAddress, + agentName: `${next.agentName} valid_until ${next.agentExpiry}`, + }), + revoke: await newApproval(current, 'approveAgent', { + agentAddress: '0x' + '0'.repeat(40), + agentName: current.agentName, + }), + swapped: false, + revoked: false, + }; + await atomicWrite(path, JSON.stringify(rotation)); + } + if (input.signature) { + await submitApproval( + current.network, + rotation.approval, + input.signature as string, + ); + await rotationHandoff(rotation, { + verify: async () => { + const agents = await hlInfo< + { address: string; validUntil: number }[] + >(current.network, 'extraAgents', { user: current.masterAddress }); + return agents.some( + (a) => + a.address.toLowerCase() === + rotation.next.agentAddress?.toLowerCase() && + a.validUntil >= rotation.next.agentExpiry!, + ); + }, + swap: async (next) => { + const signer = await privySigner(next, client); + next.approved = true; + await saveConfig(next, context.configPath); + if (activeRunner) { + activeRunner.signer = signer; + Object.assign(activeRunner.config, next); + } + await events.append('runner_state', { + rotation: { + old: rotation.old.agentAddress, + next: next.agentAddress, + }, + }); + }, + persist: () => atomicWrite(path, JSON.stringify(rotation)), + }); + } + if (input.revokeSignature) { + if (!rotation.swapped) + throw new AgentCliError( + 'POLICY_REJECTED', + 'Approve and swap new agent before revoking old', + ); + await submitApproval( + current.network, + rotation.revoke, + input.revokeSignature as string, + ); + rotation.revoked = true; + await atomicWrite(path, JSON.stringify(rotation)); + } + return { + oldAgent: rotation.old.agentAddress, + newAgent: rotation.next.agentAddress, + swapped: rotation.swapped, + oldRevoked: rotation.revoked, + requiresSignature: !rotation.swapped, + approval: rotation.approval, + ...(rotation.swapped ? { revokeApproval: rotation.revoke } : {}), + }; + }, +}); diff --git a/src/commands/run.ts b/src/commands/run.ts index 86c98e2..5ca9ca8 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -1,12 +1,48 @@ -import {startHeartbeat} from '../runner/health.js'; -import {events,eventTypes,type EventType} from '../runner/events.js'; -import {startSubscriptions} from '../runner/subscriptions.js'; -import {rpcServer} from '../runner/rpc.js'; -import {z} from 'zod'; -import {register} from '../core/router.js'; -import {commandConfig} from './read.js'; -import {Runner,detachRunner,setActiveRunner} from '../runner/lifecycle.js'; -register('run',{description:'Start persistent signing runner (foreground or detach)',schema:z.object({detach:z.boolean().default(false),daemon:z.boolean().default(false)}),execute:async(input,context)=>{ - if(input.detach||input.daemon) return detachRunner([...(context.configPath?['--config',context.configPath]:[]),...(context.network?['--network',context.network]:[])]); - const runner=new Runner(await commandConfig(context));const info=await runner.start();if(!info.existing){setActiveRunner(runner);runner.signals();runner.onShutdown(()=>events.flush());try{await rpcServer(runner,context);const subscriptions=await startSubscriptions(runner,async(type,data)=>{if(eventTypes.includes(type as EventType)) await events.append(type as EventType,data);runner.logger.log(type==='error'?'error':'info',type,{data});});startHeartbeat(runner,subscriptions);}catch(error){await runner.shutdown();throw error;}}return info; -}}); +import { startHeartbeat } from '../runner/health.js'; +import { events, eventTypes, type EventType } from '../runner/events.js'; +import { startSubscriptions } from '../runner/subscriptions.js'; +import { rpcServer } from '../runner/rpc.js'; +import { z } from 'zod'; +import { register } from '../core/router.js'; +import { commandConfig } from './read.js'; +import { Runner, detachRunner, setActiveRunner } from '../runner/lifecycle.js'; +register('run', { + description: 'Start persistent signing runner (foreground or detach)', + schema: z.object({ + detach: z.boolean().default(false), + daemon: z.boolean().default(false), + }), + execute: async (input, context) => { + if (input.detach || input.daemon) + return detachRunner([ + ...(context.configPath ? ['--config', context.configPath] : []), + ...(context.network ? ['--network', context.network] : []), + ]); + const runner = new Runner(await commandConfig(context)); + const info = await runner.start(); + if (!info.existing) { + setActiveRunner(runner); + runner.signals(); + runner.onShutdown(() => events.flush()); + try { + await rpcServer(runner, context); + const subscriptions = await startSubscriptions( + runner, + async (type, data) => { + if (eventTypes.includes(type as EventType)) + await events.append(type as EventType, data); + runner.logger.log(type === 'error' ? 'error' : 'info', type, { + data, + }); + }, + ); + startHeartbeat(runner, subscriptions); + await runner.markReady(); + } catch (error) { + await runner.shutdown(); + throw error; + } + } + return { ...info, ready: info.existing ? info.ready : runner.info.ready }; + }, +}); diff --git a/src/commands/skill.ts b/src/commands/skill.ts new file mode 100644 index 0000000..f372972 --- /dev/null +++ b/src/commands/skill.ts @@ -0,0 +1,52 @@ +import { z } from 'zod'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; +import { readFile } from 'node:fs/promises'; +import { register } from '../core/router.js'; +import { atomicWrite } from '../core/config.js'; +import { cliVersion, skillContent } from '../core/skill-content.js'; +import { AgentCliError } from '../errors/index.js'; +export async function installSkill( + target: 'claude' | 'codex', + options: { home?: string; codexHome?: string; force?: boolean } = {}, +) { + const home = options.home ?? homedir(); + const root = + target === 'codex' + ? (options.codexHome ?? process.env.CODEX_HOME ?? join(home, '.codex')) + : join(home, '.claude'); + const path = join(root, 'skills', 'agent-cli', 'SKILL.md'); + try { + const existing = await readFile(path, 'utf8'); + if (existing === skillContent) + return { target, path, version: cliVersion, changed: false }; + if (!options.force) + throw new AgentCliError( + 'CONFIG_INVALID', + 'Skill already exists with different content; use --force to replace', + ); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + } + await atomicWrite(path, skillContent); + return { target, path, version: cliVersion, changed: true }; +} +register('skill', { + description: + 'Install packaged version-matched skill for Claude Code or Codex', + schema: z.object({ + args: z.array(z.string()).default(['install']), + target: z.enum(['claude', 'codex']).default('codex'), + force: z.boolean().default(false), + }), + execute: async (input) => { + if ((input.args as string[])[0] !== 'install') + throw new AgentCliError( + 'CONFIG_INVALID', + 'Use skill install --target claude|codex', + ); + return installSkill(input.target as 'claude' | 'codex', { + force: input.force as boolean, + }); + }, +}); diff --git a/src/core/account.ts b/src/core/account.ts new file mode 100644 index 0000000..689e731 --- /dev/null +++ b/src/core/account.ts @@ -0,0 +1,13 @@ +import { parseUsdc, formatUsdc } from '../funding/amounts.js'; +// Remove marked PnL so a price move cannot masquerade as receipt of a deposit. +export function collateralBalance(state: { + marginSummary: { accountValue: string }; + assetPositions?: { position: { unrealizedPnl: string } }[]; +}) { + const equity = parseUsdc(state.marginSummary.accountValue, 8); + const pnl = (state.assetPositions ?? []).reduce( + (sum, { position }) => sum + parseUsdc(position.unrealizedPnl, 8), + 0n, + ); + return formatUsdc(equity - pnl, 8); +} diff --git a/src/core/audit.ts b/src/core/audit.ts index 60bb857..aa516c6 100644 --- a/src/core/audit.ts +++ b/src/core/audit.ts @@ -1,36 +1,163 @@ -import {AsyncLocalStorage} from 'node:async_hooks'; -import {createHash,randomUUID} from 'node:crypto'; -import {readFile,open,unlink,mkdir} from 'node:fs/promises'; -import {join,dirname} from 'node:path'; -import {stateDir} from './config.js'; -import {redact} from './logger.js'; -import {AgentCliError,normalizeError} from '../errors/index.js'; -export interface AuditEntry {seq:number;timestamp:string;intentId:string;cloid?:string;stage:string;data:unknown;previousHash:string;hash:string;} -const digest=(value:unknown)=>createHash('sha256').update(JSON.stringify(value)).digest('hex'); -const context=new AsyncLocalStorage<{intentId:string;cloid?:string}>(); -export async function readAudit(path=join(stateDir(),'audit.jsonl')):Promise{try{return (await readFile(path,'utf8')).split('\n').filter(Boolean).map(line=>JSON.parse(line));}catch(error){if((error as NodeJS.ErrnoException).code==='ENOENT') return [];throw error;}} -export function verifyAudit(entries:AuditEntry[]){let previousHash='0'.repeat(64);let seq=0;for(const entry of entries){const {hash,...payload}=entry;if(entry.seq!==++seq||entry.previousHash!==previousHash||digest(payload)!==hash) throw new AgentCliError('INTERNAL','Audit chain integrity failure');previousHash=hash;}return true;} +import { requestContext as context } from './request-context.js'; +import { createHash, randomUUID } from 'node:crypto'; +import { readFile, open, unlink, mkdir, chmod } from 'node:fs/promises'; +import { join, dirname } from 'node:path'; +import { stateDir } from './config.js'; +import { redact } from './logger.js'; +import { AgentCliError, normalizeError } from '../errors/index.js'; +export interface AuditEntry { + seq: number; + timestamp: string; + intentId: string; + cloid?: string; + requestId?: string; + stage: string; + data: unknown; + previousHash: string; + hash: string; +} +const digest = (value: unknown) => + createHash('sha256').update(JSON.stringify(value)).digest('hex'); + +export async function readAudit( + path = join(stateDir(), 'audit.jsonl'), +): Promise { + try { + return (await readFile(path, 'utf8')) + .split('\n') + .filter(Boolean) + .map((line) => JSON.parse(line)); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []; + throw error; + } +} +export function verifyAudit(entries: AuditEntry[]) { + let previousHash = '0'.repeat(64); + let seq = 0; + for (const entry of entries) { + const { hash, ...payload } = entry; + if ( + entry.seq !== ++seq || + entry.previousHash !== previousHash || + digest(payload) !== hash + ) + throw new AgentCliError('INTERNAL', 'Audit chain integrity failure'); + previousHash = hash; + } + return true; +} export class AuditStore { - private queue=Promise.resolve(); - constructor(readonly path=join(stateDir(),'audit.jsonl')){} - append(stage:string,data:unknown,metadata=context.getStore()??{intentId:randomUUID()}){ - const operation=this.queue.then(async()=>{await mkdir(dirname(this.path),{recursive:true,mode:0o700});const lock=this.path+'.lock';let handle; - for(let i=0;i<100;i++){try{handle=await open(lock,'wx',0o600);break;}catch(error){if((error as NodeJS.ErrnoException).code!=='EEXIST') throw error;await new Promise(resolve=>setTimeout(resolve,10));}} - if(!handle) throw new AgentCliError('INTERNAL','Audit lock busy; inspect stale lock'); - try{const entries=await readAudit(this.path);verifyAudit(entries);const last=entries.at(-1);const payload={seq:(last?.seq??0)+1,timestamp:new Date().toISOString(),...metadata,stage,data:redact(data),previousHash:last?.hash??'0'.repeat(64)};const entry={...payload,hash:digest(payload)};const file=await open(this.path,'a',0o600);try{await file.writeFile(JSON.stringify(entry)+'\n');await file.sync();}finally{await file.close();}return entry;}finally{await handle.close();await unlink(lock);} - });this.queue=operation.then(()=>{});return operation; - } - async flush(){await this.queue;} -} -export const audit=new AuditStore(); -export async function auditedAction(name:string,input:Record,operation:()=>Promise):Promise{ - return context.run({intentId:input.intentId as string??randomUUID(),cloid:input.cloid as string|undefined},async()=>{ - await audit.append('intent',{action:name,input});try{const result=await operation();await audit.append('response',{action:name,result});return result;}catch(error){await audit.append('decision',{allowed:false,error:normalizeError(error).toJSON()});throw error;} - }); -} -export async function signatureAudit(signature:string){if(context.getStore()) await audit.append('signature',{digest:createHash('sha256').update(signature).digest('hex')});} -export async function decisionAudit(data:unknown){if(context.getStore()) await audit.append('decision',data);} -export async function auditFill(fill:Record){ - const entries=await readAudit();const linked=[...entries].reverse().find(entry=>entry.cloid&&entry.cloid===fill.cloid||entry.stage==='response'&&JSON.stringify(entry.data).includes(`"oid":${fill.oid}`)); - await audit.append('fill',fill,{intentId:linked?.intentId??'external',cloid:linked?.cloid}); + private queue = Promise.resolve(); + constructor(readonly path = join(stateDir(), 'audit.jsonl')) {} + append( + stage: string, + data: unknown, + metadata = context.getStore() ?? { intentId: randomUUID() }, + ) { + const operation = this.queue.then(async () => { + await mkdir(dirname(this.path), { recursive: true, mode: 0o700 }); + await chmod(dirname(this.path), 0o700); + const lock = this.path + '.lock'; + let handle; + for (let i = 0; i < 100; i++) { + try { + handle = await open(lock, 'wx', 0o600); + break; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error; + await new Promise((resolve) => setTimeout(resolve, 10)); + } + } + if (!handle) + throw new AgentCliError( + 'INTERNAL', + 'Audit lock busy; inspect stale lock', + ); + try { + const entries = await readAudit(this.path); + verifyAudit(entries); + const last = entries.at(-1); + const payload = { + seq: (last?.seq ?? 0) + 1, + timestamp: new Date().toISOString(), + ...metadata, + stage, + data: redact(data), + previousHash: last?.hash ?? '0'.repeat(64), + }; + const entry = { ...payload, hash: digest(payload) }; + const file = await open(this.path, 'a', 0o600); + await file.chmod(0o600); + try { + await file.writeFile(JSON.stringify(entry) + '\n'); + await file.sync(); + } finally { + await file.close(); + } + return entry; + } finally { + await handle.close(); + await unlink(lock); + } + }); + this.queue = operation.then(() => {}); + return operation; + } + async flush() { + await this.queue; + } +} +export const audit = new AuditStore(); +export async function auditedAction( + name: string, + input: Record, + operation: () => Promise, +): Promise { + return context.run( + { + intentId: (input.intentId as string) ?? randomUUID(), + cloid: input.cloid as string | undefined, + requestId: randomUUID(), + }, + async () => { + await audit.append('intent', { action: name, input }); + try { + const result = await operation(); + await audit.append('response', { action: name, result }); + return result; + } catch (error) { + await audit.append('decision', { + allowed: false, + error: normalizeError(error).toJSON(), + }); + throw error; + } + }, + ); +} +export async function signatureAudit(signature: string) { + if (context.getStore()) + await audit.append('signature', { + digest: createHash('sha256').update(signature).digest('base64'), + }); +} +export async function decisionAudit(data: unknown) { + if (context.getStore()) await audit.append('decision', data); +} +export async function auditFill(fill: Record) { + const entries = await readAudit(); + const linked = [...entries] + .reverse() + .find( + (entry) => + (entry.cloid && entry.cloid === fill.cloid) || + (entry.stage === 'response' && + fill.oid !== undefined && + JSON.stringify(entry.data).includes(`"oid":${fill.oid}`)), + ); + await audit.append('fill', fill, { + intentId: linked?.intentId ?? 'external', + cloid: linked?.cloid, + }); } diff --git a/src/core/builder.ts b/src/core/builder.ts index eb3c624..adb77e4 100644 --- a/src/core/builder.ts +++ b/src/core/builder.ts @@ -1,11 +1,31 @@ -import type {Config} from './config.js'; -import {hlInfo} from './http.js'; -import {AgentCliError} from '../errors/index.js'; -export async function approvedBuilder(config:Config,fetchMaximum=(user:string,builder:string)=>hlInfo(config.network,'maxBuilderFee',{user,builder})){ - if(!config.builder.address){if(config.builder.fee) throw new AgentCliError('CONFIG_INVALID','Builder address is required for a fee');return undefined;} - if(!config.masterAddress) throw new AgentCliError('AGENT_NOT_APPROVED','Master address missing'); - const maximum=await fetchMaximum(config.masterAddress,config.builder.address); - if(config.builder.fee>maximum) throw new AgentCliError('AGENT_NOT_APPROVED','Approve configured builder fee with master wallet',{approvedMax:maximum,requested:config.builder.fee}); - return {b:config.builder.address.toLowerCase(),f:config.builder.fee}; +import type { Config } from './config.js'; +import { hlInfo } from './http.js'; +import { AgentCliError } from '../errors/index.js'; +export async function approvedBuilder( + config: Config, + fetchMaximum = (user: string, builder: string) => + hlInfo(config.network, 'maxBuilderFee', { user, builder }), +) { + if (!config.builder.address) { + if (config.builder.fee) + throw new AgentCliError( + 'CONFIG_INVALID', + 'Builder address is required for a fee', + ); + return undefined; + } + if (!config.masterAddress) + throw new AgentCliError('AGENT_NOT_APPROVED', 'Master address missing'); + const maximum = await fetchMaximum( + config.masterAddress, + config.builder.address, + ); + if (config.builder.fee > maximum) + throw new AgentCliError( + 'AGENT_NOT_APPROVED', + 'Approve configured builder fee with master wallet', + { approvedMax: maximum, requested: config.builder.fee }, + ); + return { b: config.builder.address.toLowerCase(), f: config.builder.fee }; } -export const builderRate=(config:Config)=>`${config.builder.fee/1000}%`; +export const builderRate = (config: Config) => `${config.builder.fee / 1000}%`; diff --git a/src/core/config.ts b/src/core/config.ts index f6f25b8..db110fc 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -5,43 +5,96 @@ import { randomUUID } from 'node:crypto'; import { z } from 'zod'; import { AgentCliError } from '../errors/index.js'; export const stateDir = () => join(homedir(), '.bloxwap'); -const address=z.string().regex(/^0x[0-9a-fA-F]{40}$/); -const positive=z.number().positive().finite(); -export const configSchema=z.object({ - version:z.literal(1).default(1),network:z.enum(['mainnet','testnet']).default('testnet'), - agentName:z.string().max(16).default('bloxwap'),masterAddress:address.optional(),agentAddress:address.optional(),agentExpiry:z.number().int().positive().optional(), - privyAppId:z.string().optional(),privyWalletId:z.string().optional(),policyId:z.string().optional(),policyVersion:z.number().int().optional(), - authorizationKeyRef:z.string().regex(/^env:[A-Z_][A-Z0-9_]*$|^file:.+/).optional(), - privyAppSecretRef:z.string().regex(/^env:[A-Z_][A-Z0-9_]*$/).optional(), - revoked:z.boolean().default(false),approved:z.boolean().default(false), - limits:z.object({maxNotional:positive.optional(),maxPositionNotional:positive.optional(),maxLeverage:positive.optional(),maxOrderSize:positive.optional(),dailyLoss:positive.optional(),maxOpenOrders:z.number().int().nonnegative().optional()}).default({}), - allowedMarkets:z.array(z.string()).default([]), - builder:z.object({address:address.optional(),fee:z.number().int().min(0).max(100).default(0)}).default({fee:0}), - logLevel:z.enum(['debug','info','warn','error']).default('info'), - runnerFallback:z.boolean().default(true),expiryWarningDays:z.array(positive).default([14,3,1]), -}).strict(); -export type Config=z.infer; -export const configPath=()=>join(stateDir(),'config.json'); -export async function atomicWrite(path: string,data: string) { - await mkdir(dirname(path),{recursive:true,mode:0o700}); await chmod(dirname(path),0o700); - const temporary=`${path}.${randomUUID()}.tmp`; - await writeFile(temporary,data,{mode:0o600,flag:'wx'}); await rename(temporary,path); await chmod(path,0o600); +const address = z.string().regex(/^0x[0-9a-fA-F]{40}$/); +const positive = z.number().positive().finite(); +export const configSchema = z + .object({ + version: z.literal(1).default(1), + network: z.enum(['mainnet', 'testnet']).default('testnet'), + agentName: z.string().max(16).default('bloxwap'), + masterAddress: address.optional(), + agentAddress: address.optional(), + agentExpiry: z.number().int().positive().optional(), + privyAppId: z.string().optional(), + privyWalletId: z.string().optional(), + policyId: z.string().optional(), + policyVersion: z.number().int().optional(), + authorizationKeyRef: z + .string() + .regex(/^env:[A-Z_][A-Z0-9_]*$|^file:.+/) + .optional(), + privyAppSecretRef: z + .string() + .regex(/^env:[A-Z_][A-Z0-9_]*$/) + .optional(), + revoked: z.boolean().default(false), + approved: z.boolean().default(false), + limits: z + .object({ + maxNotional: positive.optional(), + maxPositionNotional: positive.optional(), + maxLeverage: positive.optional(), + maxOrderSize: positive.optional(), + dailyLoss: positive.optional(), + maxOpenOrders: z.number().int().nonnegative().optional(), + }) + .default({}), + allowedMarkets: z.array(z.string()).default([]), + builder: z + .object({ + address: address.optional(), + fee: z.number().int().min(0).max(100).default(0), + }) + .default({ fee: 0 }), + logLevel: z.enum(['debug', 'info', 'warn', 'error']).default('info'), + runnerFallback: z.boolean().default(true), + expiryWarningDays: z.array(positive).default([14, 3, 1]), + }) + .strict(); +export type Config = z.infer; +export const configPath = () => join(stateDir(), 'config.json'); +export async function atomicWrite(path: string, data: string) { + await mkdir(dirname(path), { recursive: true, mode: 0o700 }); + await chmod(dirname(path), 0o700); + const temporary = `${path}.${randomUUID()}.tmp`; + await writeFile(temporary, data, { mode: 0o600, flag: 'wx' }); + await rename(temporary, path); + await chmod(path, 0o600); } export function migrate(raw: unknown): Config { - if (raw && typeof raw==='object' && !('version' in raw)) raw={...raw,version:1}; - const result=configSchema.safeParse(raw); - if (!result.success) throw new AgentCliError('CONFIG_INVALID','Invalid config',result.error.flatten()); - return result.data; + if (raw && typeof raw === 'object' && !('version' in raw)) + raw = { ...raw, version: 1 }; + const result = configSchema.safeParse(raw); + if (!result.success) + throw new AgentCliError( + 'CONFIG_INVALID', + 'Invalid config', + result.error.flatten(), + ); + return result.data; } -export async function loadConfig(path=configPath(),env: NodeJS.ProcessEnv=process.env): Promise { - let raw: unknown={}; - try {raw=JSON.parse(await readFile(path,'utf8'));} catch(error) { - if ((error as NodeJS.ErrnoException).code!=='ENOENT') throw new AgentCliError('CONFIG_INVALID','Cannot read config'); - } - const config=migrate(raw); - for (const [name,key] of Object.entries({BLOXWAP_NETWORK:'network',BLOXWAP_LOG_LEVEL:'logLevel',BLOXWAP_PRIVY_APP_ID:'privyAppId',BLOXWAP_MASTER_ADDRESS:'masterAddress'})) { - if (env[name]) Object.assign(config,{[key]:env[name]}); - } - return migrate(config); +export async function loadConfig( + path = configPath(), + env: NodeJS.ProcessEnv = process.env, +): Promise { + let raw: unknown = {}; + try { + raw = JSON.parse(await readFile(path, 'utf8')); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') + throw new AgentCliError('CONFIG_INVALID', 'Cannot read config'); + } + const config = migrate(raw); + for (const [name, key] of Object.entries({ + BLOXWAP_NETWORK: 'network', + BLOXWAP_LOG_LEVEL: 'logLevel', + BLOXWAP_PRIVY_APP_ID: 'privyAppId', + BLOXWAP_MASTER_ADDRESS: 'masterAddress', + })) { + if (env[name]) Object.assign(config, { [key]: env[name] }); + } + return migrate(config); +} +export async function saveConfig(config: unknown, path = configPath()) { + await atomicWrite(path, JSON.stringify(migrate(config), null, 2) + '\n'); } -export async function saveConfig(config: unknown,path=configPath()) {await atomicWrite(path,JSON.stringify(migrate(config),null,2)+'\n');} diff --git a/src/core/hlclient.ts b/src/core/hlclient.ts index 3ffdc91..0279274 100644 --- a/src/core/hlclient.ts +++ b/src/core/hlclient.ts @@ -1,30 +1,127 @@ -import { ExchangeClient, HttpTransport, InfoClient, SubscriptionClient, WebSocketTransport } from '@bloxwap/hyperliquid'; +import { + ExchangeClient, + HttpTransport, + InfoClient, + SubscriptionClient, + WebSocketTransport, +} from '@bloxwap/hyperliquid'; import type { AbstractViemLocalAccount } from '@bloxwap/hyperliquid/signing'; import { formatPrice, formatSize } from '@bloxwap/hyperliquid/utils'; import { AgentCliError } from '../errors/index.js'; -export type Signer=AbstractViemLocalAccount; -export interface Market {name:string;szDecimals:number;maxLeverage:number;asset:number;} +export type Signer = AbstractViemLocalAccount; +export interface Market { + name: string; + szDecimals: number; + maxLeverage: number; + asset: number; +} export class MetaCache { - private markets=new Map(); private updated=0; - constructor(private fetchMeta:()=>Promise<{universe:{name:string;szDecimals:number;maxLeverage:number}[]}>,private ttl=60_000){} - async refresh(){const meta=await this.fetchMeta();this.markets=new Map(meta.universe.map((market,asset)=>[market.name,{...market,asset}]));this.updated=Date.now();} - async lookup(coin:string){if(Date.now()-this.updated>this.ttl) await this.refresh();const market=this.markets.get(coin);if(!market) throw new AgentCliError('UNKNOWN_MARKET',`Unknown market ${coin}`);return market;} + private markets = new Map(); + private updated = 0; + constructor( + private fetchMeta: () => Promise<{ + universe: { name: string; szDecimals: number; maxLeverage: number }[]; + }>, + private ttl = 60_000, + ) {} + async refresh() { + const meta = await this.fetchMeta(); + if ( + !Array.isArray(meta.universe) || + meta.universe.some( + (market) => + !market.name || + !Number.isInteger(market.szDecimals) || + market.szDecimals < 0 || + market.szDecimals > 6 || + !Number.isInteger(market.maxLeverage) || + market.maxLeverage < 1, + ) + ) + throw new AgentCliError('UPSTREAM_ERROR', 'Invalid market metadata'); + this.markets = new Map( + meta.universe.map((market, asset) => [market.name, { ...market, asset }]), + ); + this.updated = Date.now(); + } + async lookup(coin: string) { + if (Date.now() - this.updated > this.ttl) await this.refresh(); + const market = this.markets.get(coin); + if (!market) + throw new AgentCliError('UNKNOWN_MARKET', `Unknown market ${coin}`); + return market; + } } -export function roundSize(size:string,szDecimals:number){ - if(!/^\d+(\.\d+)?$/.test(size)||Number(size)<=0) throw new AgentCliError('INVALID_SIZE','Size must be positive'); - const rounded=formatSize(size,szDecimals);if(Number(rounded)<=0) throw new AgentCliError('INVALID_SIZE','Size rounds to zero');return rounded; +export function roundSize(size: string, szDecimals: number) { + if (!/^\d+(\.\d+)?$/.test(size) || Number(size) <= 0) + throw new AgentCliError('INVALID_SIZE', 'Size must be positive'); + const rounded = formatSize(size, szDecimals); + if (Number(rounded) <= 0) + throw new AgentCliError('INVALID_SIZE', 'Size rounds to zero'); + return rounded; } -export function roundPrice(price:string,szDecimals:number){ - if(!/^\d+(\.\d+)?$/.test(price)||Number(price)<=0) throw new AgentCliError('INVALID_PRICE','Price must be positive'); - const rounded=formatPrice(price,szDecimals);if(Number(rounded)<=0) throw new AgentCliError('INVALID_PRICE','Price rounds to zero');return rounded; +export function roundPrice(price: string, szDecimals: number) { + if (!/^\d+(\.\d+)?$/.test(price) || Number(price) <= 0) + throw new AgentCliError('INVALID_PRICE', 'Price must be positive'); + const rounded = formatPrice(price, szDecimals); + if (Number(rounded) <= 0) + throw new AgentCliError('INVALID_PRICE', 'Price rounds to zero'); + return rounded; } // Bound SDK generic depth at the adapter boundary; runtime clients remain unchanged. -const Exchange = ExchangeClient as unknown as new (options: {transport:HttpTransport;wallet:Signer;nonceManager?:()=>Promise}) => ExchangeAdapter; +const Exchange = ExchangeClient as unknown as new (options: { + transport: HttpTransport; + wallet: Signer; + nonceManager?: () => Promise; +}) => ExchangeAdapter; export interface ExchangeAdapter { - order(params:Record):Promise; cancel(params:Record):Promise; cancelByCloid(params:Record):Promise; batchModify(params:Record):Promise; updateLeverage(params:Record):Promise; + order(params: Record): Promise; + cancel(params: Record): Promise; + cancelByCloid(params: Record): Promise; + batchModify(params: Record): Promise; + updateLeverage(params: Record): Promise; } -export function createClients(network:'mainnet'|'testnet',signer?:Signer,nonce?:()=>Promise) { - const transport=new HttpTransport({isTestnet:network==='testnet'});const info=new InfoClient({transport}); - return {info,meta:new MetaCache((info as unknown as {meta:()=>Promise<{universe:{name:string;szDecimals:number;maxLeverage:number}[]}>}).meta.bind(info)),exchange:signer ? new Exchange({transport,wallet:signer,nonceManager:nonce}) : undefined, - subscriptions:()=>{const transport=new WebSocketTransport({isTestnet:network==='testnet'});return {transport,client:new SubscriptionClient({transport})};}}; +const readClients = new Map< + 'mainnet' | 'testnet', + { transport: HttpTransport; info: InfoClient; meta: MetaCache } +>(); +export function createClients( + network: 'mainnet' | 'testnet', + signer?: Signer, + nonce?: () => Promise, +) { + let reads = readClients.get(network); + if (!reads) { + const transport = new HttpTransport({ isTestnet: network === 'testnet' }); + const info = new InfoClient({ transport }); + const meta = new MetaCache( + ( + info as unknown as { + meta: () => Promise<{ + universe: { + name: string; + szDecimals: number; + maxLeverage: number; + }[]; + }>; + } + ).meta.bind(info), + ); + reads = { transport, info, meta }; + readClients.set(network, reads); + } + const { transport, info, meta } = reads; + return { + info, + meta, + exchange: signer + ? new Exchange({ transport, wallet: signer, nonceManager: nonce }) + : undefined, + subscriptions: () => { + const transport = new WebSocketTransport({ + isTestnet: network === 'testnet', + }); + return { transport, client: new SubscriptionClient({ transport }) }; + }, + }; } diff --git a/src/core/http.ts b/src/core/http.ts index 6c54a49..53604eb 100644 --- a/src/core/http.ts +++ b/src/core/http.ts @@ -1,7 +1,37 @@ -import {mapNetworkError} from '../errors/mappers.js'; -export async function jsonRequest(url:string,body?:unknown,fetcher:typeof fetch=fetch){ - let response:Response;try{response=await fetcher(url,{method:body===undefined?'GET':'POST',headers:{'content-type':'application/json'},body:body===undefined?undefined:JSON.stringify(body),signal:AbortSignal.timeout(15_000)});}catch(error){throw mapNetworkError(error);} - const data:unknown=await response.json();if(!response.ok) throw {status:response.status,...(data&&typeof data==='object'?data:{message:String(data)})};return data; +import { mapNetworkError } from '../errors/mappers.js'; +export async function jsonRequest( + url: string, + body?: unknown, + fetcher: typeof fetch = fetch, +) { + let response: Response; + try { + response = await fetcher(url, { + method: body === undefined ? 'GET' : 'POST', + headers: { 'content-type': 'application/json' }, + body: body === undefined ? undefined : JSON.stringify(body), + signal: AbortSignal.timeout(15_000), + }); + } catch (error) { + throw mapNetworkError(error); + } + const data: unknown = await response.json(); + if (!response.ok) + throw { + status: response.status, + ...(data && typeof data === 'object' ? data : { message: String(data) }), + }; + return data; +} +export const hlEndpoint = (network: 'mainnet' | 'testnet') => + `https://api${network === 'testnet' ? '.hyperliquid-testnet' : '.hyperliquid'}.xyz`; +export async function hlInfo( + network: 'mainnet' | 'testnet', + type: string, + params: Record = {}, +): Promise { + return (await jsonRequest(`${hlEndpoint(network)}/info`, { + type, + ...params, + })) as T; } -export const hlEndpoint=(network:'mainnet'|'testnet')=>`https://api${network==='testnet'?'.hyperliquid-testnet':'.hyperliquid'}.xyz`; -export async function hlInfo(network:'mainnet'|'testnet',type:string,params:Record={}):Promise{return await jsonRequest(`${hlEndpoint(network)}/info`,{type,...params}) as T;} diff --git a/src/core/logger.ts b/src/core/logger.ts index 0360e6c..bebe36f 100644 --- a/src/core/logger.ts +++ b/src/core/logger.ts @@ -1,33 +1,104 @@ +import { currentRequestContext } from './request-context.js'; import { randomUUID } from 'node:crypto'; import { appendFile, mkdir, stat, rename, chmod } from 'node:fs/promises'; import { join } from 'node:path'; import { stateDir } from './config.js'; -export const levels={debug:10,info:20,warn:30,error:40}; -export type Level=keyof typeof levels; +export const levels = { debug: 10, info: 20, warn: 30, error: 40 }; +export type Level = keyof typeof levels; export function redact(value: unknown): unknown { - if (typeof value==='string') { - let safe=value;for(const [key,secret] of Object.entries(process.env)) if(/secret|token|authorization|private.?key|password/i.test(key)&&secret&&secret.length>=4) safe=safe.split(secret).join('[REDACTED]'); - return safe.replace(/(?:0x)?[a-fA-F0-9]{64,}/g,'[REDACTED]').replace(/Bearer\s+\S+/gi,'Bearer [REDACTED]');} + if (typeof value === 'string') { + let safe = value; + for (const [key, secret] of Object.entries(process.env)) + if ( + /secret|token|authorization|private.?key|password|api.?key|auth.?key|app.?key/i.test( + key, + ) && + secret && + secret.length >= 4 + ) + safe = safe.split(secret).join('[REDACTED]'); + return safe + .replace( + /-----BEGIN [^-]+-----[\s\S]*?-----END [^-]+-----/g, + '[REDACTED]', + ) + .replace(/[A-Za-z0-9+/]{80,}={0,2}/g, '[REDACTED]') + .replace(/(?:0x)?[a-fA-F0-9]{64,}/g, '[REDACTED]') + .replace(/Bearer\s+\S+/gi, 'Bearer [REDACTED]'); + } - if (Array.isArray(value)) return value.map(redact); - if (value && typeof value==='object') return Object.fromEntries(Object.entries(value).map(([key,v])=>[key,/secret|signature|token|authorization|private.?key|seed|mnemonic|password|app.?key/i.test(key) ? '[REDACTED]' : redact(v)])); - return value; + if (Array.isArray(value)) return value.map(redact); + if (value && typeof value === 'object') + return Object.fromEntries( + Object.entries(value).map(([key, v]) => [ + key, + /secret|signature|token|authorization|private.?key|seed|mnemonic|password|app.?key|api.?key|auth.?key/i.test( + key, + ) + ? '[REDACTED]' + : redact(v), + ]), + ); + return value; } export class Logger { - private queue:Promise=Promise.resolve(); - private day=new Date().toISOString().slice(0,10); - readonly requestId: string; - constructor(private options:{level?:Level;directory?:string;maxBytes?:number;requestId?:string;intentId?:string;stderr?:(line:string)=>void}={}) {this.requestId=options.requestId??randomUUID();} - log(level:Level,message:string,fields:Record={}) { - if(levels[level]process.stderr.write(line)))(line); - this.queue=this.queue.then(async()=>{ - const dir=this.options.directory??join(stateDir(),'logs');await mkdir(dir,{recursive:true,mode:0o700});await chmod(dir,0o700); - const path=join(dir,'runner.jsonl');const day=new Date().toISOString().slice(0,10); - try {const info=await stat(path);if(info.size+Buffer.byteLength(line)>(this.options.maxBytes??5_000_000)||day!==this.day) await rename(path,join(dir,`runner.${Date.now()}.${randomUUID()}.jsonl`));} catch(error) {if((error as NodeJS.ErrnoException).code!=='ENOENT') throw error;} - this.day=day;await appendFile(path,line,{mode:0o600}); - }); - } - async flush(){await this.queue;} + private queue: Promise = Promise.resolve(); + private day = new Date().toISOString().slice(0, 10); + readonly requestId: string; + constructor( + private options: { + level?: Level; + directory?: string; + maxBytes?: number; + requestId?: string; + intentId?: string; + stderr?: (line: string) => void; + } = {}, + ) { + this.requestId = + options.requestId ?? currentRequestContext()?.requestId ?? randomUUID(); + } + log(level: Level, message: string, fields: Record = {}) { + if (levels[level] < levels[this.options.level ?? 'info']) return; + const line = + JSON.stringify( + redact({ + ...fields, + time: new Date().toISOString(), + level, + message, + requestId: this.requestId, + intentId: this.options.intentId ?? currentRequestContext()?.intentId, + }), + ) + '\n'; + (this.options.stderr ?? ((line) => process.stderr.write(line)))(line); + this.queue = this.queue.then(async () => { + const dir = this.options.directory ?? join(stateDir(), 'logs'); + await mkdir(dir, { recursive: true, mode: 0o700 }); + await chmod(dir, 0o700); + const path = join(dir, 'runner.jsonl'); + const day = new Date().toISOString().slice(0, 10); + try { + const info = await stat(path); + if ( + info.size + Buffer.byteLength(line) > + (this.options.maxBytes ?? 5_000_000) || + day !== this.day || + info.mtime.toISOString().slice(0, 10) !== day + ) + await rename( + path, + join(dir, `runner.${Date.now()}.${randomUUID()}.jsonl`), + ); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + } + this.day = day; + await appendFile(path, line, { mode: 0o600 }); + await chmod(path, 0o600); + }); + } + async flush() { + await this.queue; + } } diff --git a/src/core/nonce.ts b/src/core/nonce.ts index acc326d..2b04d94 100644 --- a/src/core/nonce.ts +++ b/src/core/nonce.ts @@ -1,22 +1,47 @@ -import { mkdir,readFile,open,unlink } from 'node:fs/promises'; -import { dirname,join } from 'node:path'; -import { atomicWrite,stateDir } from './config.js'; +import { mkdir, readFile, open, unlink } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { atomicWrite, stateDir } from './config.js'; import { AgentCliError } from '../errors/index.js'; -export async function nextNonce(signer:string,directory=join(stateDir(),'nonces'),now=Date.now):Promise{ - if(!/^0x[0-9a-fA-F]{40}$/.test(signer)) throw new AgentCliError('CONFIG_INVALID','Invalid nonce signer'); - const path=join(directory,`${signer.toLowerCase()}.json`);const lock=`${path}.lock`;await mkdir(dirname(path),{recursive:true,mode:0o700}); - for(let attempt=0;attempt<100;attempt++) { - let handle; - try{handle=await open(lock,'wx',0o600);}catch(error){ - if((error as NodeJS.ErrnoException).code!=='EEXIST') throw error; - await new Promise(resolve=>setTimeout(resolve,10));continue; +export async function nextNonce( + signer: string, + directory = join(stateDir(), 'nonces'), + now = Date.now, +): Promise { + if (!/^0x[0-9a-fA-F]{40}$/.test(signer)) + throw new AgentCliError('CONFIG_INVALID', 'Invalid nonce signer'); + const path = join(directory, `${signer.toLowerCase()}.json`); + const lock = `${path}.lock`; + await mkdir(dirname(path), { recursive: true, mode: 0o700 }); + for (let attempt = 0; attempt < 100; attempt++) { + let handle; + try { + handle = await open(lock, 'wx', 0o600); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error; + await new Promise((resolve) => setTimeout(resolve, 10)); + continue; + } + try { + await handle.writeFile(String(process.pid)); + let previous = 0; + try { + previous = JSON.parse(await readFile(path, 'utf8')) as number; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') + throw new AgentCliError('CONFIG_INVALID', 'Corrupt nonce state'); + } + if (!Number.isSafeInteger(previous) || previous < 0) + throw new AgentCliError('CONFIG_INVALID', 'Invalid nonce state'); + const nonce = Math.max(now(), previous + 1); + await atomicWrite(path, JSON.stringify(nonce)); + return nonce; + } finally { + await handle.close(); + await unlink(lock); + } } - try{ - await handle.writeFile(String(process.pid));let previous=0; - try {previous=JSON.parse(await readFile(path,'utf8')) as number;}catch(error){if((error as NodeJS.ErrnoException).code!=='ENOENT') throw new AgentCliError('CONFIG_INVALID','Corrupt nonce state');} - if(!Number.isSafeInteger(previous)||previous<0) throw new AgentCliError('CONFIG_INVALID','Invalid nonce state'); - const nonce=Math.max(now(),previous+1);await atomicWrite(path,JSON.stringify(nonce));return nonce; - } finally{await handle.close();await unlink(lock);} - } - throw new AgentCliError('NONCE_COLLISION','Nonce lock busy; inspect stale lock before retrying'); + throw new AgentCliError( + 'NONCE_COLLISION', + 'Nonce lock busy; inspect stale lock before retrying', + ); } diff --git a/src/core/output-schemas.ts b/src/core/output-schemas.ts new file mode 100644 index 0000000..eb34f19 --- /dev/null +++ b/src/core/output-schemas.ts @@ -0,0 +1,247 @@ +// Stable command data shapes; unknown upstream response fields remain extensible. +const string = { type: 'string' }; +const boolean = { type: 'boolean' }; +const unknown = {}; +const object = ( + properties: Record, + required: string[] = [], +) => ({ type: 'object', properties, required }); +const array = (items: unknown) => ({ type: 'array', items }); +const balance = object( + { + accountValue: string, + accountValueUnits: string, + withdrawable: string, + marginUsed: string, + }, + ['accountValue', 'accountValueUnits', 'withdrawable', 'marginUsed'], +); +const positions = array( + object( + { + coin: string, + size: string, + entryPrice: { type: ['string', 'null'] }, + unrealizedPnl: string, + liquidationPrice: { type: ['string', 'null'] }, + leverage: string, + marginMode: string, + }, + ['coin', 'size', 'leverage'], + ), +); +const event = object( + { + seq: { type: 'integer', minimum: 1 }, + timestamp: { type: 'string', format: 'date-time' }, + type: { + enum: [ + 'fill', + 'order_update', + 'position_change', + 'deposit', + 'error', + 'agent_expiry_warning', + 'runner_state', + ], + }, + data: unknown, + }, + ['seq', 'timestamp', 'type', 'data'], +); +export const outputSchemas: Record = { + market: object( + { + coin: string, + asset: string, + szDecimals: string, + maxLeverage: string, + mid: string, + }, + ['coin', 'asset', 'szDecimals', 'maxLeverage', 'mid'], + ), + config: unknown, + init: object( + { + masterAddress: string, + agentAddress: string, + expiry: { type: 'number' }, + policyId: string, + approved: boolean, + requiresSignature: boolean, + approvals: unknown, + deeplink: string, + qrAscii: { type: ['string', 'null'] }, + }, + ['masterAddress', 'agentAddress', 'expiry', 'policyId', 'approved'], + ), + fund: object( + { + depositId: string, + depositAddress: string, + originChain: { type: 'number' }, + originCurrency: string, + minDeposit: string, + expectedReceived: string, + expectedReceivedHuman: string, + fees: unknown, + builderFeeRate: string, + expiresAt: { type: 'number' }, + state: string, + signatureSteps: array(unknown), + qrAscii: { type: ['string', 'null'] }, + qrPath: { type: ['string', 'null'] }, + }, + [ + 'depositId', + 'depositAddress', + 'expectedReceived', + 'fees', + 'builderFeeRate', + 'expiresAt', + 'qrAscii', + ], + ), + deposit: object({ + depositId: string, + steps: array(unknown), + complete: boolean, + }), + balance, + positions, + orders: array( + object( + { + coin: string, + oid: string, + sz: string, + limitPx: string, + side: { enum: ['A', 'B'] }, + cloid: string, + }, + ['coin', 'oid', 'sz', 'limitPx', 'side'], + ), + ), + status: object( + { + balance, + positions, + agent: object({ + address: string, + approved: boolean, + revoked: boolean, + expiry: { type: ['number', 'null'] }, + daysRemaining: { type: ['string', 'null'] }, + warnings: array(string), + }), + policy: object({ id: { type: ['string', 'null'] }, version: string }), + deposits: array( + object({ + id: string, + state: { + enum: [ + 'awaiting_funds', + 'pending', + 'settled', + 'failed', + 'refunded', + ], + }, + }), + ), + approvedMaxBuilderFee: string, + runner: { type: ['object', 'null'] }, + }, + [ + 'balance', + 'positions', + 'agent', + 'policy', + 'deposits', + 'approvedMaxBuilderFee', + 'runner', + ], + ), + order: object( + { + result: unknown, + builderFeeRate: string, + cloid: { type: ['string', 'null'] }, + }, + ['result', 'builderFeeRate', 'cloid'], + ), + cancel: object({ cancelled: array(string), result: unknown }, [ + 'cancelled', + 'result', + ]), + modify: object({ status: string, response: unknown }, ['status']), + leverage: object({ status: string, response: unknown }, ['status']), + run: object( + { + pid: { type: 'integer' }, + startedAt: { type: 'number' }, + network: { enum: ['mainnet', 'testnet'] }, + ready: boolean, + existing: boolean, + }, + ['pid', 'startedAt', 'network'], + ), + events: array(event), + logs: { + description: 'JSON-line stream of redacted log objects', + type: 'object', + }, + rotate: object( + { + oldAgent: string, + newAgent: string, + swapped: boolean, + oldRevoked: boolean, + requiresSignature: boolean, + approval: unknown, + revokeApproval: unknown, + }, + ['oldAgent', 'newAgent', 'swapped', 'oldRevoked'], + ), + revoke: object( + { + agentAddress: string, + locallyRevoked: boolean, + exchangeRevoked: boolean, + requiresSignature: boolean, + approval: unknown, + }, + [ + 'agentAddress', + 'locallyRevoked', + 'exchangeRevoked', + 'requiresSignature', + 'approval', + ], + ), + audit: array( + object( + { + seq: { type: 'integer' }, + timestamp: string, + intentId: string, + requestId: string, + cloid: string, + stage: string, + data: unknown, + previousHash: string, + hash: string, + }, + ['seq', 'timestamp', 'intentId', 'stage', 'data', 'previousHash', 'hash'], + ), + ), + skill: object( + { + target: { enum: ['claude', 'codex'] }, + path: string, + version: string, + changed: boolean, + }, + ['target', 'path', 'version', 'changed'], + ), +}; diff --git a/src/core/request-context.ts b/src/core/request-context.ts new file mode 100644 index 0000000..471f054 --- /dev/null +++ b/src/core/request-context.ts @@ -0,0 +1,7 @@ +import { AsyncLocalStorage } from 'node:async_hooks'; +export const requestContext = new AsyncLocalStorage<{ + intentId: string; + cloid?: string; + requestId?: string; +}>(); +export const currentRequestContext = () => requestContext.getStore(); diff --git a/src/core/retry.ts b/src/core/retry.ts index a3bff84..91c4b98 100644 --- a/src/core/retry.ts +++ b/src/core/retry.ts @@ -1,13 +1,32 @@ import { normalizeError } from '../errors/index.js'; -export async function retry(operation:()=>Promise,options:{attempts?:number;baseMs?:number;maxMs?:number;random?:()=>number;sleep?:(ms:number)=>Promise}={}) { - const attempts=options.attempts??3; - if(!Number.isInteger(attempts)||attempts<1) throw new RangeError('attempts must be positive'); - for(let attempt=0;;attempt++) { - try{return await operation();}catch(error){ - const normalized=normalizeError(error);if(!normalized.retryable||attempt+1>=attempts) throw normalized; - const base=Math.min(options.maxMs??30_000,(options.baseMs??250)*2**attempt); - const jitter=0.5+(options.random??Math.random)(); - await (options.sleep??(ms=>new Promise(resolve=>setTimeout(resolve,ms))))(Math.min(options.maxMs??30_000,base*jitter)); +export async function retry( + operation: () => Promise, + options: { + attempts?: number; + baseMs?: number; + maxMs?: number; + random?: () => number; + sleep?: (ms: number) => Promise; + } = {}, +) { + const attempts = options.attempts ?? 3; + if (!Number.isInteger(attempts) || attempts < 1) + throw new RangeError('attempts must be positive'); + for (let attempt = 0; ; attempt++) { + try { + return await operation(); + } catch (error) { + const normalized = normalizeError(error); + if (!normalized.retryable || attempt + 1 >= attempts) throw normalized; + const base = Math.min( + options.maxMs ?? 30_000, + (options.baseMs ?? 250) * 2 ** attempt, + ); + const jitter = 0.5 + (options.random ?? Math.random)(); + await ( + options.sleep ?? + ((ms) => new Promise((resolve) => setTimeout(resolve, ms))) + )(Math.min(options.maxMs ?? 30_000, base * jitter)); + } } - } } diff --git a/src/core/risk.ts b/src/core/risk.ts index 59f2de2..c81c73d 100644 --- a/src/core/risk.ts +++ b/src/core/risk.ts @@ -1,21 +1,101 @@ -import type {Config} from './config.js'; -import {AgentCliError} from '../errors/index.js'; -import {parseUsdc} from '../funding/amounts.js'; -export interface RiskIntent {coin:string;size:string;price:string;side:'buy'|'sell';leverage:number;reduceOnly?:boolean;} -export interface RiskState {positionSize:string;dailyLoss:string;openOrders:number;marketMaxLeverage:number;pendingSize?:string;} -const scale=10n**18n; -const decimal=(value:string)=>value.startsWith('-')?-parseUsdc(value.slice(1),18):parseUsdc(value,18); -const absolute=(value:bigint)=>value<0n?-value:value; -export function evaluateRisk(config:Config,intent:RiskIntent,state:RiskState){ - const fail=(limit:string,actual:unknown,maximum:unknown):never=>{throw new AgentCliError('POLICY_REJECTED',`Risk limit: ${limit}`,{limit,actual,maximum});}; - if(config.allowedMarkets.length&&!config.allowedMarkets.includes(intent.coin)) fail('allowedMarkets',intent.coin,config.allowedMarkets); - if(!Number.isInteger(intent.leverage)||intent.leverage<1||intent.leverage>Math.min(config.limits.maxLeverage??Infinity,state.marketMaxLeverage)) fail('maxLeverage',intent.leverage,Math.min(config.limits.maxLeverage??Infinity,state.marketMaxLeverage)); - const size=decimal(intent.size),price=decimal(intent.price);if(size<=0n||price<=0n) fail('positive size/price',intent.size,intent.price); - const notional=size*price/scale;const signed=intent.side==='buy'?size:-size;const position=decimal(state.positionSize); - if(intent.reduceOnly&&((position===0n)||(position>0n&&signed>0n)||(position<0n&&signed<0n))) fail('reduceOnly',intent.side,state.positionSize); - const projected=intent.reduceOnly?absolute(position)- (size>absolute(position)?absolute(position):size):absolute(position+signed+decimal(state.pendingSize??'0')); - const comparisons:[string,bigint,number|undefined][]=[['maxOrderSize',size,config.limits.maxOrderSize],['maxNotional',notional,config.limits.maxNotional],['maxPositionNotional',projected*price/scale,config.limits.maxPositionNotional],['dailyLoss',decimal(state.dailyLoss),config.limits.dailyLoss]]; - for(const [name,actual,maximum] of comparisons) if(maximum!==undefined&&actual>decimal(String(maximum))) fail(name,actual.toString(),maximum); - if(config.limits.maxOpenOrders!==undefined&&state.openOrders>=config.limits.maxOpenOrders) fail('maxOpenOrders',state.openOrders,config.limits.maxOpenOrders); - return {allowed:true,notional: notional.toString()}; +import type { Config } from './config.js'; +import { AgentCliError } from '../errors/index.js'; +import { parseUsdc } from '../funding/amounts.js'; +export interface RiskIntent { + coin: string; + size: string; + price: string; + side: 'buy' | 'sell'; + leverage: number; + reduceOnly?: boolean; +} +export interface RiskState { + positionSize: string; + dailyLoss: string; + openOrders: number; + marketMaxLeverage: number; + pendingSize?: string; + pendingBuySize?: string; + pendingSellSize?: string; +} +const scale = 10n ** 18n; +const decimal = (value: string) => + value.startsWith('-') ? -parseUsdc(value.slice(1), 18) : parseUsdc(value, 18); +const absolute = (value: bigint) => (value < 0n ? -value : value); +export function evaluateRisk( + config: Config, + intent: RiskIntent, + state: RiskState, +) { + const fail = (limit: string, actual: unknown, maximum: unknown): never => { + throw new AgentCliError('POLICY_REJECTED', `Risk limit: ${limit}`, { + limit, + actual, + maximum, + }); + }; + if ( + config.allowedMarkets.length && + !config.allowedMarkets.includes(intent.coin) + ) + fail('allowedMarkets', intent.coin, config.allowedMarkets); + if ( + !Number.isInteger(intent.leverage) || + intent.leverage < 1 || + intent.leverage > + Math.min(config.limits.maxLeverage ?? Infinity, state.marketMaxLeverage) + ) + fail( + 'maxLeverage', + intent.leverage, + Math.min(config.limits.maxLeverage ?? Infinity, state.marketMaxLeverage), + ); + const size = decimal(intent.size), + price = decimal(intent.price); + if (size <= 0n || price <= 0n) + fail('positive size/price', intent.size, intent.price); + const notional = (size * price) / scale; + const signed = intent.side === 'buy' ? size : -size; + const position = decimal(state.positionSize); + if ( + intent.reduceOnly && + (position === 0n || + (position > 0n && signed > 0n) || + (position < 0n && signed < 0n)) + ) + fail('reduceOnly', intent.side, state.positionSize); + const effective = intent.reduceOnly + ? (position > 0n ? 1n : -1n) * + (absolute(position) - + (size > absolute(position) ? absolute(position) : size)) + : position + signed; + const legacy = decimal(state.pendingSize ?? '0'); + const buys = decimal( + state.pendingBuySize ?? (legacy > 0n ? state.pendingSize! : '0'), + ); + const sells = decimal( + state.pendingSellSize ?? (legacy < 0n ? state.pendingSize!.slice(1) : '0'), + ); + const long = absolute(effective + buys), + short = absolute(effective - sells); + const projected = long > short ? long : short; + const comparisons: [string, bigint, number | undefined][] = [ + ['maxOrderSize', size, config.limits.maxOrderSize], + ['maxNotional', notional, config.limits.maxNotional], + [ + 'maxPositionNotional', + (projected * price) / scale, + config.limits.maxPositionNotional, + ], + ['dailyLoss', decimal(state.dailyLoss), config.limits.dailyLoss], + ]; + for (const [name, actual, maximum] of comparisons) + if (maximum !== undefined && actual > decimal(String(maximum))) + fail(name, actual.toString(), maximum); + if ( + config.limits.maxOpenOrders !== undefined && + state.openOrders >= config.limits.maxOpenOrders + ) + fail('maxOpenOrders', state.openOrders, config.limits.maxOpenOrders); + return { allowed: true, notional: notional.toString() }; } diff --git a/src/core/router.ts b/src/core/router.ts index 0272fa3..38cf556 100644 --- a/src/core/router.ts +++ b/src/core/router.ts @@ -1,54 +1,207 @@ -import {assertSafeInput} from '../wallet/safety.js'; -import {auditedAction} from './audit.js'; +import { outputSchemas } from './output-schemas.js'; +import { errorDefaults } from '../errors/index.js'; +import { assertSafeInput } from '../wallet/safety.js'; +import { auditedAction } from './audit.js'; import { z } from 'zod'; import { AgentCliError, normalizeError, success } from '../errors/index.js'; -export interface Context { configPath?: string; network?: 'mainnet' | 'testnet'; pretty: boolean; runner?: boolean; streaming?:boolean; } -export interface Command { description: string; schema: z.ZodType; execute: (input: Record, context: Context) => Promise; } -export let routing:((name:string,input:Record,context:Context)=>Promise<{routed:boolean;data?:unknown}>)|undefined; -export function setRouting(value:typeof routing){routing=value;} +export interface Context { + configPath?: string; + network?: 'mainnet' | 'testnet'; + pretty: boolean; + runner?: boolean; + streaming?: boolean; +} +export interface Command { + description: string; + schema: z.ZodType; + execute: ( + input: Record, + context: Context, + ) => Promise; +} +export let routing: + | (( + name: string, + input: Record, + context: Context, + ) => Promise<{ routed: boolean; data?: unknown }>) + | undefined; +export function setRouting(value: typeof routing) { + routing = value; +} export const commands = new Map(); export function register(name: string, command: Command) { - if(['order','cancel','modify','leverage','rotate','revoke'].includes(name)){const original=command.execute;command={...command,schema:command.schema instanceof z.ZodObject?command.schema.extend({intentId:z.string().optional(),intent:z.string().optional()}):command.schema,execute:(input,context)=>auditedAction(name,input,()=>original(input,context))};} - commands.set(name,command); + if (command.schema instanceof z.ZodObject) + command = { ...command, schema: command.schema.strict() }; + if ( + ['order', 'cancel', 'modify', 'leverage', 'rotate', 'revoke'].includes(name) + ) { + const original = command.execute; + command = { + ...command, + schema: + command.schema instanceof z.ZodObject + ? command.schema.extend({ + intentId: z.string().optional(), + intent: z.string().optional(), + }) + : command.schema, + execute: (input, context) => + auditedAction(name, input, () => original(input, context)), + }; + } + commands.set(name, command); } export function schemas(name: string) { - const command = commands.get(name); - if (!command) throw new AgentCliError('CONFIG_INVALID', `Unknown command: ${name}`); - return { input: z.toJSONSchema(command.schema), output: {oneOf:[{type:'object',required:['ok','data'],properties:{ok:{const:true},data:{}}},{type:'object',required:['ok','code','message','retryable','details'],properties:{ok:{const:false},code:{type:'string'},message:{type:'string'},retryable:{type:'boolean'},details:{}}}]} }; + const command = commands.get(name); + if (!command) + throw new AgentCliError('CONFIG_INVALID', `Unknown command: ${name}`); + return { + input: z.toJSONSchema(command.schema), + output: { + oneOf: [ + { + type: 'object', + required: ['ok', 'data'], + properties: { ok: { const: true }, data: outputSchemas[name] ?? {} }, + }, + { + type: 'object', + required: ['ok', 'code', 'message', 'retryable', 'details'], + properties: { + ok: { const: false }, + code: { enum: Object.keys(errorDefaults) }, + message: { type: 'string' }, + retryable: { type: 'boolean' }, + details: {}, + }, + }, + ], + }, + }; +} +function parseJsonObject(value: string) { + const parsed: unknown = JSON.parse(value); + assertSafeInput(parsed); + return z.record(z.string(), z.unknown()).parse(parsed); +} +function parseValue(value: string): unknown { + try { + return JSON.parse(value); + } catch { + return value; + } } -function parseValue(value: string): unknown { try { return JSON.parse(value); } catch { return value; } } export async function dispatch(argv: string[], stdin = '') { - const input: Record = {}; const positional: string[] = []; - const context: Context = { pretty: false }; - try { - for (let i=0;ic.toUpperCase())]=false; continue; } - if (arg.startsWith('--')) { - const [flag,inline] = arg.slice(2).split('=',2); const key=flag.replace(/-([a-z])/g,(_,c: string)=>c.toUpperCase()); - const raw=inline !== undefined ? inline : argv[i+1] && !argv[i+1].startsWith('--') ? argv[++i] : undefined; - input[key]=raw===undefined?true:['size','price','amount','triggerPrice','signature','builderSignature','cloid'].includes(key)?raw:parseValue(raw);continue; - } - positional.push(arg); + const input = Object.create(null) as Record; + const positional: string[] = []; + const context: Context = { pretty: false }; + try { + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + if (arg === '--pretty') { + context.pretty = true; + continue; + } + if (arg === '--help' || arg === '-h') { + positional.splice(0, positional.length, 'help'); + continue; + } + if (arg === '--config') { + context.configPath = argv[++i]; + if (!context.configPath) + throw new AgentCliError('CONFIG_INVALID', '--config requires a path'); + continue; + } + if (arg === '--network') { + context.network = z.enum(['mainnet', 'testnet']).parse(argv[++i]); + continue; + } + if (arg === '--json') { + Object.assign(input, parseJsonObject(argv[++i])); + continue; + } + if (arg.startsWith('--no-')) { + input[ + arg.slice(5).replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()) + ] = false; + continue; + } + if (arg.startsWith('--')) { + const [flag, inline] = arg.slice(2).split('=', 2); + const key = flag.replace(/-([a-z])/g, (_, c: string) => + c.toUpperCase(), + ); + const raw = + inline !== undefined + ? inline + : argv[i + 1] && !argv[i + 1].startsWith('--') + ? argv[++i] + : undefined; + input[key] = + raw === undefined + ? true + : [ + 'size', + 'price', + 'amount', + 'triggerPrice', + 'signature', + 'builderSignature', + 'cloid', + ].includes(key) + ? raw + : parseValue(raw); + continue; + } + positional.push(arg); + } + if (stdin.trim()) Object.assign(input, parseJsonObject(stdin)); + const [name = 'help', ...args] = positional; + if (name === 'help') + return { + body: success({ + commands: [...commands].map(([name, c]) => ({ + name, + description: c.description, + })), + globalFlags: ['--json', '--config', '--network', '--pretty'], + }), + exitCode: 0, + context, + }; + if (name === 'schema') + return { body: success(schemas(args[0])), exitCode: 0, context }; + const command = commands.get(name); + if (!command) + throw new AgentCliError('CONFIG_INVALID', `Unknown command: ${name}`); + if (args.length) input.args = args; + assertSafeInput(input); + const parsed = command.schema.safeParse(input); + if (!parsed.success) + throw new AgentCliError( + 'CONFIG_INVALID', + 'Invalid command input', + parsed.error.flatten(), + ); + const route = await routing?.( + name, + parsed.data as Record, + context, + ); + const data = route?.routed + ? route.data + : await command.execute(parsed.data as Record, context); + return { body: success(data), exitCode: 0, context }; + } catch (error) { + const normalized = + error instanceof z.ZodError || error instanceof SyntaxError + ? new AgentCliError('CONFIG_INVALID', 'Invalid JSON or flags') + : normalizeError(error); + return { + body: normalized.toJSON(), + exitCode: normalized.exitCode, + context, + }; } - if (stdin.trim()) Object.assign(input,z.record(z.string(),z.unknown()).parse(JSON.parse(stdin))); - const [name='help', ...args]=positional; - if (name === 'help') return {body:success({commands:[...commands].map(([name,c])=>({name,description:c.description})),globalFlags:['--json','--config','--network','--pretty']}),exitCode:0,context}; - if (name === 'schema') return {body:success(schemas(args[0])),exitCode:0,context}; - const command=commands.get(name); if (!command) throw new AgentCliError('CONFIG_INVALID',`Unknown command: ${name}`); - if (args.length) input.args=args; - assertSafeInput(input);const parsed=command.schema.safeParse(input); - if (!parsed.success) throw new AgentCliError('CONFIG_INVALID','Invalid command input',parsed.error.flatten()); - const route=await routing?.(name,parsed.data as Record,context); - const data=route?.routed?route.data:await command.execute(parsed.data as Record,context); - return {body:success(data),exitCode:0,context}; - } catch (error) { - const normalized=error instanceof z.ZodError || error instanceof SyntaxError ? new AgentCliError('CONFIG_INVALID','Invalid JSON or flags') : normalizeError(error); - return {body:normalized.toJSON(),exitCode:normalized.exitCode,context}; - } } diff --git a/src/core/skill-content.ts b/src/core/skill-content.ts new file mode 100644 index 0000000..d4519e9 --- /dev/null +++ b/src/core/skill-content.ts @@ -0,0 +1,3 @@ +// Generated from SKILL.md and package.json. +export const cliVersion="0.1.0"; +export const skillContent="---\nname: agent-cli\ndescription: Operate the agent-cli Hyperliquid trading CLI, including managed-agent onboarding, funding, orders, event monitoring, rotation, and normalized error recovery.\nmetadata:\n version: \"0.1.0\"\n---\n\nUse `agent-cli` for the user's authorized Hyperliquid account operations. Obtain\ncommand schemas with `agent-cli schema COMMAND`; arguments can be flags or\n`--json '{...}'`, or a JSON object on stdin. Ordinary commands print exactly one\nJSON object: `{ok:true,data}` or `{ok:false,code,message,retryable,details}`.\nLogs/QR appear on stderr. `events --follow` and `logs` explicitly stream JSON lines.\nUse `--network testnet` during testing; never silently change to mainnet.\n\n## Onboarding sequence\n\n1. Configure Privy app ID and secret reference, e.g. `config set privyAppId APP`\n and `config set privyAppSecretRef env:BLOXWAP_PRIVY_APP_SECRET`. The user sets\n the environment secret outside the conversation. Configure published builder\n address/fee if applicable; unset defaults charge no builder fee.\n2. Run `init --master-address 0x... --network testnet`. Show the returned approval\n payload/deeplink/QR. The user signs in their master wallet; submit only the\n resulting signature with `init --signature 0x...`. Submit builder approval\n separately if requested. Continue only after `approved:true`.\n3. Run `fund --amount 10 --origin-chain 42161`. Show `qrAscii`/deposit address,\n origin chain/token, minimum, fees, expected received amount and expiry. On\n testnet, do not treat the default Arbitrum mainnet chain/token as a test route:\n use an explicitly supported test chain/token and `originDecimals`, or the\n credential-gated CI harness with mocked Relay and pre-funded test collateral.\n If no test route is known, stop funding and report the missing route. The user\n transfers the quoted origin token. If `signatureSteps` require signatures,\n show the exact payload and submit each via `deposit` in returned order.\n4. Poll `status` until each active deposit is `settled`, for at most 12 polls\n or 5 minutes, whichever comes first. Start with 1-second delays, double to a\n 15-second cap, and stop with an explicit pending report if the budget expires.\n Settlement requires Relay success and Hyperliquid balance receipt. Stop on\n failed/refunded deposits and explain their status. Do not place orders while\n any deposit is awaiting funds or pending.\n5. Run `run --detach`, then monitor `events --follow --since SEQ`. Persist the\n last handled sequence; replay after reconnect. An unfilled or rejected order\n remains incomplete: do not claim the requested trade succeeded. A fill updates the trading\n plan, an error follows the catalog below, and expiry warnings prompt rotation\n using external master-wallet approval.\n\n## Commands and example envelopes\n\nExamples show representative `data`, not live balances or service promises.\nFull inputs/outputs are discoverable with `schema`; amounts/prices are strings.\n\n| Input | Example output |\n| --- | --- |\n| `--help` | `{\"ok\":true,\"data\":{\"commands\":[{\"name\":\"status\",\"description\":\"Read account status\"}],\"globalFlags\":[\"--json\",\"--config\",\"--network\",\"--pretty\"]}}` |\n| `schema order` | `{\"ok\":true,\"data\":{\"input\":{\"type\":\"object\"},\"output\":{\"oneOf\":[]}}}` (actual schemas include properties) |\n| `config get network` | `{\"ok\":true,\"data\":\"testnet\"}` |\n| `config set limits.maxLeverage 5` | `{\"ok\":true,\"data\":{\"version\":1,\"limits\":{\"maxLeverage\":5}}}` |\n| `config validate` | `{\"ok\":true,\"data\":{\"valid\":true,\"version\":1}}` |\n| `init --master-address 0x...` | `{\"ok\":true,\"data\":{\"approved\":false,\"requiresSignature\":true,\"approvals\":{\"agent\":{\"typedData\":{}}},\"qrAscii\":\"...\"}}` |\n| `fund --amount 10 --no-qr` | `{\"ok\":true,\"data\":{\"depositId\":\"...\",\"depositAddress\":\"0x...\",\"expectedReceived\":\"100000000\",\"builderFeeRate\":\"0%\",\"state\":\"awaiting_funds\",\"qrAscii\":null}}` |\n| `deposit --deposit-id ID` | `{\"ok\":true,\"data\":{\"depositId\":\"ID\",\"steps\":[{\"id\":\"step:0\",\"requiresSignature\":true,\"payload\":{}}]}}` |\n| `deposit --deposit-id ID --step-id step:0 --signature 0x...` | `{\"ok\":true,\"data\":{\"depositId\":\"ID\",\"steps\":[]}}` |\n| `status` | `{\"ok\":true,\"data\":{\"balance\":{\"accountValue\":\"100\"},\"agent\":{\"approved\":true,\"daysRemaining\":\"179\"},\"deposits\":[],\"policy\":{\"id\":\"...\",\"version\":\"1\"},\"runner\":{\"pid\":\"123\"}}}` |\n| `balance` | `{\"ok\":true,\"data\":{\"accountValue\":\"100\",\"accountValueUnits\":\"10000000000\",\"withdrawable\":\"96\",\"marginUsed\":\"4\"}}` |\n| `market --coin BTC` | `{\"ok\":true,\"data\":{\"coin\":\"BTC\",\"asset\":\"0\",\"szDecimals\":\"5\",\"maxLeverage\":\"40\",\"mid\":\"20000\"}}` |\n| `positions` | `{\"ok\":true,\"data\":[{\"coin\":\"BTC\",\"size\":\"0.001\",\"entryPrice\":\"20000\",\"leverage\":\"5\",\"marginMode\":\"cross\"}]}` |\n| `orders` | `{\"ok\":true,\"data\":[{\"coin\":\"BTC\",\"oid\":\"123\",\"sz\":\"0.001\",\"limitPx\":\"20000\",\"side\":\"B\"}]}` |\n| `order --json '{\"coin\":\"BTC\",\"side\":\"buy\",\"size\":\"0.001\",\"price\":\"20000\",\"tif\":\"Gtc\",\"cloid\":\"0x11111111111111111111111111111111\"}'` | `{\"ok\":true,\"data\":{\"result\":{\"status\":\"ok\",\"response\":{\"data\":{\"statuses\":[{\"resting\":{\"oid\":123}}]}}},\"builderFeeRate\":\"0%\",\"cloid\":\"0x...\"}}` |\n| `order --json '{\"coin\":\"BTC\",\"side\":\"sell\",\"size\":\"0.001\",\"type\":\"market\",\"slippage\":0.01,\"reduceOnly\":true}'` | `{\"ok\":true,\"data\":{\"result\":{\"status\":\"ok\",\"response\":{\"data\":{\"statuses\":[{\"filled\":{\"oid\":123}}]}}}}}` |\n| `cancel --oid 123` or `cancel --all` | `{\"ok\":true,\"data\":{\"cancelled\":[\"123\"],\"result\":{\"status\":\"ok\"}}}` |\n| `modify --json '{\"modifications\":[{\"oid\":123,\"price\":\"20100\",\"size\":\"0.001\"}]}'` | `{\"ok\":true,\"data\":{\"status\":\"ok\"}}` |\n| `leverage --coin BTC --leverage 5 --mode isolated` | `{\"ok\":true,\"data\":{\"status\":\"ok\"}}` |\n| `run --detach` | `{\"ok\":true,\"data\":{\"pid\":123,\"startedAt\":1700000000000,\"network\":\"testnet\"}}` |\n| `events --since 12 --types '[\"fill\"]'` | `{\"ok\":true,\"data\":[{\"seq\":13,\"timestamp\":\"2026-01-01T00:00:00.000Z\",\"type\":\"fill\",\"data\":{\"tid\":1}}]}` |\n| `events --follow` | `{\"seq\":13,\"timestamp\":\"2026-01-01T00:00:00.000Z\",\"type\":\"fill\",\"data\":{\"tid\":1}}` (one line per event) |\n| `logs --follow --level warn` | `{\"level\":\"warn\",\"requestId\":\"...\",\"message\":\"Risk rejected\",\"time\":\"...\"}` (JSON lines) |\n| `rotate` then `rotate --signature 0x...` | `{\"ok\":true,\"data\":{\"oldAgent\":\"0x...\",\"newAgent\":\"0x...\",\"swapped\":true,\"oldRevoked\":false,\"revokeApproval\":{}}}` |\n| `rotate --revoke-signature 0x...` | `{\"ok\":true,\"data\":{\"swapped\":true,\"oldRevoked\":true}}` |\n| `revoke --cancel-orders` | `{\"ok\":true,\"data\":{\"locallyRevoked\":true,\"exchangeRevoked\":false,\"requiresSignature\":true,\"approval\":{}}}` |\n| `revoke --signature 0x...` | `{\"ok\":true,\"data\":{\"locallyRevoked\":true,\"exchangeRevoked\":true}}` |\n| `audit --intent ID` | `{\"ok\":true,\"data\":[{\"intentId\":\"ID\",\"stage\":\"decision\",\"data\":{\"allowed\":true},\"previousHash\":\"...\",\"hash\":\"...\"}]}` |\n| `skill install --target codex` | `{\"ok\":true,\"data\":{\"target\":\"codex\",\"path\":\".../agent-cli/SKILL.md\",\"version\":\"0.1.0\"}}` |\n\nBefore an order, query `market --coin BTC` for `mid`, `szDecimals`, asset and\nmarket leverage ceiling. Compute size/notional with decimal strings/bigints,\nusing the user-approved limit or the bounded IOC execution price. A valid schema\nalone does not establish that an order is tradable or within the budget.\n\nUse `--intent-id ID --intent \"reason\"` on trading actions to connect the user's\nreasoning to the audit. Keep cloid stable when retrying a known order intent.\nFor TP/SL use `type:\"tp\"|\"sl\"` and `triggerPrice` with size/price/reduceOnly;\nmarket orders use an explicit slippage-bounded IOC, not an unbounded market order.\n\n## Error recovery\n\n| Code | Action |\n| --- | --- |\n| RATE_LIMIT, NETWORK_ERROR | At most 3 read retries with exponential backoff; for uncertain mutations, reconcile order state/cloid before retrying. Stop and report unresolved results |\n| NONCE_COLLISION | Retry using a newly issued nonce; inspect a stale lock if persistent |\n| DEPOSIT_PENDING | Poll status until settled; stop on terminal funding failure |\n| RUNNER_NOT_RUNNING | Start runner or use configured direct fallback |\n| INSUFFICIENT_MARGIN | Reduce exposure or request authorized funding |\n| POLICY_REJECTED | Stop and revise intent within limits; do not bypass policy |\n| AGENT_EXPIRED | Rotate with master-wallet approval |\n| AGENT_NOT_APPROVED | Complete init/approval or explain revocation |\n| INVALID_SIZE, INVALID_PRICE | Correct input using market precision |\n| UNKNOWN_MARKET | Refresh metadata; use an allowed market |\n| ORDER_NOT_FOUND | Refresh orders before changing intent |\n| CONFIG_INVALID | Correct configuration/input; never insert secrets |\n| UPSTREAM_ERROR, INTERNAL | Stop, inspect redacted details/logs and report failure |\n\n## Gotchas\n\nHyperCore perps USDC uses **8 decimals**; EVM USDC uses **6**. Preserve strings and\nbigints. Some deposits require **two distinct signatures**; resume stored steps\nrather than restarting the route. Agent expiry is at most **180 days**. Never\nask for, paste, log or import a master private key or seed. The managed agent is\ntrade-only; local limits include markets, size/notional, leverage, daily loss and\nopen orders. Privy sees an L1 action hash, so it cannot independently inspect\nnotional/action contents; Hyperliquid agent permissions and local checks enforce\nthose boundaries. State is always `~/.bloxwap`, env prefix `BLOXWAP_`.\n"; diff --git a/src/core/trading.ts b/src/core/trading.ts index 4d0e9bb..1bd0e3c 100644 --- a/src/core/trading.ts +++ b/src/core/trading.ts @@ -1,41 +1,156 @@ -import {decisionAudit} from './audit.js'; -import {assertAgentValid} from '../wallet/expiry.js'; -import {activeRunner} from '../runner/lifecycle.js'; -import {randomUUID} from 'node:crypto'; -import {type Config} from './config.js'; -import {createClients,type ExchangeAdapter,type Market} from './hlclient.js'; -import {privyClient,privySigner} from '../wallet/privy.js'; -import {nextNonce} from './nonce.js'; -import {hlInfo} from './http.js'; -import {type AccountState,type OpenOrder} from '../commands/read.js'; -import {AgentCliError} from '../errors/index.js'; -import {assertDepositsSettled} from '../funding/status.js'; -import {assertTradeAction} from '../wallet/policy.js'; -import {mapHyperliquidError} from '../errors/mappers.js'; -import {evaluateRisk,type RiskIntent} from './risk.js'; -import {Logger} from './logger.js'; -export interface TradingServices {exchange:ExchangeAdapter;market:(coin:string)=>Promise;state:()=>Promise;orders:()=>Promise;mids:()=>Promise>;} -export async function tradingServices(config:Config):Promise{ - const signer=activeRunner?.signer??await privySigner(config,await privyClient(config));const clients=createClients(config.network,signer,()=>nextNonce(signer.address)); - return {exchange:clients.exchange!,market:coin=>clients.meta.lookup(coin),state:()=>hlInfo(config.network,'clearinghouseState',{user:config.masterAddress}),orders:()=>hlInfo(config.network,'frontendOpenOrders',{user:config.masterAddress}),mids:()=>hlInfo(config.network,'allMids')}; +import { parseUsdc, formatUsdc } from '../funding/amounts.js'; +import { decisionAudit } from './audit.js'; +import { assertAgentValid } from '../wallet/expiry.js'; +import { activeRunner } from '../runner/lifecycle.js'; +import { type Config } from './config.js'; +import { + createClients, + type ExchangeAdapter, + type Market, +} from './hlclient.js'; +import { privyClient, privySigner } from '../wallet/privy.js'; +import { nextNonce } from './nonce.js'; +import { hlInfo } from './http.js'; +import { type AccountState, type OpenOrder } from '../commands/read.js'; +import { AgentCliError } from '../errors/index.js'; +import { assertDepositsSettled } from '../funding/status.js'; +import { assertTradeAction } from '../wallet/policy.js'; +import { mapHyperliquidError } from '../errors/mappers.js'; +import { evaluateRisk, type RiskIntent } from './risk.js'; +import { Logger } from './logger.js'; +export interface TradingServices { + exchange: ExchangeAdapter; + market: (coin: string) => Promise; + state: () => Promise; + orders: () => Promise; + mids: () => Promise>; + dailyLoss?: () => Promise; } -export async function assertCanTrade(config:Config){ - if(config.revoked||!config.approved||!config.masterAddress) throw new AgentCliError('AGENT_NOT_APPROVED','Agent approval required'); - assertAgentValid(config); - await assertDepositsSettled(config.network,config.masterAddress); +export async function tradingServices( + config: Config, +): Promise { + const signer = + activeRunner?.signer ?? + (await privySigner(config, await privyClient(config))); + const clients = createClients(config.network, signer, () => + nextNonce(signer.address), + ); + return { + exchange: clients.exchange!, + market: (coin) => clients.meta.lookup(coin), + state: () => + hlInfo(config.network, 'clearinghouseState', { + user: config.masterAddress, + }), + orders: () => + hlInfo(config.network, 'frontendOpenOrders', { + user: config.masterAddress, + }), + mids: () => hlInfo(config.network, 'allMids'), + dailyLoss: async () => + dailyLossFromPortfolio( + await hlInfo(config.network, 'portfolio', { + user: config.masterAddress, + }), + ), + }; } -export async function checkRisk(config:Config,intent:RiskIntent,services:TradingServices,ignoreOid?:number){ - const [market,state,orders]=await Promise.all([services.market(intent.coin),services.state(),services.orders()]); - const position=state.assetPositions.find(p=>p.position.coin===intent.coin)?.position; - const open=orders.filter(o=>o.oid!==ignoreOid);const pending=open.filter(o=>o.coin===intent.coin).reduce((sum,o)=>sum+(o.side==='B'?1:-1)*Number(o.sz),0); - const logger=new Logger({level:config.logLevel,intentId:randomUUID()}); - try{return evaluateRisk(config,intent,{positionSize:position?.szi??'0',dailyLoss:String(Math.max(0,-state.assetPositions.reduce((sum,p)=>sum+Number(p.position.unrealizedPnl),0))),openOrders:open.length,marketMaxLeverage:market.maxLeverage,pendingSize:String(pending)});}catch(error){logger.log('warn','Risk rejected',{error:mapHyperliquidError(error).toJSON()});await logger.flush();throw error;} +export async function assertCanTrade(config: Config) { + if (config.revoked || !config.approved || !config.masterAddress) + throw new AgentCliError('AGENT_NOT_APPROVED', 'Agent approval required'); + assertAgentValid(config); + await assertDepositsSettled(config.network, config.masterAddress); } -export async function exchangeAction(action:string,operation:()=>Promise){ - assertTradeAction(action);await decisionAudit({allowed:true,action});try{ - const response=await operation();const data=response as {status?:string;response?:{data?:{statuses?:unknown[]}}}; - if(data.status==='err') throw mapHyperliquidError(response); - for(const status of data.response?.data?.statuses??[]) if(status&&typeof status==='object'&&'error' in status) throw mapHyperliquidError((status as {error:string}).error); - return response; - }catch(error){throw mapHyperliquidError(error);} +export async function checkRisk( + config: Config, + intent: RiskIntent, + services: TradingServices, + ignoreOid?: number, +) { + const [market, state, orders] = await Promise.all([ + services.market(intent.coin), + services.state(), + services.orders(), + ]); + const position = state.assetPositions.find( + (p) => p.position.coin === intent.coin, + )?.position; + const open = orders.filter((o) => o.oid !== ignoreOid); + const pending = open.filter((o) => o.coin === intent.coin && !o.reduceOnly); + const buys = pending + .filter((o) => o.side === 'B') + .reduce((sum, o) => sum + parseUsdc(o.sz, 18), 0n), + sells = pending + .filter((o) => o.side === 'A') + .reduce((sum, o) => sum + parseUsdc(o.sz, 18), 0n); + const loss = + config.limits.dailyLoss !== undefined + ? await requiredDailyLoss(services) + : '0'; + const logger = new Logger({ level: config.logLevel }); + try { + return evaluateRisk(config, intent, { + positionSize: position?.szi ?? '0', + dailyLoss: loss, + openOrders: open.length, + marketMaxLeverage: market.maxLeverage, + pendingBuySize: formatUsdc(buys, 18), + pendingSellSize: formatUsdc(sells, 18), + }); + } catch (error) { + logger.log('warn', 'Risk rejected', { + error: mapHyperliquidError(error).toJSON(), + }); + await logger.flush(); + throw error; + } +} +export async function exchangeAction( + action: string, + operation: () => Promise, +) { + assertTradeAction(action); + await decisionAudit({ allowed: true, action }); + try { + const response = await operation(); + const data = response as { + status?: string; + response?: { data?: { statuses?: unknown[] } }; + }; + if (data.status === 'err') throw mapHyperliquidError(response); + for (const status of data.response?.data?.statuses ?? []) + if (status && typeof status === 'object' && 'error' in status) + throw mapHyperliquidError((status as { error: string }).error); + return response; + } catch (error) { + throw mapHyperliquidError(error); + } +} + +export function dailyLossFromPortfolio( + portfolio: [string, { pnlHistory: [number, string][] }][], + now = Date.now(), +) { + const history = portfolio.find(([period]) => period === 'day')?.[1] + .pnlHistory; + if (!history?.length) + throw new AgentCliError( + 'POLICY_REJECTED', + 'Daily loss history unavailable', + ); + const midnight = new Date(now); + midnight.setUTCHours(0, 0, 0, 0); + const initial = + [...history].reverse().find(([time]) => time <= midnight.getTime()) ?? + history[0]; + const last = history.at(-1)!; + const signed = (value: string) => + value.startsWith('-') ? -parseUsdc(value.slice(1), 8) : parseUsdc(value, 8); + const pnl = signed(last[1]) - signed(initial[1]); + return formatUsdc(pnl < 0n ? -pnl : 0n, 8); +} +export async function requiredDailyLoss(services: TradingServices) { + if (!services.dailyLoss) + throw new AgentCliError('POLICY_REJECTED', 'Daily loss check unavailable'); + return services.dailyLoss(); } diff --git a/src/errors/index.ts b/src/errors/index.ts index b4ad26a..bcbd186 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -1,21 +1,54 @@ -import {redact} from '../core/logger.js'; +import { redact } from '../core/logger.js'; export const errorDefaults = { - INSUFFICIENT_MARGIN: false, POLICY_REJECTED: false, AGENT_EXPIRED: false, - AGENT_NOT_APPROVED: false, DEPOSIT_PENDING: true, NONCE_COLLISION: true, - RATE_LIMIT: true, INVALID_SIZE: false, INVALID_PRICE: false, - UNKNOWN_MARKET: false, ORDER_NOT_FOUND: false, RUNNER_NOT_RUNNING: true, - CONFIG_INVALID: false, NETWORK_ERROR: true, UPSTREAM_ERROR: false, INTERNAL: false, + INSUFFICIENT_MARGIN: false, + POLICY_REJECTED: false, + AGENT_EXPIRED: false, + AGENT_NOT_APPROVED: false, + DEPOSIT_PENDING: true, + NONCE_COLLISION: true, + RATE_LIMIT: true, + INVALID_SIZE: false, + INVALID_PRICE: false, + UNKNOWN_MARKET: false, + ORDER_NOT_FOUND: false, + RUNNER_NOT_RUNNING: true, + CONFIG_INVALID: false, + NETWORK_ERROR: true, + UPSTREAM_ERROR: false, + INTERNAL: false, } as const; export type ErrorCode = keyof typeof errorDefaults; export class AgentCliError extends Error { readonly retryable: boolean; - constructor(readonly code: ErrorCode, message: string, readonly details: unknown = null, retryable = errorDefaults[code] as boolean) { - super(message); this.name = 'AgentCliError'; this.retryable = retryable; + constructor( + readonly code: ErrorCode, + message: string, + readonly details: unknown = null, + retryable = errorDefaults[code] as boolean, + ) { + super(message); + this.name = 'AgentCliError'; + this.retryable = retryable; + } + toJSON() { + return { + ok: false as const, + code: this.code, + message: String(redact(this.message)), + retryable: this.retryable, + details: redact(this.details), + }; + } + get exitCode() { + return this.code === 'CONFIG_INVALID' ? 2 : this.retryable ? 75 : 1; } - toJSON() { return { ok: false as const, code: this.code, message: String(redact(this.message)), retryable: this.retryable, details: redact(this.details) }; } - get exitCode() { return this.code === 'CONFIG_INVALID' ? 2 : this.retryable ? 75 : 1; } } export function normalizeError(error: unknown): AgentCliError { - return error instanceof AgentCliError ? error : new AgentCliError('INTERNAL', error instanceof Error ? error.message : String(error)); + return error instanceof AgentCliError + ? error + : new AgentCliError( + 'INTERNAL', + error instanceof Error ? error.message : String(error), + ); } export const success = (data: T) => ({ ok: true as const, data }); diff --git a/src/errors/mappers.ts b/src/errors/mappers.ts index a216bfc..0d62640 100644 --- a/src/errors/mappers.ts +++ b/src/errors/mappers.ts @@ -1,14 +1,54 @@ import { AgentCliError, type ErrorCode } from './index.js'; import { redact } from '../core/logger.js'; -function describe(error:unknown):string {if(error instanceof Error) return error.message;if(typeof error==='string') return error;return JSON.stringify(error)??'Unknown upstream error';} -function map(error:unknown,service:string):AgentCliError { - if(error instanceof AgentCliError) return error; - const message=String(redact(describe(error)));let code:ErrorCode='UPSTREAM_ERROR'; - const patterns:[RegExp,ErrorCode][]=[[/429|rate.?limit|too many requests/i,'RATE_LIMIT'],[/nonce|duplicate.*timestamp/i,'NONCE_COLLISION'],[/insufficient.*margin|margin.*insufficient/i,'INSUFFICIENT_MARGIN'],[/minimum.*(size|value)|min.*notional|invalid.*size|too small/i,'INVALID_SIZE'],[/tick|invalid.*price/i,'INVALID_PRICE'],[/policy|not allowed|denied/i,'POLICY_REJECTED'],[/agent.*expired/i,'AGENT_EXPIRED'],[/agent.*not.*approved|not.*authorized/i,'AGENT_NOT_APPROVED'],[/order.*not.*found|does not exist/i,'ORDER_NOT_FOUND']]; - for(const [pattern,candidate] of patterns) if(pattern.test(message)){code=candidate;break;} - return new AgentCliError(code,message,redact({service,upstream:error instanceof Error?{message:error.message}:error})); +function describe(error: unknown): string { + if (error instanceof Error) return error.message; + if (typeof error === 'string') return error; + return JSON.stringify(error) ?? 'Unknown upstream error'; } -export const mapHyperliquidError=(error:unknown)=>map(error,'hyperliquid'); -export const mapPrivyError=(error:unknown)=>map(error,'privy'); -export const mapRelayError=(error:unknown)=>map(error,'relay'); -export const mapNetworkError=(error:unknown)=>new AgentCliError('NETWORK_ERROR',String(redact(describe(error))),redact({service:'network',upstream:describe(error)})); +function map(error: unknown, service: string): AgentCliError { + if (error instanceof AgentCliError) return error; + const message = String(redact(describe(error))); + if ( + (error instanceof TypeError && /fetch|network/i.test(message)) || + /ECONNRESET|ETIMEDOUT|ENOTFOUND|ECONNREFUSED/.test(message) + ) + return mapNetworkError(error); + let code: ErrorCode = 'UPSTREAM_ERROR'; + const patterns: [RegExp, ErrorCode][] = [ + [/429|rate.?limit|too many requests/i, 'RATE_LIMIT'], + [/nonce|duplicate.*timestamp/i, 'NONCE_COLLISION'], + [/insufficient.*margin|margin.*insufficient/i, 'INSUFFICIENT_MARGIN'], + [ + /minimum.*(size|value)|min.*notional|invalid.*size|too small/i, + 'INVALID_SIZE', + ], + [/tick|invalid.*price/i, 'INVALID_PRICE'], + [/policy|not allowed|denied/i, 'POLICY_REJECTED'], + [/agent.*expired/i, 'AGENT_EXPIRED'], + [/agent.*not.*approved|not.*authorized/i, 'AGENT_NOT_APPROVED'], + [/order.*not.*found|does not exist/i, 'ORDER_NOT_FOUND'], + ]; + for (const [pattern, candidate] of patterns) + if (pattern.test(message)) { + code = candidate; + break; + } + return new AgentCliError( + code, + message, + redact({ + service, + upstream: error instanceof Error ? { message: error.message } : error, + }), + ); +} +export const mapHyperliquidError = (error: unknown) => + map(error, 'hyperliquid'); +export const mapPrivyError = (error: unknown) => map(error, 'privy'); +export const mapRelayError = (error: unknown) => map(error, 'relay'); +export const mapNetworkError = (error: unknown) => + new AgentCliError( + 'NETWORK_ERROR', + String(redact(describe(error))), + redact({ service: 'network', upstream: describe(error) }), + ); diff --git a/src/funding/amounts.ts b/src/funding/amounts.ts index ac4b1d4..0e49aaa 100644 --- a/src/funding/amounts.ts +++ b/src/funding/amounts.ts @@ -1,11 +1,42 @@ -import {AgentCliError} from '../errors/index.js'; -function checkDecimals(decimals:number){if(!Number.isInteger(decimals)||decimals<0||decimals>30) throw new RangeError('Invalid decimals');} -export function parseUsdc(value:string,decimals:number):bigint{ - checkDecimals(decimals);if(!/^\d+(\.\d+)?$/.test(value)) throw new AgentCliError('INVALID_SIZE','Expected nonnegative decimal string'); - const [whole,fraction='']=value.split('.');if(fraction.length>decimals) throw new AgentCliError('INVALID_SIZE','Too many decimal places');return BigInt(whole)*10n**BigInt(decimals)+BigInt(fraction.padEnd(decimals,'0')||'0'); +import { AgentCliError } from '../errors/index.js'; +function checkDecimals(decimals: number) { + if (!Number.isInteger(decimals) || decimals < 0 || decimals > 30) + throw new RangeError('Invalid decimals'); } -export function formatUsdc(value:bigint,decimals:number):string{ - checkDecimals(decimals);const negative=value<0n;const absolute=negative?-value:value;const scale=10n**BigInt(decimals);const fraction=(absolute%scale).toString().padStart(decimals,'0').replace(/0+$/,'');return `${negative?'-':''}${absolute/scale}${fraction?'.'+fraction:''}`; +export function parseUsdc(value: string, decimals: number): bigint { + checkDecimals(decimals); + if (!/^-?\d+(\.\d+)?$/.test(value)) + throw new AgentCliError( + 'INVALID_SIZE', + 'Expected nonnegative decimal string', + ); + const negative = value.startsWith('-'); + const [whole, fraction = ''] = (negative ? value.slice(1) : value).split('.'); + if (fraction.length > decimals) + throw new AgentCliError('INVALID_SIZE', 'Too many decimal places'); + return ( + (negative ? -1n : 1n) * + (BigInt(whole) * 10n ** BigInt(decimals) + + BigInt(fraction.padEnd(decimals, '0') || '0')) + ); +} +export function formatUsdc(value: bigint, decimals: number): string { + checkDecimals(decimals); + const negative = value < 0n; + const absolute = negative ? -value : value; + const scale = 10n ** BigInt(decimals); + const fraction = (absolute % scale) + .toString() + .padStart(decimals, '0') + .replace(/0+$/, ''); + return `${negative ? '-' : ''}${absolute / scale}${fraction ? '.' + fraction : ''}`; +} +export const toHyperCore = (evm: bigint) => evm * 100n; +export function fromHyperCore(core: bigint) { + if (core % 100n !== 0n) + throw new AgentCliError( + 'INVALID_SIZE', + 'Conversion would lose HyperCore precision', + ); + return core / 100n; } -export const toHyperCore=(evm:bigint)=>evm*100n; -export function fromHyperCore(core:bigint){if(core%100n!==0n) throw new AgentCliError('INVALID_SIZE','Conversion would lose HyperCore precision');return core/100n;} diff --git a/src/funding/qr.ts b/src/funding/qr.ts index 6df32e7..f33afdb 100644 --- a/src/funding/qr.ts +++ b/src/funding/qr.ts @@ -1,9 +1,32 @@ import QRCode from 'qrcode'; -import {writeFile} from 'node:fs/promises'; -export async function renderQr(value:string,options:{enabled?:boolean;path?:string;stderr?:(value:string)=>void}={}){ - if(options.enabled===false) return {qrAscii:null,qrPath:null}; - const qrAscii=await QRCode.toString(value,{type:'terminal',small:true}); - (options.stderr??(value=>process.stderr.write(value)))(qrAscii+'\n'); - if(options.path){if(options.path.endsWith('.svg')) await writeFile(options.path,await QRCode.toString(value,{type:'svg'}),{mode:0o600});else await writeFile(options.path,await QRCode.toBuffer(value,{type:'png'}),{mode:0o600});} - return {qrAscii,qrPath:options.path??null}; +import { writeFile } from 'node:fs/promises'; +export async function renderQr( + value: string, + options: { + enabled?: boolean; + path?: string; + stderr?: (value: string) => void; + } = {}, +) { + if (options.enabled === false) return { qrAscii: null, qrPath: null }; + const qrAscii = await QRCode.toString(value, { + type: 'terminal', + small: true, + }); + (options.stderr ?? ((value) => process.stderr.write(value)))(qrAscii + '\n'); + if (options.path) { + if (options.path.endsWith('.svg')) + await writeFile( + options.path, + await QRCode.toString(value, { type: 'svg' }), + { mode: 0o600 }, + ); + else + await writeFile( + options.path, + await QRCode.toBuffer(value, { type: 'png' }), + { mode: 0o600 }, + ); + } + return { qrAscii, qrPath: options.path ?? null }; } diff --git a/src/funding/relay.ts b/src/funding/relay.ts index 0862d7c..c880c18 100644 --- a/src/funding/relay.ts +++ b/src/funding/relay.ts @@ -1,17 +1,98 @@ -import {z} from 'zod'; -import {jsonRequest} from '../core/http.js'; -import {mapRelayError} from '../errors/mappers.js'; -export const PERPS_USDC='0x00000000000000000000000000000000'; -export interface QuoteRequest {user:string;recipient:string;originChainId:number;originCurrency:string;amount:string;destinationChainId:1337;destinationCurrency:typeof PERPS_USDC;useDepositAddress:true;tradeType:'EXACT_INPUT';} -const item=z.object({status:z.string(),data:z.record(z.string(),z.unknown()),check:z.object({endpoint:z.string(),method:z.string()}).optional()}).passthrough(); -export const quoteSchema=z.object({steps:z.array(z.object({id:z.string(),kind:z.string(),requestId:z.string().optional(),depositAddress:z.string().optional(),items:z.array(item)}).passthrough()),fees:z.record(z.string(),z.unknown()).default({}),details:z.object({currencyOut:z.object({amount:z.string(),currency:z.object({decimals:z.number()}).passthrough()}).passthrough(),currencyIn:z.object({amount:z.string()}).passthrough().optional(),minAmount:z.string().optional(),expiresAt:z.number().optional()}).passthrough()}).passthrough(); -export type RelayQuote=z.infer; +import { z } from 'zod'; +import { jsonRequest } from '../core/http.js'; +import { mapRelayError } from '../errors/mappers.js'; +export const PERPS_USDC = '0x00000000000000000000000000000000'; +export interface QuoteRequest { + user: string; + recipient: string; + originChainId: number; + originCurrency: string; + amount: string; + destinationChainId: 1337; + destinationCurrency: typeof PERPS_USDC; + useDepositAddress: true; + tradeType: 'EXACT_INPUT'; +} +const item = z + .object({ + status: z.string(), + data: z.record(z.string(), z.unknown()), + check: z.object({ endpoint: z.string(), method: z.string() }).optional(), + }) + .passthrough(); +export const quoteSchema = z + .object({ + steps: z.array( + z + .object({ + id: z.string(), + kind: z.string(), + requestId: z.string().optional(), + depositAddress: z.string().optional(), + items: z.array(item), + }) + .passthrough(), + ), + fees: z.record(z.string(), z.unknown()).default({}), + details: z + .object({ + currencyOut: z + .object({ + amount: z.string(), + currency: z.object({ decimals: z.number() }).passthrough(), + }) + .passthrough(), + currencyIn: z.object({ amount: z.string() }).passthrough().optional(), + minAmount: z.string().optional(), + expiresAt: z.number().optional(), + }) + .passthrough(), + }) + .passthrough(); +export type RelayQuote = z.infer; export class RelayClient { - constructor(readonly base='https://api.relay.link',private fetcher:typeof fetch=fetch){} - async quote(request:QuoteRequest){try{return quoteSchema.parse(await jsonRequest(`${this.base}/quote/v2`,request,this.fetcher));}catch(error){throw mapRelayError(error);}} - async status(requestId:string){try{return await jsonRequest(`${this.base}/intents/status/v3?requestId=${encodeURIComponent(requestId)}`,undefined,this.fetcher) as {status:string};}catch(error){throw mapRelayError(error);}} + constructor( + readonly base = 'https://api.relay.link', + private fetcher: typeof fetch = fetch, + ) {} + async quote(request: QuoteRequest) { + try { + return quoteSchema.parse( + await jsonRequest(`${this.base}/quote/v2`, request, this.fetcher), + ); + } catch (error) { + throw mapRelayError(error); + } + } + async status(requestId: string) { + try { + return (await jsonRequest( + `${this.base}/intents/status/v3?requestId=${encodeURIComponent(requestId)}`, + undefined, + this.fetcher, + )) as { status: string }; + } catch (error) { + throw mapRelayError(error); + } + } } -export function parseQuote(quote:RelayQuote){ - const deposit=quote.steps.find(step=>step.depositAddress);if(!deposit?.requestId) throw mapRelayError('Quote missing deposit address/requestId'); - return {depositId:deposit.requestId,depositAddress:deposit.depositAddress!,fees:{app:quote.fees.app??quote.fees.appFees??null,gas:quote.fees.gas??null,relayer:quote.fees.relayer??quote.fees.relayerService??null,raw:quote.fees},minAmount:quote.details.minAmount??'0',expectedReceived:quote.details.currencyOut.amount,receivedDecimals:quote.details.currencyOut.currency.decimals,expiresAt:quote.details.expiresAt??Date.now()+30_000,steps:quote.steps}; +export function parseQuote(quote: RelayQuote) { + const deposit = quote.steps.find((step) => step.depositAddress); + if (!deposit?.requestId) + throw mapRelayError('Quote missing deposit address/requestId'); + return { + depositId: deposit.requestId, + depositAddress: deposit.depositAddress!, + fees: { + app: quote.fees.app ?? quote.fees.appFees ?? null, + gas: quote.fees.gas ?? null, + relayer: quote.fees.relayer ?? quote.fees.relayerService ?? null, + raw: quote.fees, + }, + minAmount: quote.details.minAmount ?? '0', + expectedReceived: quote.details.currencyOut.amount, + receivedDecimals: quote.details.currencyOut.currency.decimals, + expiresAt: quote.details.expiresAt ?? Date.now() + 30_000, + steps: quote.steps, + }; } diff --git a/src/funding/signatures.ts b/src/funding/signatures.ts index 743d9ff..f21858e 100644 --- a/src/funding/signatures.ts +++ b/src/funding/signatures.ts @@ -1,15 +1,88 @@ -import {AgentCliError} from '../errors/index.js'; -import {jsonRequest} from '../core/http.js'; -import {readDeposits,writeDeposits,type Deposit} from './store.js'; -export function signatureSteps(deposit:Deposit){return deposit.quote.steps.flatMap((step)=>step.kind==='signature'?step.items.map((item,index)=>({id:`${step.id}:${index}`,requiresSignature:!deposit.completedSteps.includes(`${step.id}:${index}`),payload:item.data.sign,post:item.data.post})):[]);} -export async function submitDepositSignature(depositId:string,stepId:string,signature:string,options:{path?:string;base?:string;submit?:(url:string,body:unknown)=>Promise}={}){ - if(!/^0x[0-9a-fA-F]{130}$/.test(signature)) throw new AgentCliError('CONFIG_INVALID','Expected wallet signature'); - const deposits=await readDeposits(options.path);const deposit=deposits.find(d=>d.id===depositId);if(!deposit) throw new AgentCliError('CONFIG_INVALID','Unknown deposit'); - const pending=signatureSteps(deposit).filter(s=>s.requiresSignature);if(!pending.length) return {complete:true};const step=pending[0]; - if(step.id!==stepId) throw new AgentCliError('CONFIG_INVALID','Submit signatures in step order',{nextStep:step.id}); - const post=step.post as {endpoint:string;method:string;body:Record}; - if(!post||post.method!=='POST'||!post.endpoint.startsWith('/')||post.endpoint.startsWith('//')) throw new AgentCliError('UPSTREAM_ERROR','Invalid signature submission endpoint'); - const base=options.base??(deposit.network==='testnet'?'https://api.testnets.relay.link':'https://api.relay.link');const url=new URL(post.endpoint,base);if(url.origin!==new URL(base).origin) throw new AgentCliError('POLICY_REJECTED','Untrusted signature destination'); - await (options.submit??jsonRequest)(url.toString(),{...post.body,signature});deposit.completedSteps.push(step.id);await writeDeposits(deposits,options.path); - return {depositId,steps:signatureSteps(deposit)}; +import { AgentCliError } from '../errors/index.js'; +import { jsonRequest } from '../core/http.js'; +import { readDeposits, writeDeposits, type Deposit } from './store.js'; +export function signatureSteps(deposit: Deposit) { + return deposit.quote.steps.flatMap((step) => + step.kind === 'signature' + ? step.items.map((item, index) => ({ + id: `${step.id}:${index}`, + requiresSignature: !deposit.completedSteps.includes( + `${step.id}:${index}`, + ), + payload: signaturePayload(item.data.sign), + deeplink: `ethereum:${deposit.recipient}?request=${encodeURIComponent(JSON.stringify(signaturePayload(item.data.sign)))}`, + post: item.data.post, + })) + : [], + ); +} +export async function submitDepositSignature( + depositId: string, + stepId: string, + signature: string, + options: { + path?: string; + base?: string; + submit?: (url: string, body: unknown) => Promise; + } = {}, +) { + if (!/^0x[0-9a-fA-F]{130}$/.test(signature)) + throw new AgentCliError('CONFIG_INVALID', 'Expected wallet signature'); + const deposits = await readDeposits(options.path); + const deposit = deposits.find((d) => d.id === depositId); + if (!deposit) throw new AgentCliError('CONFIG_INVALID', 'Unknown deposit'); + const pending = signatureSteps(deposit).filter((s) => s.requiresSignature); + if (!pending.length) return { complete: true }; + const step = pending[0]; + if (step.id !== stepId) + throw new AgentCliError( + 'CONFIG_INVALID', + 'Submit signatures in step order', + { nextStep: step.id }, + ); + const post = step.post as { + endpoint: string; + method: string; + body: Record; + }; + if ( + !post || + post.method !== 'POST' || + !post.endpoint.startsWith('/') || + post.endpoint.startsWith('//') + ) + throw new AgentCliError( + 'UPSTREAM_ERROR', + 'Invalid signature submission endpoint', + ); + const base = + options.base ?? + (deposit.network === 'testnet' + ? 'https://api.testnets.relay.link' + : 'https://api.relay.link'); + const url = new URL(post.endpoint, base); + if (url.origin !== new URL(base).origin) + throw new AgentCliError( + 'POLICY_REJECTED', + 'Untrusted signature destination', + ); + // Relay's generic step contract puts the wallet signature in the query. + url.searchParams.set('signature', signature); + await (options.submit ?? jsonRequest)(url.toString(), post.body); + deposit.completedSteps.push(step.id); + await writeDeposits(deposits, options.path); + return { depositId, steps: signatureSteps(deposit) }; +} + +function signaturePayload(value: unknown) { + if (!value || typeof value !== 'object') return value; + const sign = value as Record; + if (sign.signatureKind === 'eip712') + return { + domain: sign.domain, + types: sign.types, + primaryType: sign.primaryType, + message: sign.message ?? sign.value, + }; + return value; } diff --git a/src/funding/status.ts b/src/funding/status.ts index 2ba719c..46c4a8c 100644 --- a/src/funding/status.ts +++ b/src/funding/status.ts @@ -1,26 +1,88 @@ -import {readDeposits,writeDeposits,type Deposit} from './store.js'; -import {RelayClient,parseQuote} from './relay.js'; -import {parseUsdc} from './amounts.js'; -import {AgentCliError} from '../errors/index.js'; -export function advanceDeposit(deposit:Deposit,relayStatus:string,balance:string){ - if(['settled','failed','refunded'].includes(deposit.state)) return deposit.state; - if(relayStatus==='failure'||relayStatus==='failed') return 'failed' as const; - if(relayStatus==='refunded') return 'refunded' as const; - const quote=parseQuote(deposit.quote);const needed=BigInt(quote.expectedReceived)*10n**BigInt(8-quote.receivedDecimals); - if(relayStatus==='success'&&parseUsdc(balance,8)>=parseUsdc(deposit.baseline,8)+needed) return 'settled' as const; - if(['success','pending','deposit-confirmed','waiting'].includes(relayStatus)) return 'pending' as const; - return deposit.state; +import { readDeposits, writeDeposits, type Deposit } from './store.js'; +import { RelayClient, parseQuote } from './relay.js'; +import { parseUsdc } from './amounts.js'; +import { AgentCliError } from '../errors/index.js'; +export function advanceDeposit( + deposit: Deposit, + relayStatus: string, + balance: string, +) { + if (['settled', 'failed', 'refunded'].includes(deposit.state)) + return deposit.state; + if (relayStatus === 'failure' || relayStatus === 'failed') + return 'failed' as const; + if (relayStatus === 'refunded') return 'refunded' as const; + const quote = parseQuote(deposit.quote); + const needed = + quote.receivedDecimals <= 8 + ? BigInt(quote.expectedReceived) * + 10n ** BigInt(8 - quote.receivedDecimals) + : BigInt(quote.expectedReceived) / + 10n ** BigInt(quote.receivedDecimals - 8); + if ( + relayStatus === 'success' && + parseUsdc(balance, 8) >= parseUsdc(deposit.baseline, 8) + needed + ) + return 'settled' as const; + if ( + ['success', 'pending', 'deposit-confirmed', 'waiting'].includes(relayStatus) + ) + return 'pending' as const; + return deposit.state; } -export async function pollDeposits(network:Deposit['network'],recipient:string,balance:string,options:{path?:string;relay?:RelayClient;emit?:(deposit:Deposit)=>Promise}={}){ - const deposits=await readDeposits(options.path);let changed=false; - for(const deposit of deposits.filter(d=>d.network===network&&d.recipient.toLowerCase()===recipient.toLowerCase())){ - if(['settled','failed','refunded'].includes(deposit.state)) continue; - const status=await (options.relay??new RelayClient(network==='testnet'?'https://api.testnets.relay.link':'https://api.relay.link')).status(deposit.id); - const next=advanceDeposit(deposit,status.status,balance);if(next!==deposit.state){deposit.state=next;changed=true;await options.emit?.(deposit);} - } - if(changed) await writeDeposits(deposits,options.path);return deposits.filter(d=>d.network===network&&d.recipient.toLowerCase()===recipient.toLowerCase()); +export async function pollDeposits( + network: Deposit['network'], + recipient: string, + balance: string, + options: { + path?: string; + relay?: RelayClient; + emit?: (deposit: Deposit) => Promise; + } = {}, +) { + const deposits = await readDeposits(options.path); + let changed = false; + for (const deposit of deposits.filter( + (d) => + d.network === network && + d.recipient.toLowerCase() === recipient.toLowerCase(), + )) { + if (['settled', 'failed', 'refunded'].includes(deposit.state)) continue; + const status = await ( + options.relay ?? + new RelayClient( + network === 'testnet' + ? 'https://api.testnets.relay.link' + : 'https://api.relay.link', + ) + ).status(deposit.id); + const next = advanceDeposit(deposit, status.status, balance); + if (next !== deposit.state) { + deposit.state = next; + changed = true; + await options.emit?.(deposit); + } + } + if (changed) await writeDeposits(deposits, options.path); + return deposits.filter( + (d) => + d.network === network && + d.recipient.toLowerCase() === recipient.toLowerCase(), + ); } -export async function assertDepositsSettled(network:Deposit['network'],recipient:string,path?:string){ - const pending=(await readDeposits(path)).filter(d=>d.network===network&&d.recipient.toLowerCase()===recipient.toLowerCase()&&['awaiting_funds','pending'].includes(d.state)); - if(pending.length) throw new AgentCliError('DEPOSIT_PENDING','Wait for deposit settlement',{depositIds:pending.map(d=>d.id)}); +export async function assertDepositsSettled( + network: Deposit['network'], + recipient: string, + path?: string, +) { + const pending = (await readDeposits(path)).filter( + (d) => + d.network === network && + d.recipient.toLowerCase() === recipient.toLowerCase() && + ['awaiting_funds', 'pending'].includes(d.state), + ); + if (pending.length) + throw new AgentCliError('DEPOSIT_PENDING', 'Wait for deposit settlement', { + depositIds: pending.map((d) => d.id), + }); } diff --git a/src/funding/store.ts b/src/funding/store.ts index 2d92bb6..b456e30 100644 --- a/src/funding/store.ts +++ b/src/funding/store.ts @@ -1,8 +1,28 @@ -import {readFile} from 'node:fs/promises'; -import {join} from 'node:path'; -import {atomicWrite,stateDir} from '../core/config.js'; -import type {RelayQuote} from './relay.js'; -export interface Deposit {id:string;network:'mainnet'|'testnet';recipient:string;route:string;quote:RelayQuote;expiresAt:number;state:'awaiting_funds'|'pending'|'settled'|'failed'|'refunded';baseline:string;createdAt:number;completedSteps:string[];} -export const depositPath=()=>join(stateDir(),'deposits.json'); -export async function readDeposits(path=depositPath()):Promise{try{return JSON.parse(await readFile(path,'utf8'));}catch(error){if((error as NodeJS.ErrnoException).code==='ENOENT') return [];throw error;}} -export async function writeDeposits(deposits:Deposit[],path=depositPath()){await atomicWrite(path,JSON.stringify(deposits));} +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { atomicWrite, stateDir } from '../core/config.js'; +import type { RelayQuote } from './relay.js'; +export interface Deposit { + id: string; + network: 'mainnet' | 'testnet'; + recipient: string; + route: string; + quote: RelayQuote; + expiresAt: number; + state: 'awaiting_funds' | 'pending' | 'settled' | 'failed' | 'refunded'; + baseline: string; + createdAt: number; + completedSteps: string[]; +} +export const depositPath = () => join(stateDir(), 'deposits.json'); +export async function readDeposits(path = depositPath()): Promise { + try { + return JSON.parse(await readFile(path, 'utf8')); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []; + throw error; + } +} +export async function writeDeposits(deposits: Deposit[], path = depositPath()) { + await atomicWrite(path, JSON.stringify(deposits)); +} diff --git a/src/runner/events.ts b/src/runner/events.ts index 492f6bf..5354d41 100644 --- a/src/runner/events.ts +++ b/src/runner/events.ts @@ -1,30 +1,138 @@ -import {auditFill} from '../core/audit.js'; -import {appendFile,mkdir,readFile,chmod} from 'node:fs/promises'; -import {dirname,join} from 'node:path'; -import {z} from 'zod'; -import {stateDir} from '../core/config.js'; -import {redact} from '../core/logger.js'; -export const eventTypes=['fill','order_update','position_change','deposit','error','agent_expiry_warning','runner_state'] as const; -export type EventType=typeof eventTypes[number]; -export const eventSchema=z.object({seq:z.number().int().positive(),timestamp:z.string(),type:z.enum(eventTypes),data:z.unknown()}); -export type RunnerEvent=z.infer; -export async function readEvents(path=join(stateDir(),'events.jsonl'),since=0,types:EventType[]=[]):Promise{ - let contents:string;try{contents=await readFile(path,'utf8');}catch(error){if((error as NodeJS.ErrnoException).code==='ENOENT') return [];throw error;} - return contents.split('\n').filter(Boolean).map(line=>eventSchema.parse(JSON.parse(line))).filter(event=>event.seq>since&&(!types.length||types.includes(event.type))); +import { parseUsdc } from '../funding/amounts.js'; +import { AgentCliError } from '../errors/index.js'; +import { auditFill } from '../core/audit.js'; +import { + appendFile, + mkdir, + readFile, + chmod, + open, + unlink, +} from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { z } from 'zod'; +import { stateDir } from '../core/config.js'; +import { redact } from '../core/logger.js'; +export const eventTypes = [ + 'fill', + 'order_update', + 'position_change', + 'deposit', + 'error', + 'agent_expiry_warning', + 'runner_state', +] as const; +export type EventType = (typeof eventTypes)[number]; +export const eventSchema = z.object({ + seq: z.number().int().positive(), + timestamp: z.string(), + type: z.enum(eventTypes), + data: z.unknown(), +}); +export type RunnerEvent = z.infer; +export async function readEvents( + path = join(stateDir(), 'events.jsonl'), + since = 0, + types: EventType[] = [], +): Promise { + let contents: string; + try { + contents = await readFile(path, 'utf8'); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []; + throw error; + } + return contents + .split('\n') + .slice(0, -1) + .filter(Boolean) + .map((line) => eventSchema.parse(JSON.parse(line))) + .filter( + (event) => + event.seq > since && (!types.length || types.includes(event.type)), + ); } export class EventStore { - private queue=Promise.resolve(); - constructor(readonly path=join(stateDir(),'events.jsonl')){} - append(type:EventType,data:unknown){let event:RunnerEvent;const operation=this.queue.then(async()=>{ - if(type==='fill'&&data&&typeof data==='object') await auditFill(data as Record); - const previous=(await readEvents(this.path)).at(-1)?.seq??0;event=eventSchema.parse({seq:previous+1,timestamp:new Date().toISOString(),type,data:redact(data)}); - await mkdir(dirname(this.path),{recursive:true,mode:0o700});await appendFile(this.path,JSON.stringify(event)+'\n',{mode:0o600});await chmod(this.path,0o600);return event; - });this.queue=operation.then(()=>{});return operation; - } - async flush(){await this.queue;} + private queue = Promise.resolve(); + constructor(readonly path = join(stateDir(), 'events.jsonl')) {} + append(type: EventType, data: unknown) { + let event: RunnerEvent; + const operation = this.queue.then(async () => { + if (type === 'fill' && data && typeof data === 'object') + await auditFill(data as Record); + await mkdir(dirname(this.path), { recursive: true, mode: 0o700 }); + await chmod(dirname(this.path), 0o700); + let guard; + for (let attempt = 0; attempt < 100; attempt++) { + try { + guard = await open(this.path + '.lock', 'wx', 0o600); + break; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error; + await new Promise((resolve) => setTimeout(resolve, 10)); + } + } + if (!guard) + throw new AgentCliError('INTERNAL', 'Event journal lock busy'); + try { + const previous = (await readEvents(this.path)).at(-1)?.seq ?? 0; + event = eventSchema.parse({ + seq: previous + 1, + timestamp: new Date().toISOString(), + type, + data: redact(type === 'fill' ? normalizeFillAmounts(data) : data), + }); + await mkdir(dirname(this.path), { recursive: true, mode: 0o700 }); + await appendFile(this.path, JSON.stringify(event) + '\n', { + mode: 0o600, + }); + await chmod(this.path, 0o600); + return event; + } finally { + await guard.close(); + await unlink(this.path + '.lock'); + } + }); + this.queue = operation.then(() => {}); + return operation; + } + async flush() { + await this.queue; + } } -export async function followEvents(options:{path?:string;since?:number;types?:EventType[];signal?:AbortSignal;write:(event:RunnerEvent)=>void;interval?:number}){ - let since=options.since??0; - while(!options.signal?.aborted){for(const event of await readEvents(options.path,since,options.types)){options.write(event);since=event.seq;}await new Promise(resolve=>{const timer=setTimeout(done,options.interval??250);function done(){clearTimeout(timer);options.signal?.removeEventListener('abort',done);resolve();}options.signal?.addEventListener('abort',done,{once:true});});} +export async function followEvents(options: { + path?: string; + since?: number; + types?: EventType[]; + signal?: AbortSignal; + write: (event: RunnerEvent) => void; + interval?: number; +}) { + let since = options.since ?? 0; + while (!options.signal?.aborted) { + for (const event of await readEvents(options.path, since, options.types)) { + options.write(event); + since = event.seq; + } + await new Promise((resolve) => { + const timer = setTimeout(done, options.interval ?? 250); + function done() { + clearTimeout(timer); + options.signal?.removeEventListener('abort', done); + resolve(); + } + options.signal?.addEventListener('abort', done, { once: true }); + }); + } +} +export const events = new EventStore(); + +function normalizeFillAmounts(data: unknown) { + if (!data || typeof data !== 'object') return data; + const fill = data as Record; + if (typeof fill.fee !== 'string') return fill; + const units = fill.fee.startsWith('-') + ? -parseUsdc(fill.fee.slice(1), 8) + : parseUsdc(fill.fee, 8); + return { ...fill, feeUnits: units.toString() }; } -export const events=new EventStore(); diff --git a/src/runner/health.ts b/src/runner/health.ts index c26c499..a39c2a9 100644 --- a/src/runner/health.ts +++ b/src/runner/health.ts @@ -1,19 +1,72 @@ -import {expiryWarnings} from '../wallet/expiry.js'; -import {activeRunner,runningInfo,type Runner} from './lifecycle.js'; -import type {Subscriptions} from './subscriptions.js'; -import {events} from './events.js'; -import {hlInfo} from '../core/http.js'; -import {pollDeposits, } from '../funding/status.js'; -import {normalizeError} from '../errors/index.js'; -export function health(runner:Runner,now=Date.now()){ - const subscriptions=runner.subscriptions;return {pid:String(runner.info.pid),uptimeSeconds:String(Math.max(0,(now-runner.info.startedAt)/1000)),wsState:subscriptions?.state??'disconnected',wsStale:!subscriptions||now-subscriptions.lastMessage>90_000,lastFillTime:subscriptions?.lastFill??null,signingSession:runner.signer?'ready':'unavailable'}; +import { collateralBalance } from '../core/account.js'; +import { expiryWarnings } from '../wallet/expiry.js'; +import { activeRunner, runningInfo, type Runner } from './lifecycle.js'; +import type { Subscriptions } from './subscriptions.js'; +import { events } from './events.js'; +import { hlInfo } from '../core/http.js'; +import { pollDeposits } from '../funding/status.js'; +import { normalizeError } from '../errors/index.js'; +export function health(runner: Runner, now = Date.now()) { + const subscriptions = runner.subscriptions; + return { + pid: String(runner.info.pid), + uptimeSeconds: String(Math.max(0, (now - runner.info.startedAt) / 1000)), + wsState: subscriptions?.state ?? 'disconnected', + wsStale: !subscriptions || now - subscriptions.lastMessage > 90_000, + lastFillTime: subscriptions?.lastFill ?? null, + signingSession: runner.signer ? 'ready' : 'unavailable', + }; } -export async function runnerHealth(){if(activeRunner) return health(activeRunner);const info=await runningInfo();return info?{pid:String(info.pid),uptimeSeconds:String((Date.now()-info.startedAt)/1000),wsState:'unknown',signingSession:'unknown'}:null;} -export function startHeartbeat(runner:Runner,subscriptions:Subscriptions){ - runner.subscriptions=subscriptions;let queue=Promise.resolve(); - const tick=()=>{queue=queue.then(async()=>{ - await events.append('runner_state',health(runner));await expiryWarnings(runner.config); - if(runner.config.masterAddress){const state=await hlInfo<{marginSummary:{accountValue:string}}>(runner.config.network,'clearinghouseState',{user:runner.config.masterAddress});await pollDeposits(runner.config.network,runner.config.masterAddress,state.marginSummary.accountValue,{emit:async deposit=>{await events.append('deposit',{id:deposit.id,state:deposit.state});}});} - }).catch(async error=>{await events.append('error',normalizeError(error).toJSON());});}; - tick();const timer=setInterval(tick,30_000);runner.onShutdown(async()=>{clearInterval(timer);await queue;}); +export async function runnerHealth() { + if (activeRunner) return health(activeRunner); + const info = await runningInfo(); + return info + ? { + pid: String(info.pid), + uptimeSeconds: String((Date.now() - info.startedAt) / 1000), + wsState: 'unknown', + signingSession: 'unknown', + } + : null; +} +export function startHeartbeat(runner: Runner, subscriptions: Subscriptions) { + runner.subscriptions = subscriptions; + let queue = Promise.resolve(); + const tick = () => { + queue = queue + .then(async () => { + await events.append('runner_state', health(runner)); + await expiryWarnings(runner.config); + if (runner.config.masterAddress) { + const state = await hlInfo<{ + marginSummary: { accountValue: string }; + assetPositions: { position: { unrealizedPnl: string } }[]; + }>(runner.config.network, 'clearinghouseState', { + user: runner.config.masterAddress, + }); + await pollDeposits( + runner.config.network, + runner.config.masterAddress, + collateralBalance(state), + { + emit: async (deposit) => { + await events.append('deposit', { + id: deposit.id, + state: deposit.state, + }); + }, + }, + ); + } + }) + .catch(async (error) => { + await events.append('error', normalizeError(error).toJSON()); + }); + }; + tick(); + const timer = setInterval(tick, 30_000); + runner.onShutdown(async () => { + clearInterval(timer); + await queue; + }); } diff --git a/src/runner/lifecycle.ts b/src/runner/lifecycle.ts index 58c2b20..658a944 100644 --- a/src/runner/lifecycle.ts +++ b/src/runner/lifecycle.ts @@ -1,33 +1,180 @@ -import type {Subscriptions} from './subscriptions.js'; -import {open,readFile,unlink,mkdir} from 'node:fs/promises'; -import {join} from 'node:path'; -import {spawn} from 'node:child_process'; -import {stateDir,type Config} from '../core/config.js'; -import {Logger} from '../core/logger.js'; -import {privyClient,privySigner} from '../wallet/privy.js'; -import type {Signer} from '../core/hlclient.js'; -import {AgentCliError} from '../errors/index.js'; -export interface RunnerInfo {pid:number;startedAt:number;network:Config['network'];} -export const runnerPidPath=()=>join(stateDir(),'runner.pid'); -export function isAlive(pid:number){try{process.kill(pid,0);return true;}catch(error){return (error as NodeJS.ErrnoException).code==='EPERM';}} -export async function runningInfo(path=runnerPidPath()):Promise{try{const info=JSON.parse(await readFile(path,'utf8')) as RunnerInfo;return Number.isInteger(info.pid)&&info.pid>0&&isAlive(info.pid)?info:null;}catch(error){if((error as NodeJS.ErrnoException).code==='ENOENT'||error instanceof SyntaxError) return null;throw error;}} +import type { Subscriptions } from './subscriptions.js'; +import { open, readFile, unlink, mkdir, chmod } from 'node:fs/promises'; +import { join } from 'node:path'; +import { spawn } from 'node:child_process'; +import { stateDir, atomicWrite, type Config } from '../core/config.js'; +import { Logger } from '../core/logger.js'; +import { privyClient, privySigner } from '../wallet/privy.js'; +import type { Signer } from '../core/hlclient.js'; +import { AgentCliError } from '../errors/index.js'; +export interface RunnerInfo { + pid: number; + startedAt: number; + network: Config['network']; + ready: boolean; +} +export const runnerPidPath = () => join(stateDir(), 'runner.pid'); +export function isAlive(pid: number) { + try { + process.kill(pid, 0); + return true; + } catch (error) { + return (error as NodeJS.ErrnoException).code === 'EPERM'; + } +} +export async function runningInfo( + path = runnerPidPath(), +): Promise { + try { + const info = JSON.parse(await readFile(path, 'utf8')) as RunnerInfo; + return Number.isInteger(info.pid) && info.pid > 0 && isAlive(info.pid) + ? info + : null; + } catch (error) { + if ( + (error as NodeJS.ErrnoException).code === 'ENOENT' || + error instanceof SyntaxError + ) + return null; + throw error; + } +} export class Runner { - subscriptions?:Subscriptions;readonly info:RunnerInfo;readonly logger:Logger;signer?:Signer;private timer?:NodeJS.Timeout;private cleanup:(()=>Promise)[]=[];private closing=false; - constructor(readonly config:Config,readonly directory=stateDir()){this.info={pid:process.pid,startedAt:Date.now(),network:config.network};this.logger=new Logger({level:config.logLevel,directory:join(directory,'logs')});} - async start(openSigner:()=>Promise=async()=>privySigner(this.config,await privyClient(this.config))){ - await mkdir(this.directory,{recursive:true,mode:0o700});const path=join(this.directory,'runner.pid'); - let handle;try{handle=await open(path,'wx',0o600);}catch(error){if((error as NodeJS.ErrnoException).code!=='EEXIST') throw error;const existing=await runningInfo(path);if(existing) return {existing:true,...existing};throw new AgentCliError('RUNNER_NOT_RUNNING','Stale runner lock; inspect and remove runner.pid before restart');} - try{await handle.writeFile(JSON.stringify(this.info));await handle.close();this.signer=await openSigner();this.timer=setInterval(()=>{},30_000);this.logger.log('info','Runner started',{...this.info});await this.logger.flush();return {existing:false,...this.info};}catch(error){await handle.close().catch(()=>{});await unlink(path).catch(()=>{});throw error;} - } - onShutdown(cleanup:()=>Promise){this.cleanup.push(cleanup);} - async shutdown(){if(this.closing) return;this.closing=true;if(this.timer) clearInterval(this.timer);for(const cleanup of this.cleanup.reverse()) await cleanup();this.signer=undefined;this.logger.log('info','Runner stopped');await this.logger.flush();await unlink(join(this.directory,'runner.pid')).catch(error=>{if((error as NodeJS.ErrnoException).code!=='ENOENT') throw error;});} - signals(){const handler=()=>{void this.shutdown().catch(error=>{process.stderr.write(String(error)+'\n');process.exitCode=1;});};process.once('SIGINT',handler);process.once('SIGTERM',handler);this.onShutdown(async()=>{process.off('SIGINT',handler);process.off('SIGTERM',handler);});} + subscriptions?: Subscriptions; + readonly info: RunnerInfo; + readonly logger: Logger; + signer?: Signer; + private timer?: NodeJS.Timeout; + private cleanup: (() => Promise)[] = []; + private closing = false; + constructor( + readonly config: Config, + readonly directory = stateDir(), + ) { + this.info = { + pid: process.pid, + startedAt: Date.now(), + network: config.network, + ready: false, + }; + this.logger = new Logger({ + level: config.logLevel, + directory: join(directory, 'logs'), + }); + } + async start( + openSigner: () => Promise = async () => + privySigner(this.config, await privyClient(this.config)), + ) { + await mkdir(this.directory, { recursive: true, mode: 0o700 }); + await chmod(this.directory, 0o700); + const path = join(this.directory, 'runner.pid'); + const startupLock = path + '.startup'; + let guard; + try { + guard = await open(startupLock, 'wx', 0o600); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'EEXIST') + throw new AgentCliError( + 'RUNNER_NOT_RUNNING', + 'Runner startup in progress; retry shortly', + ); + throw error; + } + let handle; + try { + const existing = await runningInfo(path); + if (existing) return { existing: true, ...existing }; + await unlink(path).catch((error) => { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + }); + handle = await open(path, 'wx', 0o600); + await handle.writeFile(JSON.stringify(this.info)); + await handle.close(); + } finally { + await guard.close(); + await unlink(startupLock); + } + try { + this.signer = await openSigner(); + this.timer = setInterval(() => {}, 30_000); + this.logger.log('info', 'Runner started', { ...this.info }); + await this.logger.flush(); + return { existing: false, ...this.info }; + } catch (error) { + await handle?.close().catch(() => {}); + await unlink(path).catch(() => {}); + throw error; + } + } + async markReady() { + this.info.ready = true; + await atomicWrite( + join(this.directory, 'runner.pid'), + JSON.stringify(this.info), + ); + } + onShutdown(cleanup: () => Promise) { + this.cleanup.push(cleanup); + } + async shutdown() { + if (this.closing) return; + this.closing = true; + if (this.timer) clearInterval(this.timer); + const errors: unknown[] = []; + for (const cleanup of this.cleanup.reverse()) + try { + await cleanup(); + } catch (error) { + errors.push(error); + } + this.signer = undefined; + this.logger.log('info', 'Runner stopped'); + await this.logger.flush(); + await unlink(join(this.directory, 'runner.pid')).catch((error) => { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + }); + if (activeRunner === this) activeRunner = undefined; + if (errors.length) + throw new AggregateError(errors, 'Runner cleanup failed'); + } + signals() { + const handler = () => { + void this.shutdown().catch((error) => { + process.stderr.write(String(error) + '\n'); + process.exitCode = 1; + }); + }; + process.once('SIGINT', handler); + process.once('SIGTERM', handler); + this.onShutdown(async () => { + process.off('SIGINT', handler); + process.off('SIGTERM', handler); + }); + } +} +export async function detachRunner(args: string[], path = runnerPidPath()) { + const existing = await runningInfo(path); + if (existing?.ready) return existing; + const compiled = + Boolean(process.versions.bun) && import.meta.url.includes('/$bunfs/'); + const child = spawn( + process.execPath, + compiled ? ['run', ...args] : [process.argv[1], 'run', ...args], + { detached: true, stdio: 'ignore', env: process.env }, + ); + child.unref(); + for (let i = 0; i < 600; i++) { + const info = await runningInfo(path); + if (info?.ready) return info; + await new Promise((resolve) => setTimeout(resolve, 50)); + } + throw new AgentCliError( + 'RUNNER_NOT_RUNNING', + 'Detached runner did not become ready', + ); } -export async function detachRunner(args:string[],path=runnerPidPath()){ - const existing=await runningInfo(path);if(existing) return existing; - const child=spawn(process.execPath,[process.argv[1],'run',...args],{detached:true,stdio:'ignore',env:process.env});child.unref(); - for(let i=0;i<100;i++){const info=await runningInfo(path);if(info) return info;await new Promise(resolve=>setTimeout(resolve,50));} - throw new AgentCliError('RUNNER_NOT_RUNNING','Detached runner did not become ready'); +export let activeRunner: Runner | undefined; +export function setActiveRunner(runner: Runner | undefined) { + activeRunner = runner; } -export let activeRunner:Runner|undefined; -export function setActiveRunner(runner:Runner|undefined){activeRunner=runner;} diff --git a/src/runner/rpc.ts b/src/runner/rpc.ts index f454b22..39eb289 100644 --- a/src/runner/rpc.ts +++ b/src/runner/rpc.ts @@ -1,50 +1,314 @@ -import {assertSafeInput} from '../wallet/safety.js'; -import {createServer,createConnection,type Server} from 'node:net'; -import {chmod,unlink} from 'node:fs/promises'; -import {join} from 'node:path'; -import {createHash} from 'node:crypto'; -import {z} from 'zod'; -import {stateDir,loadConfig} from '../core/config.js'; -import {AgentCliError,normalizeError} from '../errors/index.js'; -import {commands,type Context} from '../core/router.js'; -import type {Runner} from './lifecycle.js'; -export const socketPath=(directory=stateDir())=>process.platform==='win32'?`\\\\.\\pipe\\bloxwap-${createHash('sha256').update(directory).digest('hex').slice(0,24)}`:join(directory,'run.sock'); -const aliases:Record={'status.get':'status','balance.get':'balance','positions.get':'positions','orders.get':'orders','order.place':'order','order.cancel':'cancel','order.modify':'modify','leverage.set':'leverage','config.get':'config','runner.shutdown':'shutdown'}; -export const routedCommands=new Set(['status','balance','positions','orders','order','cancel','modify','leverage','rotate','revoke']); -const requestSchema=z.object({jsonrpc:z.literal('2.0'),id:z.union([z.string(),z.number(),z.null()]),method:z.string(),params:z.record(z.string(),z.unknown()).default({})}); -export async function rpcServer(runner:Runner,context:Context,execute?: (name:string,input:Record)=>Promise):Promise{ - const path=socketPath(runner.directory);let queue=Promise.resolve(); - const server=createServer(socket=>{ - let buffer='';socket.setEncoding('utf8');socket.setTimeout(30_000,()=>socket.destroy()); - socket.on('data',chunk=>{ - buffer+=chunk;if(Buffer.byteLength(buffer)>1_048_576){socket.destroy();return;} - let end:number;while((end=buffer.indexOf('\n'))>=0){const line=buffer.slice(0,end);buffer=buffer.slice(end+1);queue=queue.then(async()=>{ - let id:unknown=null; - try{const request=requestSchema.parse(JSON.parse(line));id=request.id;const name=aliases[request.method]??request.method; - if(name==='shutdown'){socket.write(JSON.stringify({jsonrpc:'2.0',id,result:{stopping:true}})+'\n');setImmediate(()=>{void runner.shutdown();});return;} - if(!routedCommands.has(name)&&name!=='config') throw new AgentCliError('CONFIG_INVALID','Unknown runner method'); - const command=commands.get(name);if(!command&&!execute) throw new AgentCliError('CONFIG_INVALID','Unavailable method'); - assertSafeInput(request.params);const input=name==='config'?{args:['get']}:request.params;const parsed=command?.schema.safeParse(input);if(parsed&&!parsed.success) throw new AgentCliError('CONFIG_INVALID','Invalid runner input'); - const data=execute?await execute(name,input):await command!.execute(parsed!.data as Record,{...context,runner:true}); - socket.write(JSON.stringify({jsonrpc:'2.0',id,result:data})+'\n'); - }catch(error){const normalized=error instanceof SyntaxError||error instanceof z.ZodError?new AgentCliError('CONFIG_INVALID','Invalid JSON-RPC request'):normalizeError(error);socket.write(JSON.stringify({jsonrpc:'2.0',id,error:{code:-32000,message:normalized.message,data:normalized.toJSON()}})+'\n');} - }).catch(error=>{runner.logger.log('error','RPC write failed',{error:String(error)});socket.destroy();});} - });socket.on('error',()=>{}); - }); - await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(path,()=>{server.off('error',reject);resolve();});}); - if(process.platform!=='win32') await chmod(path,0o600); - runner.onShutdown(async()=>{await queue;await new Promise(resolve=>server.close(()=>resolve()));if(process.platform!=='win32') await unlink(path).catch(()=>{});});return server; +import { assertSafeInput } from '../wallet/safety.js'; +import { + createServer, + createConnection, + type Server, + type Socket, +} from 'node:net'; +import { chmod, unlink, lstat } from 'node:fs/promises'; +import { join, resolve } from 'node:path'; +import { createHash } from 'node:crypto'; +import { z } from 'zod'; +import { stateDir, loadConfig } from '../core/config.js'; +import { AgentCliError, normalizeError } from '../errors/index.js'; +import { commands, type Context } from '../core/router.js'; +import { runningInfo, type Runner } from './lifecycle.js'; +export const socketPath = (directory = stateDir()) => + process.platform === 'win32' + ? `\\\\.\\pipe\\bloxwap-${createHash('sha256').update(directory).digest('hex').slice(0, 24)}` + : join(directory, 'run.sock'); +const aliases: Record = { + 'status.get': 'status', + 'market.get': 'market', + 'balance.get': 'balance', + 'positions.get': 'positions', + 'orders.get': 'orders', + 'order.place': 'order', + 'order.cancel': 'cancel', + 'order.modify': 'modify', + 'leverage.set': 'leverage', + 'config.get': 'config', + 'runner.shutdown': 'shutdown', +}; +export const routedCommands = new Set([ + 'market', + 'status', + 'balance', + 'positions', + 'orders', + 'order', + 'cancel', + 'modify', + 'leverage', + 'rotate', + 'revoke', +]); +const requestSchema = z.object({ + jsonrpc: z.literal('2.0'), + id: z.union([z.string(), z.number(), z.null()]), + method: z.string(), + params: z.record(z.string(), z.unknown()).default({}), +}); +export async function rpcServer( + runner: Runner, + context: Context, + execute?: (name: string, input: Record) => Promise, +): Promise { + const path = socketPath(runner.directory); + let queue = Promise.resolve(); + const sockets = new Set(); + let stopping = false; + const server = createServer((socket) => { + sockets.add(socket); + socket.once('close', () => sockets.delete(socket)); + let buffer = ''; + socket.setEncoding('utf8'); + socket.setTimeout(30_000, () => socket.destroy()); + socket.on('data', (chunk) => { + buffer += chunk; + if (Buffer.byteLength(buffer) > 1_048_576) { + socket.destroy(); + return; + } + let end: number; + while ((end = buffer.indexOf('\n')) >= 0) { + const line = buffer.slice(0, end); + buffer = buffer.slice(end + 1); + queue = queue + .then(async () => { + let id: unknown = null; + try { + if (stopping) + throw new AgentCliError( + 'RUNNER_NOT_RUNNING', + 'Runner stopping', + ); + const request = requestSchema.parse(JSON.parse(line)); + id = request.id; + const metadata = request.params.__context as + { configPath?: string; network?: string } | undefined; + if ( + metadata && + ((metadata.network && + metadata.network !== runner.config.network) || + (metadata.configPath && + resolve(metadata.configPath) !== + resolve( + context.configPath ?? join(stateDir(), 'config.json'), + ))) + ) + throw new AgentCliError( + 'CONFIG_INVALID', + 'Runner belongs to a different config or network', + ); + const name = aliases[request.method] ?? request.method; + if (name === 'shutdown') { + socket.write( + JSON.stringify({ + jsonrpc: '2.0', + id, + result: { stopping: true }, + }) + '\n', + ); + setImmediate(() => { + void runner.shutdown(); + }); + return; + } + if (!routedCommands.has(name) && name !== 'config') + throw new AgentCliError( + 'CONFIG_INVALID', + 'Unknown runner method', + ); + const command = commands.get(name); + if (!command && !execute) + throw new AgentCliError('CONFIG_INVALID', 'Unavailable method'); + assertSafeInput(request.params); + const { __context, ...params } = request.params; + void __context; + const input = name === 'config' ? { args: ['get'] } : params; + const parsed = command?.schema.safeParse(input); + if (parsed && !parsed.success) + throw new AgentCliError( + 'CONFIG_INVALID', + 'Invalid runner input', + ); + const data = execute + ? await execute(name, input) + : await command!.execute( + parsed!.data as Record, + { ...context, runner: true }, + ); + socket.write( + JSON.stringify({ jsonrpc: '2.0', id, result: data }) + '\n', + ); + } catch (error) { + const normalized = + error instanceof SyntaxError || error instanceof z.ZodError + ? new AgentCliError( + 'CONFIG_INVALID', + 'Invalid JSON-RPC request', + ) + : normalizeError(error); + socket.write( + JSON.stringify({ + jsonrpc: '2.0', + id, + error: { + code: -32000, + message: normalized.message, + data: normalized.toJSON(), + }, + }) + '\n', + ); + } + }) + .catch((error) => { + runner.logger.log('error', 'RPC write failed', { + error: String(error), + }); + socket.destroy(); + }); + } + }); + socket.on('error', () => {}); + }); + if (process.platform !== 'win32') { + try { + const stale = await lstat(path); + if (!stale.isSocket()) + throw new AgentCliError( + 'CONFIG_INVALID', + 'Runner socket path contains a non-socket file', + ); + await unlink(path); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + } + } + await new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(path, () => { + server.off('error', reject); + resolve(); + }); + }); + if (process.platform !== 'win32') await chmod(path, 0o600); + runner.onShutdown(async () => { + stopping = true; + const closed = new Promise((resolve) => + server.close(() => resolve()), + ); + await queue; + for (const socket of sockets) socket.destroy(); + await closed; + if (process.platform !== 'win32') await unlink(path).catch(() => {}); + }); + return server; } -export function rpcCall(method:string,params:Record={},path=socketPath()):Promise{ - return new Promise((resolve,reject)=>{const id=1;const socket=createConnection(path);let buffer='';socket.setEncoding('utf8');socket.setTimeout(15_000,()=>socket.destroy(new Error('Runner timeout'))); - socket.once('connect',()=>socket.write(JSON.stringify({jsonrpc:'2.0',id,method,params})+'\n')); - socket.on('data',chunk=>{buffer+=chunk;if(buffer.length>1_048_576){socket.destroy(new Error('Oversize runner response'));return;}if(!buffer.includes('\n')) return; - try{const response=JSON.parse(buffer.slice(0,buffer.indexOf('\n')));if(response.jsonrpc!=='2.0'||response.id!==id) throw new Error('Invalid runner response');if(response.error){const error=response.error.data;reject(new AgentCliError(error.code,error.message,error.details,error.retryable));}else resolve(response.result);socket.end();}catch(error){reject(error);socket.destroy();}}); - socket.once('error',error=>reject(new AgentCliError('RUNNER_NOT_RUNNING',error.message)));socket.once('end',()=>{if(!buffer.includes('\n')) reject(new AgentCliError('RUNNER_NOT_RUNNING','Runner disconnected'));}); - }); +export function rpcCall( + method: string, + params: Record = {}, + path = socketPath(), +): Promise { + return new Promise((resolve, reject) => { + const id = 1; + const socket = createConnection(path); + let buffer = ''; + socket.setEncoding('utf8'); + socket.setTimeout(15_000, () => + socket.destroy( + new AgentCliError( + 'NETWORK_ERROR', + 'Runner timed out; reconcile exchange state before retry', + ), + ), + ); + socket.once('connect', () => + socket.write( + JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n', + ), + ); + socket.on('data', (chunk) => { + buffer += chunk; + if (buffer.length > 1_048_576) { + socket.destroy(new Error('Oversize runner response')); + return; + } + if (!buffer.includes('\n')) return; + try { + const response = JSON.parse(buffer.slice(0, buffer.indexOf('\n'))); + if (response.jsonrpc !== '2.0' || response.id !== id) + throw new Error('Invalid runner response'); + if (response.error) { + const error = response.error.data; + reject( + new AgentCliError( + error.code, + error.message, + error.details, + error.retryable, + ), + ); + } else resolve(response.result); + socket.end(); + } catch (error) { + reject(error); + socket.destroy(); + } + }); + socket.once('error', (error) => + reject( + error instanceof AgentCliError + ? error + : new AgentCliError( + ['ENOENT', 'ECONNREFUSED'].includes( + (error as NodeJS.ErrnoException).code ?? '', + ) + ? 'RUNNER_NOT_RUNNING' + : 'NETWORK_ERROR', + error.message, + ), + ), + ); + socket.once('end', () => { + if (!buffer.includes('\n')) + reject( + new AgentCliError( + 'NETWORK_ERROR', + 'Runner disconnected; reconcile exchange state', + ), + ); + }); + }); } -export async function routeToRunner(name:string,input:Record,context:Context){ - if(context.runner||!routedCommands.has(name)) return {routed:false as const}; - const config=await loadConfig(context.configPath); - try{return {routed:true as const,data:await rpcCall(name,{...input,...(context.network?{network:context.network}:{})})};}catch(error){if(error instanceof AgentCliError&&error.code==='RUNNER_NOT_RUNNING'&&config.runnerFallback) return {routed:false as const};throw error;} +export async function routeToRunner( + name: string, + input: Record, + context: Context, +) { + if (context.runner || !routedCommands.has(name)) + return { routed: false as const }; + const config = await loadConfig(context.configPath); + try { + return { + routed: true as const, + data: await rpcCall(name, { + ...input, + __context: { + configPath: context.configPath ?? join(stateDir(), 'config.json'), + network: context.network ?? config.network, + }, + }), + }; + } catch (error) { + if ( + error instanceof AgentCliError && + error.code === 'RUNNER_NOT_RUNNING' && + config.runnerFallback && + !(await runningInfo()) + ) + return { routed: false as const }; + throw error; + } } diff --git a/src/runner/subscriptions.ts b/src/runner/subscriptions.ts index d6c9200..a9e0e30 100644 --- a/src/runner/subscriptions.ts +++ b/src/runner/subscriptions.ts @@ -1,37 +1,178 @@ -import {SubscriptionClient,WebSocketTransport} from '@bloxwap/hyperliquid'; +import { SubscriptionClient, WebSocketTransport } from '@bloxwap/hyperliquid'; import WebSocket from 'ws'; -import {readFile} from 'node:fs/promises'; -import {join} from 'node:path'; -import {atomicWrite,type Config} from '../core/config.js'; -import {hlInfo} from '../core/http.js'; -import {normalizeError} from '../errors/index.js'; -import type {Runner} from './lifecycle.js'; -interface Fill {tid:number;time:number;[key:string]:unknown;} -interface Backend {subscribe:(type:string,params:Record,listener:(data:unknown)=>void)=>Promise<{unsubscribe:()=>Promise}>;onState:(listener:(state:string)=>void)=>void;close:()=>Promise;gap:()=>Promise;} -export function subscriptionSet(config:Config){return ['userEvents','userFills','orderUpdates','clearinghouseState',...(config.allowedMarkets.length?['allMids',...config.allowedMarkets.map(coin=>`l2Book:${coin}`)]:[])];} +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { atomicWrite, type Config } from '../core/config.js'; +import { hlInfo } from '../core/http.js'; +import { normalizeError } from '../errors/index.js'; +import type { Runner } from './lifecycle.js'; +interface Fill { + tid: number; + time: number; + [key: string]: unknown; +} +interface Backend { + subscribe: ( + type: string, + params: Record, + listener: (data: unknown) => void, + ) => Promise<{ unsubscribe: () => Promise }>; + onState: (listener: (state: string) => void) => void; + close: () => Promise; + gap: (startTime?: number) => Promise; +} +export function subscriptionSet(config: Config) { + return [ + 'userEvents', + 'userFills', + 'orderUpdates', + 'clearinghouseState', + ...(config.allowedMarkets.length + ? ['allMids', ...config.allowedMarkets.map((coin) => `l2Book:${coin}`)] + : []), + ]; +} export class Subscriptions { - state='connecting';lastMessage=Date.now();lastFill:number|null=null;private seen=new Set();private queue=Promise.resolve();private subscriptions:{unsubscribe:()=>Promise}[]=[]; - constructor(private runner:Runner,private backend:Backend,private emit:(type:string,data:unknown)=>Promise){} - async start(){ - const path=join(this.runner.directory,'subscriptions.json');try{const saved=JSON.parse(await readFile(path,'utf8'));this.seen=new Set(saved.seen);this.lastFill=saved.lastFill;}catch(error){if((error as NodeJS.ErrnoException).code!=='ENOENT') throw error;} - this.backend.onState(state=>{const reconnect=this.state!=='connecting'&&state==='connected';this.state=state;this.lastMessage=Date.now();if(reconnect) this.enqueue(async()=>{for(const fill of await this.backend.gap()) await this.fill(fill);await this.emit('runner_state',{wsState:state});});}); - for(const name of subscriptionSet(this.runner.config)){ - const [type,coin]=name.split(':');const params=coin?{coin}:type==='allMids'?{}:{user:this.runner.config.masterAddress}; - this.subscriptions.push(await this.backend.subscribe(type,params,data=>{this.lastMessage=Date.now();this.enqueue(async()=>{ - if(type==='userFills'){for(const fill of (data as {fills:Fill[]}).fills) await this.fill(fill);} - else if(type==='userEvents'&&'fills' in (data as object)){for(const fill of (data as {fills:Fill[]}).fills) await this.fill(fill);} - else if(type==='orderUpdates') await this.emit('order_update',data);else if(type==='clearinghouseState') await this.emit('position_change',data); - });})); - } - this.runner.onShutdown(async()=>{await this.queue;await Promise.all(this.subscriptions.map(s=>s.unsubscribe()));await this.backend.close();}); - } - private enqueue(operation:()=>Promise){this.queue=this.queue.then(operation).catch(async error=>{await this.emit('error',normalizeError(error).toJSON());});} - async fill(fill:Fill){if(this.seen.has(fill.tid)) return;this.seen.add(fill.tid);this.lastFill=fill.time;await this.emit('fill',fill);await atomicWrite(join(this.runner.directory,'subscriptions.json'),JSON.stringify({seen:[...this.seen].slice(-10000),lastFill:this.lastFill}));} - async flush(){await this.queue;} + state = 'connecting'; + lastMessage = Date.now(); + lastFill: number | null = null; + private seen = new Set(); + private queue = Promise.resolve(); + private subscriptions: { unsubscribe: () => Promise }[] = []; + constructor( + private runner: Runner, + private backend: Backend, + private emit: (type: string, data: unknown) => Promise, + ) {} + async start() { + const path = join(this.runner.directory, 'subscriptions.json'); + try { + const saved = JSON.parse(await readFile(path, 'utf8')); + this.seen = new Set(saved.seen); + this.lastFill = saved.lastFill; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + } + this.backend.onState((state) => { + const reconnect = this.state !== 'connecting' && state === 'connected'; + this.state = state; + this.lastMessage = Date.now(); + if (reconnect) + this.enqueue(async () => { + for (const fill of await this.backend.gap( + this.lastFill ?? Date.now() - 60_000, + )) + await this.fill(fill); + await this.emit('runner_state', { wsState: state }); + }); + }); + for (const name of subscriptionSet(this.runner.config)) { + const [type, coin] = name.split(':'); + const params = coin + ? { coin } + : type === 'allMids' + ? {} + : { user: this.runner.config.masterAddress }; + this.subscriptions.push( + await this.backend.subscribe(type, params, (data) => { + this.lastMessage = Date.now(); + this.enqueue(async () => { + if (type === 'userFills') { + for (const fill of (data as { fills: Fill[] }).fills) + await this.fill(fill); + } else if (type === 'userEvents' && 'fills' in (data as object)) { + for (const fill of (data as { fills: Fill[] }).fills) + await this.fill(fill); + } else if (type === 'orderUpdates') + await this.emit('order_update', data); + else if (type === 'clearinghouseState') + await this.emit('position_change', data); + }); + }), + ); + } + this.runner.onShutdown(async () => { + await this.queue; + await Promise.all(this.subscriptions.map((s) => s.unsubscribe())); + await this.backend.close(); + }); + } + private enqueue(operation: () => Promise) { + this.queue = this.queue.then(operation).catch(async (error) => { + await this.emit('error', normalizeError(error).toJSON()); + }); + } + async fill(fill: Fill) { + if (this.seen.has(fill.tid)) return; + this.seen.add(fill.tid); + this.lastFill = Math.max(this.lastFill ?? 0, fill.time); + if (this.seen.size > 10000) + this.seen.delete(this.seen.values().next().value!); + await this.emit('fill', fill); + await atomicWrite( + join(this.runner.directory, 'subscriptions.json'), + JSON.stringify({ + seen: [...this.seen].slice(-10000), + lastFill: this.lastFill, + }), + ); + } + async flush() { + await this.queue; + } +} +export function sdkBackend(config: Config): Backend { + const transport = new WebSocketTransport({ + isTestnet: config.network === 'testnet', + resubscribe: true, + webSocketFactory: (url, protocols) => + new WebSocket(url, protocols) as unknown as globalThis.WebSocket, + }); + const client = new SubscriptionClient({ transport }) as unknown as Record< + string, + ( + params: Record, + listener: (data: unknown) => void, + ) => Promise<{ unsubscribe: () => Promise }> + >; + return { + subscribe: (type, params, listener) => client[type](params, listener), + onState: (listener) => + transport.events.addEventListener('connectionstatechange', (event) => + listener(event.detail), + ), + close: async () => { + await transport.close(); + }, + gap: async (startTime = Date.now() - 60_000) => { + const fills: Fill[] = []; + const endTime = Date.now(); + for (let page = 0; page < 100; page++) { + const batch = await hlInfo(config.network, 'userFillsByTime', { + user: config.masterAddress, + startTime, + endTime, + }); + fills.push(...batch); + if (batch.length < 2000) return fills; + const last = Math.max(...batch.map((fill) => fill.time)); + if (last <= startTime) + throw new Error('Fill backfill cannot advance; inspect gap'); + startTime = last; + } + throw new Error('Fill backfill exceeded pagination bound'); + }, + }; } -export function sdkBackend(config:Config):Backend { - const transport=new WebSocketTransport({isTestnet:config.network==='testnet',resubscribe:true,webSocketFactory:(url,protocols)=>new WebSocket(url,protocols) as unknown as globalThis.WebSocket}); - const client=new SubscriptionClient({transport}) as unknown as Record,listener:(data:unknown)=>void)=>Promise<{unsubscribe:()=>Promise}>>; - return {subscribe:(type,params,listener)=>client[type](params,listener),onState:listener=>transport.events.addEventListener('connectionstatechange',event=>listener(event.detail)),close:async()=>{await transport.close();},gap:()=>hlInfo(config.network,'userFills',{user:config.masterAddress})}; +export async function startSubscriptions( + runner: Runner, + emit: (type: string, data: unknown) => Promise, +) { + const subscriptions = new Subscriptions( + runner, + sdkBackend(runner.config), + emit, + ); + await subscriptions.start(); + return subscriptions; } -export async function startSubscriptions(runner:Runner,emit:(type:string,data:unknown)=>Promise){const subscriptions=new Subscriptions(runner,sdkBackend(runner.config),emit);await subscriptions.start();return subscriptions;} diff --git a/src/wallet/approval.ts b/src/wallet/approval.ts index 1e9ec1e..1bbff2c 100644 --- a/src/wallet/approval.ts +++ b/src/wallet/approval.ts @@ -1,19 +1,84 @@ -import {assertSafeInput} from './safety.js'; -import {ApproveAgentTypes} from '@bloxwap/hyperliquid/api/exchange/approveAgent'; -import {ApproveBuilderFeeTypes} from '@bloxwap/hyperliquid/api/exchange/approveBuilderFee'; -import {AgentCliError} from '../errors/index.js'; -import {jsonRequest,hlEndpoint} from '../core/http.js'; -export interface Approval {action:Record;typedData:Record;} -export function approval(network:'mainnet'|'testnet',type:'approveAgent'|'approveBuilderFee',fields:Record,nonce=Date.now()):Approval{ - const types=type==='approveAgent'?ApproveAgentTypes:ApproveBuilderFeeTypes; - const action={type,signatureChainId:'0xa4b1',hyperliquidChain:network==='mainnet'?'Mainnet':'Testnet',...fields,nonce}; - const {type:_type,signatureChainId:_chain,...message}=action;void _type;void _chain; - return {action,typedData:{domain:{name:'HyperliquidSignTransaction',version:'1',chainId:42161,verifyingContract:'0x'+'0'.repeat(40)},types,primaryType:Object.keys(types)[0],message}}; +import { nextNonce } from '../core/nonce.js'; +import type { Config } from '../core/config.js'; +import { assertSafeInput } from './safety.js'; +import { ApproveAgentTypes } from '@bloxwap/hyperliquid/api/exchange/approveAgent'; +import { ApproveBuilderFeeTypes } from '@bloxwap/hyperliquid/api/exchange/approveBuilderFee'; +import { AgentCliError } from '../errors/index.js'; +import { jsonRequest, hlEndpoint } from '../core/http.js'; +export interface Approval { + action: Record; + typedData: Record; } -export async function submitApproval(network:'mainnet'|'testnet',pending:Approval,signature:string){ - if(!/^0x[0-9a-fA-F]{130}$/.test(signature)) throw new AgentCliError('CONFIG_INVALID','Expected 65-byte signature'); - const recovery=parseInt(signature.slice(-2),16);const v=recovery<27?recovery+27:recovery;if(v!==27&&v!==28) throw new AgentCliError('CONFIG_INVALID','Invalid signature recovery'); - const response=await jsonRequest(`${hlEndpoint(network)}/exchange`,{action:pending.action,nonce:pending.action.nonce,signature:{r:signature.slice(0,66),s:'0x'+signature.slice(66,130),v}}) as {status:string;response:unknown}; - if(response.status!=='ok') throw new AgentCliError('UPSTREAM_ERROR','Approval failed');return response; +export function approval( + network: 'mainnet' | 'testnet', + type: 'approveAgent' | 'approveBuilderFee', + fields: Record, + nonce = Date.now(), +): Approval { + const types = + type === 'approveAgent' ? ApproveAgentTypes : ApproveBuilderFeeTypes; + const action = { + type, + signatureChainId: '0xa4b1', + hyperliquidChain: network === 'mainnet' ? 'Mainnet' : 'Testnet', + ...fields, + nonce, + }; + const { type: _type, signatureChainId: _chain, ...message } = action; + void _type; + void _chain; + return { + action, + typedData: { + domain: { + name: 'HyperliquidSignTransaction', + version: '1', + chainId: 42161, + verifyingContract: '0x' + '0'.repeat(40), + }, + types, + primaryType: Object.keys(types)[0], + message, + }, + }; +} +export async function submitApproval( + network: 'mainnet' | 'testnet', + pending: Approval, + signature: string, +) { + if (!/^0x[0-9a-fA-F]{130}$/.test(signature)) + throw new AgentCliError('CONFIG_INVALID', 'Expected 65-byte signature'); + const recovery = parseInt(signature.slice(-2), 16); + const v = recovery < 27 ? recovery + 27 : recovery; + if (v !== 27 && v !== 28) + throw new AgentCliError('CONFIG_INVALID', 'Invalid signature recovery'); + const response = (await jsonRequest(`${hlEndpoint(network)}/exchange`, { + action: pending.action, + nonce: pending.action.nonce, + signature: { + r: signature.slice(0, 66), + s: '0x' + signature.slice(66, 130), + v, + }, + })) as { status: string; response: unknown }; + if (response.status !== 'ok') + throw new AgentCliError('UPSTREAM_ERROR', 'Approval failed'); + return response; +} +export const rejectMasterKey = assertSafeInput; + +export async function newApproval( + config: Config, + type: 'approveAgent' | 'approveBuilderFee', + fields: Record, +) { + if (!config.masterAddress) + throw new AgentCliError('CONFIG_INVALID', 'Master address required'); + return approval( + config.network, + type, + fields, + await nextNonce(config.masterAddress), + ); } -export const rejectMasterKey=assertSafeInput; diff --git a/src/wallet/expiry.ts b/src/wallet/expiry.ts index a0fd94b..f2e403c 100644 --- a/src/wallet/expiry.ts +++ b/src/wallet/expiry.ts @@ -1,15 +1,50 @@ -import {readFile} from 'node:fs/promises'; -import {join} from 'node:path'; -import {atomicWrite,type Config,stateDir} from '../core/config.js'; -import {AgentCliError} from '../errors/index.js'; -import {events} from '../runner/events.js'; -export function assertAgentValid(config:Config,now=Date.now()){ - if(!config.agentExpiry||config.agentExpiry<=now) throw new AgentCliError('AGENT_EXPIRED','Rotate expired agent'); +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { atomicWrite, type Config, stateDir } from '../core/config.js'; +import { AgentCliError } from '../errors/index.js'; +import { events } from '../runner/events.js'; +export function assertAgentValid(config: Config, now = Date.now()) { + if (!config.agentExpiry || config.agentExpiry <= now) + throw new AgentCliError('AGENT_EXPIRED', 'Rotate expired agent'); } -export async function expiryWarnings(config:Config,options:{now?:number;path?:string;emit?:(data:unknown)=>Promise}={}){ - if(!config.agentExpiry) return [];const now=options.now??Date.now();const path=options.path??join(stateDir(),'expiry-warnings.json');let emitted:string[]=[]; - try{emitted=JSON.parse(await readFile(path,'utf8'));}catch(error){if((error as NodeJS.ErrnoException).code!=='ENOENT') throw error;} - const days=(config.agentExpiry-now)/86400000;const warnings=[]; - for(const threshold of config.expiryWarningDays){const key=`${config.agentAddress}:${config.agentExpiry}:${threshold}`;if(days<=threshold&&!emitted.includes(key)){const warning={agentAddress:config.agentAddress,expiry:config.agentExpiry,daysRemaining:String(Math.max(0,days)),threshold:String(threshold)};await (options.emit??(async data=>{await events.append('agent_expiry_warning',data);}))(warning);emitted.push(key);warnings.push(warning);}} - if(warnings.length) await atomicWrite(path,JSON.stringify(emitted));return warnings; +export async function expiryWarnings( + config: Config, + options: { + now?: number; + path?: string; + emit?: (data: unknown) => Promise; + } = {}, +) { + if (!config.agentExpiry) return []; + const now = options.now ?? Date.now(); + const path = options.path ?? join(stateDir(), 'expiry-warnings.json'); + let emitted: string[] = []; + try { + emitted = JSON.parse(await readFile(path, 'utf8')); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + } + const days = (config.agentExpiry - now) / 86400000; + const warnings = []; + for (const threshold of config.expiryWarningDays) { + const key = `${config.agentAddress}:${config.agentExpiry}:${threshold}`; + if (days <= threshold && !emitted.includes(key)) { + const warning = { + agentAddress: config.agentAddress, + expiry: config.agentExpiry, + daysRemaining: String(Math.max(0, days)), + threshold: String(threshold), + }; + await ( + options.emit ?? + (async (data) => { + await events.append('agent_expiry_warning', data); + }) + )(warning); + emitted.push(key); + warnings.push(warning); + } + } + if (warnings.length) await atomicWrite(path, JSON.stringify(emitted)); + return warnings; } diff --git a/src/wallet/policy.ts b/src/wallet/policy.ts index 631c06f..7940383 100644 --- a/src/wallet/policy.ts +++ b/src/wallet/policy.ts @@ -2,18 +2,50 @@ import type { PrivyClient } from '@privy-io/node'; import template from '../../policies/agent-trade-only.json' with { type: 'json' }; import { AgentCliError } from '../errors/index.js'; import { mapPrivyError } from '../errors/mappers.js'; -import { saveConfig,type Config } from '../core/config.js'; -export const policyVersion=1; -const allowed=new Set(['order','cancel','cancelByCloid','batchModify','modify','updateLeverage','scheduleCancel','noop']); -export function assertTradeAction(action:string){if(!allowed.has(action)) throw new AgentCliError('POLICY_REJECTED',`Agent cannot sign ${action}`);} -export function policyFor(network:Config['network']) { - const policy=structuredClone(template);policy.rules[0].conditions[2].value=network==='mainnet'?'a':'b';return policy; +import { saveConfig, type Config } from '../core/config.js'; +export const policyVersion = 1; +const allowed = new Set([ + 'order', + 'cancel', + 'cancelByCloid', + 'batchModify', + 'modify', + 'updateLeverage', + 'scheduleCancel', + 'noop', +]); +export function assertTradeAction(action: string) { + if (!allowed.has(action)) + throw new AgentCliError('POLICY_REJECTED', `Agent cannot sign ${action}`); } -export async function applyPolicy(config:Config,client:PrivyClient,path?:string){ - if(config.policyId) return config.policyId; - try{ - const policy=await client.policies().create(policyFor(config.network) as Parameters['create']>[0]); - if(config.privyWalletId) await client.wallets().update(config.privyWalletId,{policy_ids:[policy.id]}); - config.policyId=policy.id;config.policyVersion=policyVersion;await saveConfig(config,path);return policy.id; - }catch(error){throw mapPrivyError(error);} +export function policyFor(network: Config['network']) { + const policy = structuredClone(template); + policy.rules[0].conditions[2].value = network === 'mainnet' ? 'a' : 'b'; + return policy; +} +export async function applyPolicy( + config: Config, + client: PrivyClient, + path?: string, +) { + if (config.policyId) return config.policyId; + try { + const policy = await client + .policies() + .create( + policyFor(config.network) as Parameters< + ReturnType['create'] + >[0], + ); + if (config.privyWalletId) + await client + .wallets() + .update(config.privyWalletId, { policy_ids: [policy.id] }); + config.policyId = policy.id; + config.policyVersion = policyVersion; + await saveConfig(config, path); + return policy.id; + } catch (error) { + throw mapPrivyError(error); + } } diff --git a/src/wallet/privy.ts b/src/wallet/privy.ts index f3c9fd2..c9a8535 100644 --- a/src/wallet/privy.ts +++ b/src/wallet/privy.ts @@ -1,36 +1,131 @@ -import {signatureAudit} from '../core/audit.js'; +import { signatureAudit } from '../core/audit.js'; import { PrivyClient, generateP256KeyPair } from '@privy-io/node'; -import { readFile,stat } from 'node:fs/promises'; +import { randomUUID, createHash } from 'node:crypto'; +import { readFile, stat } from 'node:fs/promises'; import { join } from 'node:path'; -import { atomicWrite,saveConfig,stateDir,type Config } from '../core/config.js'; +import { + atomicWrite, + saveConfig, + stateDir, + type Config, +} from '../core/config.js'; import type { Signer } from '../core/hlclient.js'; import { AgentCliError } from '../errors/index.js'; import { mapPrivyError } from '../errors/mappers.js'; -export async function resolveSecret(ref:string|undefined,env=process.env):Promise{ - if(!ref) throw new AgentCliError('CONFIG_INVALID','Missing secret reference'); - if(ref.startsWith('env:')) {const value=env[ref.slice(4)];if(value) return value;} - if(ref.startsWith('file:')) {const path=ref.slice(5);const info=await stat(path);if(process.platform!=='win32'&&(info.mode&0o077)!==0) throw new AgentCliError('CONFIG_INVALID','Secret file must have mode 0600');return (await readFile(path,'utf8')).trim();} - throw new AgentCliError('CONFIG_INVALID','Secret unavailable'); +export async function resolveSecret( + ref: string | undefined, + env = process.env, +): Promise { + if (!ref) + throw new AgentCliError('CONFIG_INVALID', 'Missing secret reference'); + if (ref.startsWith('env:')) { + const value = env[ref.slice(4)]; + if (value) return value; + } + if (ref.startsWith('file:')) { + const path = ref.slice(5); + const info = await stat(path); + if (process.platform !== 'win32' && (info.mode & 0o077) !== 0) + throw new AgentCliError( + 'CONFIG_INVALID', + 'Secret file must have mode 0600', + ); + return (await readFile(path, 'utf8')).trim(); + } + throw new AgentCliError('CONFIG_INVALID', 'Secret unavailable'); } -export async function privyClient(config:Config){ - if(!config.privyAppId) throw new AgentCliError('CONFIG_INVALID','Set privyAppId'); - return new PrivyClient({appId:config.privyAppId,appSecret:await resolveSecret(config.privyAppSecretRef??'env:BLOXWAP_PRIVY_APP_SECRET')}); +export async function privyClient(config: Config) { + if (!config.privyAppId) + throw new AgentCliError('CONFIG_INVALID', 'Set privyAppId'); + return new PrivyClient({ + appId: config.privyAppId, + appSecret: await resolveSecret( + config.privyAppSecretRef ?? 'env:BLOXWAP_PRIVY_APP_SECRET', + ), + }); } -export async function createAgent(config:Config,client:PrivyClient,path?:string,policyId?:string) { - try { - if(config.privyWalletId){const wallet=await client.wallets().get(config.privyWalletId);config.agentAddress=wallet.address;await saveConfig(config,path);return wallet;} - const pair=await generateP256KeyPair();const keyPath=join(stateDir(),`authorization-${Date.now()}.key`);await atomicWrite(keyPath,pair.privateKey); - const wallet=await client.wallets().create({chain_type:'ethereum',owner:{public_key:pair.publicKey},policy_ids:policyId?[policyId]:[],idempotency_key:`agent-${pair.publicKey}`}); - config.privyWalletId=wallet.id;config.agentAddress=wallet.address;config.authorizationKeyRef=`file:${keyPath}`;await saveConfig(config,path);return wallet; - }catch(error){throw mapPrivyError(error);} -} -export async function privySigner(config:Config,client:PrivyClient):Promise{ - if(!config.agentAddress||!config.privyWalletId) throw new AgentCliError('AGENT_NOT_APPROVED','Initialize agent wallet first'); - const authorizationKey=await resolveSecret(config.authorizationKeyRef); - return {address:config.agentAddress as `0x${string}`,signTypedData:async data=>{ +export async function createAgent( + config: Config, + client: PrivyClient, + path?: string, + policyId?: string, +) { try { - const signed=await client.wallets().ethereum().signTypedData(config.privyWalletId!,{params:{typed_data:{domain:data.domain,types:Object.fromEntries(Object.entries(data.types).map(([key,fields])=>[key,[...fields]])),primary_type:data.primaryType,message:data.message}},authorization_context:{authorization_private_keys:[authorizationKey]}}); - await signatureAudit(signed.signature);return signed.signature as `0x${string}`; - }catch(error){throw mapPrivyError(error);} - }}; + if (config.privyWalletId) { + const wallet = await client.wallets().get(config.privyWalletId); + if (!config.authorizationKeyRef) { + const pair = await generateP256KeyPair(); + const keyPath = join(stateDir(), `authorization-${randomUUID()}.key`); + await atomicWrite(keyPath, pair.privateKey); + await client + .wallets() + .update(wallet.id, { owner: { public_key: pair.publicKey } }); + config.authorizationKeyRef = `file:${keyPath}`; + } + config.agentAddress = wallet.address; + await saveConfig(config, path); + return wallet; + } + const pair = await generateP256KeyPair(); + const keyPath = join(stateDir(), `authorization-${randomUUID()}.key`); + await atomicWrite(keyPath, pair.privateKey); + const wallet = await client + .wallets() + .create({ + chain_type: 'ethereum', + owner: { public_key: pair.publicKey }, + policy_ids: policyId ? [policyId] : [], + idempotency_key: `agent-${createHash('sha256').update(pair.publicKey).digest('hex')}`, + }); + config.privyWalletId = wallet.id; + config.agentAddress = wallet.address; + config.authorizationKeyRef = `file:${keyPath}`; + await saveConfig(config, path); + return wallet; + } catch (error) { + throw mapPrivyError(error); + } +} +export async function privySigner( + config: Config, + client: PrivyClient, +): Promise { + if (!config.agentAddress || !config.privyWalletId) + throw new AgentCliError( + 'AGENT_NOT_APPROVED', + 'Initialize agent wallet first', + ); + const authorizationKey = await resolveSecret(config.authorizationKeyRef); + return { + address: config.agentAddress as `0x${string}`, + signTypedData: async (data) => { + try { + const signed = await client + .wallets() + .ethereum() + .signTypedData(config.privyWalletId!, { + params: { + typed_data: { + domain: data.domain, + types: Object.fromEntries( + Object.entries(data.types).map(([key, fields]) => [ + key, + [...fields], + ]), + ), + primary_type: data.primaryType, + message: data.message, + }, + }, + authorization_context: { + authorization_private_keys: [authorizationKey], + }, + }); + await signatureAudit(signed.signature); + return signed.signature as `0x${string}`; + } catch (error) { + throw mapPrivyError(error); + } + }, + }; } diff --git a/src/wallet/safety.ts b/src/wallet/safety.ts index b87e96d..dc0ff86 100644 --- a/src/wallet/safety.ts +++ b/src/wallet/safety.ts @@ -1,12 +1,38 @@ -import {wordlist} from '@scure/bip39/wordlists/english.js'; -import {AgentCliError} from '../errors/index.js'; -const words=new Set(wordlist); -export function resemblesSecret(value:string){ - const trimmed=value.trim();if(/^(?:0x)?[0-9a-fA-F]{64}$/.test(trimmed)) return true; - const tokens=trimmed.toLowerCase().split(/\s+/);return [12,15,18,21,24].includes(tokens.length)&&tokens.every(word=>words.has(word)); +import { wordlist } from '@scure/bip39/wordlists/english.js'; +import { AgentCliError } from '../errors/index.js'; +const words = new Set(wordlist); +export function resemblesSecret(value: string) { + const trimmed = value.trim(); + if ( + /(?:^|[^0-9a-fA-F])(?:0x)?[0-9a-fA-F]{64}(?=$|[^0-9a-fA-F])/.test(trimmed) + ) + return true; + const tokens = trimmed + .toLowerCase() + .split(/\s+/) + .map((token) => token.replace(/^[^a-z]+|[^a-z]+$/g, '')); + for (let start = 0; start + 12 <= tokens.length; start++) + if (tokens.slice(start, start + 12).every((word) => words.has(word))) + return true; + return ( + [12, 15, 18, 21, 24].includes(tokens.length) && + tokens.every((word) => words.has(word)) + ); } -export function assertSafeInput(value:unknown):void{ - if(typeof value==='string'&&resemblesSecret(value)) throw new AgentCliError('POLICY_REJECTED','Private keys and seed phrases are forbidden; use external wallet signing'); - if(Array.isArray(value)) value.forEach(assertSafeInput); - else if(value&&typeof value==='object') for(const [key,field] of Object.entries(value)){if(/private.?key|seed.?phrase|mnemonic/i.test(key)) throw new AgentCliError('POLICY_REJECTED','Secret key input is forbidden');assertSafeInput(field);} +export function assertSafeInput(value: unknown): void { + if (typeof value === 'string' && resemblesSecret(value)) + throw new AgentCliError( + 'POLICY_REJECTED', + 'Private keys and seed phrases are forbidden; use external wallet signing', + ); + if (Array.isArray(value)) value.forEach(assertSafeInput); + else if (value && typeof value === 'object') + for (const [key, field] of Object.entries(value)) { + if (/private.?key|seed.?phrase|mnemonic/i.test(key)) + throw new AgentCliError( + 'POLICY_REJECTED', + 'Secret key input is forbidden', + ); + assertSafeInput(field); + } } diff --git a/test/amounts.test.ts b/test/amounts.test.ts index 1baa964..e14922c 100644 --- a/test/amounts.test.ts +++ b/test/amounts.test.ts @@ -1,6 +1,17 @@ -import {expect,test} from 'vitest'; -import {parseUsdc,formatUsdc,toHyperCore,fromHyperCore} from '../src/funding/amounts.js'; -test('exact bigint round trips across decimals and large values',()=>{ - for(const decimals of [0,6,8]) for(let i=0n;i<1000n;i+=7n){const value=i*123456789012345678901n;expect(parseUsdc(formatUsdc(value,decimals),decimals)).toBe(value);} - expect(fromHyperCore(toHyperCore(1234567n))).toBe(1234567n);expect(()=>fromHyperCore(101n)).toThrow();expect(()=>parseUsdc('1.0000001',6)).toThrow(); +import { expect, test } from 'vitest'; +import { + parseUsdc, + formatUsdc, + toHyperCore, + fromHyperCore, +} from '../src/funding/amounts.js'; +test('exact bigint round trips across decimals and large values', () => { + for (const decimals of [0, 6, 8]) + for (let i = -1000n; i < 1000n; i += 7n) { + const value = i * 123456789012345678901n; + expect(parseUsdc(formatUsdc(value, decimals), decimals)).toBe(value); + } + expect(fromHyperCore(toHyperCore(1234567n))).toBe(1234567n); + expect(() => fromHyperCore(101n)).toThrow(); + expect(() => parseUsdc('1.0000001', 6)).toThrow(); }); diff --git a/test/approval.test.ts b/test/approval.test.ts index 2806118..508d5e8 100644 --- a/test/approval.test.ts +++ b/test/approval.test.ts @@ -1,7 +1,17 @@ -import {expect,test} from 'vitest'; -import {approval,rejectMasterKey} from '../src/wallet/approval.js'; -test('user signature payload follows SDK types and master secrets are refused',()=>{ - const result=approval('testnet','approveAgent',{agentAddress:'0x'+'a'.repeat(40),agentName:'agent'},123); - expect(result.typedData).toMatchObject({primaryType:'HyperliquidTransaction:ApproveAgent',message:{nonce:123,hyperliquidChain:'Testnet'}}); - expect(()=>rejectMasterKey({key:'0x'+'ab'.repeat(32)})).toThrow();expect(()=>rejectMasterKey({privateKey:'anything'})).toThrow();rejectMasterKey({signature:'0x'+'ab'.repeat(65)}); +import { expect, test } from 'vitest'; +import { approval, rejectMasterKey } from '../src/wallet/approval.js'; +test('user signature payload follows SDK types and master secrets are refused', () => { + const result = approval( + 'testnet', + 'approveAgent', + { agentAddress: '0x' + 'a'.repeat(40), agentName: 'agent' }, + 123, + ); + expect(result.typedData).toMatchObject({ + primaryType: 'HyperliquidTransaction:ApproveAgent', + message: { nonce: 123, hyperliquidChain: 'Testnet' }, + }); + expect(() => rejectMasterKey({ key: '0x' + 'ab'.repeat(32) })).toThrow(); + expect(() => rejectMasterKey({ privateKey: 'anything' })).toThrow(); + rejectMasterKey({ signature: '0x' + 'ab'.repeat(65) }); }); diff --git a/test/audit.test.ts b/test/audit.test.ts index 1120fe9..39053e8 100644 --- a/test/audit.test.ts +++ b/test/audit.test.ts @@ -1,8 +1,25 @@ -import {expect,test} from 'vitest'; -import {mkdtemp,rm} from 'node:fs/promises'; -import {tmpdir} from 'node:os'; -import {join} from 'node:path'; -import {AuditStore,readAudit,verifyAudit} from '../src/core/audit.js'; -test('audit chain detects edits and excludes sensitive signatures',async()=>{ - const dir=await mkdtemp(join(tmpdir(),'bloxwap-audit-'));const path=join(dir,'audit.jsonl');try{const store=new AuditStore(path);await store.append('intent',{signature:'secret'},{intentId:'one',cloid:'client'});await store.append('decision',{allowed:true},{intentId:'one'});const entries=await readAudit(path);expect(verifyAudit(entries)).toBe(true);expect(JSON.stringify(entries)).not.toContain('secret');entries[0].data='tampered';expect(()=>verifyAudit(entries)).toThrow();}finally{await rm(dir,{recursive:true,force:true});} +import { expect, test } from 'vitest'; +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { AuditStore, readAudit, verifyAudit } from '../src/core/audit.js'; +test('audit chain detects edits and excludes sensitive signatures', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-audit-')); + const path = join(dir, 'audit.jsonl'); + try { + const store = new AuditStore(path); + await store.append( + 'intent', + { signature: 'secret' }, + { intentId: 'one', cloid: 'client' }, + ); + await store.append('decision', { allowed: true }, { intentId: 'one' }); + const entries = await readAudit(path); + expect(verifyAudit(entries)).toBe(true); + expect(JSON.stringify(entries)).not.toContain('secret'); + entries[0].data = 'tampered'; + expect(() => verifyAudit(entries)).toThrow(); + } finally { + await rm(dir, { recursive: true, force: true }); + } }); diff --git a/test/builder.test.ts b/test/builder.test.ts index decd6de..d5a3c3d 100644 --- a/test/builder.test.ts +++ b/test/builder.test.ts @@ -1,7 +1,16 @@ -import {expect,test} from 'vitest'; -import {approvedBuilder} from '../src/core/builder.js'; -import {migrate} from '../src/core/config.js'; -test('builder fee cannot exceed master approval',async()=>{ - const config=migrate({masterAddress:'0x'+'a'.repeat(40),builder:{address:'0x'+'b'.repeat(40),fee:10}}); - expect(await approvedBuilder(config,async()=>10)).toEqual({b:config.builder.address,f:10});await expect(approvedBuilder(config,async()=>9)).rejects.toMatchObject({code:'AGENT_NOT_APPROVED'}); +import { expect, test } from 'vitest'; +import { approvedBuilder } from '../src/core/builder.js'; +import { migrate } from '../src/core/config.js'; +test('builder fee cannot exceed master approval', async () => { + const config = migrate({ + masterAddress: '0x' + 'a'.repeat(40), + builder: { address: '0x' + 'b'.repeat(40), fee: 10 }, + }); + expect(await approvedBuilder(config, async () => 10)).toEqual({ + b: config.builder.address, + f: 10, + }); + await expect(approvedBuilder(config, async () => 9)).rejects.toMatchObject({ + code: 'AGENT_NOT_APPROVED', + }); }); diff --git a/test/cancel.test.ts b/test/cancel.test.ts index 52d42d7..0dbb1e1 100644 --- a/test/cancel.test.ts +++ b/test/cancel.test.ts @@ -1,8 +1,23 @@ -import {expect,test} from 'vitest'; -import {cancelOrders,cancelSchema} from '../src/commands/cancel.js'; -import {mockTrading} from './order.test.js'; -import {migrate} from '../src/core/config.js'; -test('batch cancellation selects requested orders and rejects unknown ids',async()=>{ - const config=migrate({approved:true,masterAddress:'0x'+'1'.repeat(40),agentExpiry:Date.now()+10000});const services=mockTrading();services.orders=async()=>[{coin:'BTC',oid:1,sz:'1',limitPx:'2',side:'B'},{coin:'BTC',oid:2,sz:'1',limitPx:'2',side:'B'}]; - expect((await cancelOrders(config,cancelSchema.parse({oids:[1,2]}),services)).cancelled).toEqual(['1','2']);await expect(cancelOrders(config,cancelSchema.parse({oid:9}),services)).rejects.toMatchObject({code:'ORDER_NOT_FOUND'}); +import { expect, test } from 'vitest'; +import { cancelOrders, cancelSchema } from '../src/commands/cancel.js'; +import { mockTrading } from './fixtures.js'; +import { migrate } from '../src/core/config.js'; +test('batch cancellation selects requested orders and rejects unknown ids', async () => { + const config = migrate({ + approved: true, + masterAddress: '0x' + '1'.repeat(40), + agentExpiry: Date.now() + 10000, + }); + const services = mockTrading(); + services.orders = async () => [ + { coin: 'BTC', oid: 1, sz: '1', limitPx: '2', side: 'B' }, + { coin: 'BTC', oid: 2, sz: '1', limitPx: '2', side: 'B' }, + ]; + expect( + (await cancelOrders(config, cancelSchema.parse({ oids: [1, 2] }), services)) + .cancelled, + ).toEqual(['1', '2']); + await expect( + cancelOrders(config, cancelSchema.parse({ oid: 9 }), services), + ).rejects.toMatchObject({ code: 'ORDER_NOT_FOUND' }); }); diff --git a/test/config.test.ts b/test/config.test.ts index 79a782a..1a08265 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -3,11 +3,21 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { expect, test } from 'vitest'; import { loadConfig, saveConfig } from '../src/core/config.js'; -test('atomic private store, migration and corrupt input',async()=>{ - const dir=await mkdtemp(join(tmpdir(),'bloxwap-')); const path=join(dir,'state','config.json'); - try { - await saveConfig({network:'testnet'},path);expect((await stat(path)).mode & 0o777).toBe(0o600);expect((await stat(join(dir,'state'))).mode & 0o777).toBe(0o700); - expect((await loadConfig(path,{BLOXWAP_NETWORK:'mainnet'})).network).toBe('mainnet'); - await writeFile(path,'broken');await expect(loadConfig(path,{})).rejects.toMatchObject({code:'CONFIG_INVALID'}); - } finally {await rm(dir,{recursive:true,force:true});} +test('atomic private store, migration and corrupt input', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-')); + const path = join(dir, 'state', 'config.json'); + try { + await saveConfig({ network: 'testnet' }, path); + expect((await stat(path)).mode & 0o777).toBe(0o600); + expect((await stat(join(dir, 'state'))).mode & 0o777).toBe(0o700); + expect( + (await loadConfig(path, { BLOXWAP_NETWORK: 'mainnet' })).network, + ).toBe('mainnet'); + await writeFile(path, 'broken'); + await expect(loadConfig(path, {})).rejects.toMatchObject({ + code: 'CONFIG_INVALID', + }); + } finally { + await rm(dir, { recursive: true, force: true }); + } }); diff --git a/test/daily-loss.test.ts b/test/daily-loss.test.ts new file mode 100644 index 0000000..ef69417 --- /dev/null +++ b/test/daily-loss.test.ts @@ -0,0 +1,22 @@ +import { expect, test } from 'vitest'; +import { dailyLossFromPortfolio } from '../src/core/trading.js'; +test('daily loss uses realized and marked pnl history across UTC day', () => { + const midnight = Date.UTC(2026, 0, 1); + expect( + dailyLossFromPortfolio( + [ + [ + 'day', + { + pnlHistory: [ + [midnight - 1000, '100'], + [midnight + 1000, '90.12345678'], + ], + }, + ], + ], + midnight + 2000, + ), + ).toBe('9.87654322'); + expect(() => dailyLossFromPortfolio([])).toThrow(); +}); diff --git a/test/deposit-signatures.test.ts b/test/deposit-signatures.test.ts new file mode 100644 index 0000000..731dc63 --- /dev/null +++ b/test/deposit-signatures.test.ts @@ -0,0 +1,115 @@ +import { expect, test, vi } from 'vitest'; +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { + writeDeposits, + type Deposit, + readDeposits, +} from '../src/funding/store.js'; +import { + signatureSteps, + submitDepositSignature, +} from '../src/funding/signatures.js'; +import { quoteFixture } from './fixtures.js'; +test('signature steps are ordered, typed and resumable without replaying completed steps', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-signatures-')); + const path = join(dir, 'deposits.json'); + const quote: Deposit['quote'] = structuredClone(quoteFixture); + quote.steps = [ + { + id: 'first', + kind: 'signature', + requestId: 'deposit', + depositAddress: '0x' + 'a'.repeat(40), + items: [ + { + status: 'incomplete', + data: { + sign: { + signatureKind: 'eip712', + domain: { name: 'Relay' }, + types: { Mapping: [] }, + primaryType: 'Mapping', + value: { nonce: 1 }, + }, + post: { + method: 'POST', + endpoint: '/authorize', + body: { nonce: 1 }, + }, + }, + }, + ], + }, + { + id: 'second', + kind: 'signature', + requestId: 'deposit', + depositAddress: '0x' + 'a'.repeat(40), + items: [ + { + status: 'incomplete', + data: { + sign: { message: 'second' }, + post: { + method: 'POST', + endpoint: '/authorize', + body: { nonce: 2 }, + }, + }, + }, + ], + }, + ]; + const deposit: Deposit = { + id: 'deposit', + network: 'testnet', + recipient: '0x' + 'a'.repeat(40), + route: 'route', + quote, + expiresAt: Date.now() + 10000, + state: 'awaiting_funds', + baseline: '0', + createdAt: Date.now(), + completedSteps: [], + }; + const submit = vi.fn(async () => ({ ok: true })); + const signature = '0x' + '11'.repeat(64) + '1b'; + try { + await writeDeposits([deposit], path); + expect(signatureSteps(deposit)[0].payload).toMatchObject({ + message: { nonce: 1 }, + primaryType: 'Mapping', + }); + await expect( + submitDepositSignature('deposit', 'second:0', signature, { + path, + submit, + }), + ).rejects.toMatchObject({ code: 'CONFIG_INVALID' }); + expect(submit).not.toHaveBeenCalled(); + await submitDepositSignature('deposit', 'first:0', signature, { + path, + submit, + }); + expect((await readDeposits(path))[0].completedSteps).toEqual(['first:0']); + await submitDepositSignature('deposit', 'second:0', signature, { + path, + submit, + }); + expect( + await submitDepositSignature('deposit', 'second:0', signature, { + path, + submit, + }), + ).toEqual({ complete: true }); + expect(submit).toHaveBeenCalledTimes(2); + expect(submit.mock.calls[0]).toEqual([ + `https://api.testnets.relay.link/authorize?signature=${signature}`, + { nonce: 1 }, + ]); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/test/deposit-status.test.ts b/test/deposit-status.test.ts index 5bd7f43..2606715 100644 --- a/test/deposit-status.test.ts +++ b/test/deposit-status.test.ts @@ -1,8 +1,39 @@ -import {expect,test} from 'vitest'; -import {advanceDeposit} from '../src/funding/status.js'; -import type {Deposit} from '../src/funding/store.js'; -import {quoteFixture} from './relay.test.js'; -test('settlement requires Relay success plus expected balance increase',()=>{ - const deposit={quote:quoteFixture,state:'awaiting_funds',baseline:'2',id:'one',network:'testnet',recipient:'master',route:'route',expiresAt:2000000000000,createdAt:0,completedSteps:[]} satisfies Deposit; - expect(advanceDeposit(deposit,'success','2.9')).toBe('pending');expect(advanceDeposit(deposit,'success','3')).toBe('settled');expect(advanceDeposit(deposit,'failed','3')).toBe('failed');expect(advanceDeposit({...deposit,state:'settled'},'failed','0')).toBe('settled'); +import { expect, test } from 'vitest'; +import { advanceDeposit } from '../src/funding/status.js'; +import type { Deposit } from '../src/funding/store.js'; +import { quoteFixture } from './fixtures.js'; +test('settlement requires Relay success plus expected balance increase', () => { + const deposit = { + quote: quoteFixture, + state: 'awaiting_funds', + baseline: '2', + id: 'one', + network: 'testnet', + recipient: 'master', + route: 'route', + expiresAt: 2000000000000, + createdAt: 0, + completedSteps: [], + } satisfies Deposit; + expect(advanceDeposit(deposit, 'success', '2.9')).toBe('pending'); + expect(advanceDeposit(deposit, 'success', '3')).toBe('settled'); + expect(advanceDeposit(deposit, 'failed', '3')).toBe('failed'); + expect(advanceDeposit({ ...deposit, state: 'settled' }, 'failed', '0')).toBe( + 'settled', + ); +}); +test('marked PnL changes do not count as a deposit receipt', async () => { + const { collateralBalance } = await import('../src/core/account.js'); + expect( + collateralBalance({ + marginSummary: { accountValue: '110' }, + assetPositions: [{ position: { unrealizedPnl: '60' } }], + }), + ).toBe('50'); + expect( + collateralBalance({ + marginSummary: { accountValue: '90' }, + assetPositions: [{ position: { unrealizedPnl: '-10' } }], + }), + ).toBe('100'); }); diff --git a/test/errors.test.ts b/test/errors.test.ts index 6903f70..aaa6705 100644 --- a/test/errors.test.ts +++ b/test/errors.test.ts @@ -1,8 +1,32 @@ import { expect, test } from 'vitest'; import { AgentCliError, errorDefaults, success } from '../src/errors/index.js'; -const original = ['INSUFFICIENT_MARGIN','POLICY_REJECTED','AGENT_EXPIRED','AGENT_NOT_APPROVED','DEPOSIT_PENDING','NONCE_COLLISION','RATE_LIMIT','INVALID_SIZE','INVALID_PRICE','UNKNOWN_MARKET','ORDER_NOT_FOUND','RUNNER_NOT_RUNNING','CONFIG_INVALID','NETWORK_ERROR','UPSTREAM_ERROR','INTERNAL']; -test('catalog is append-only', () => original.forEach(code => expect(errorDefaults).toHaveProperty(code))); +const original = [ + 'INSUFFICIENT_MARGIN', + 'POLICY_REJECTED', + 'AGENT_EXPIRED', + 'AGENT_NOT_APPROVED', + 'DEPOSIT_PENDING', + 'NONCE_COLLISION', + 'RATE_LIMIT', + 'INVALID_SIZE', + 'INVALID_PRICE', + 'UNKNOWN_MARKET', + 'ORDER_NOT_FOUND', + 'RUNNER_NOT_RUNNING', + 'CONFIG_INVALID', + 'NETWORK_ERROR', + 'UPSTREAM_ERROR', + 'INTERNAL', +]; +test('catalog is append-only', () => + original.forEach((code) => expect(errorDefaults).toHaveProperty(code))); test('stable wire shapes', () => { - expect(new AgentCliError('RATE_LIMIT','wait').toJSON()).toEqual({ok:false,code:'RATE_LIMIT',message:'wait',retryable:true,details:null}); - expect(success('yes')).toEqual({ok:true,data:'yes'}); + expect(new AgentCliError('RATE_LIMIT', 'wait').toJSON()).toEqual({ + ok: false, + code: 'RATE_LIMIT', + message: 'wait', + retryable: true, + details: null, + }); + expect(success('yes')).toEqual({ ok: true, data: 'yes' }); }); diff --git a/test/events.test.ts b/test/events.test.ts index 2c3e7ca..9125046 100644 --- a/test/events.test.ts +++ b/test/events.test.ts @@ -1,9 +1,38 @@ -import {expect,test} from 'vitest'; -import {mkdtemp,rm} from 'node:fs/promises'; -import {tmpdir} from 'node:os'; -import {join} from 'node:path'; -import {EventStore,readEvents} from '../src/runner/events.js'; -test('event replay preserves monotonic seq across restart and filters',async()=>{ - const dir=await mkdtemp(join(tmpdir(),'bloxwap-events-'));const path=join(dir,'events.jsonl'); - try{const store=new EventStore(path);await Promise.all([store.append('fill',{tid:1}),store.append('error',{ok:false,code:'NETWORK_ERROR'})]);await new EventStore(path).append('deposit',{id:'deposit'});expect((await readEvents(path)).map(e=>e.seq)).toEqual([1,2,3]);expect((await readEvents(path,1,['deposit']))).toHaveLength(1);}finally{await rm(dir,{recursive:true,force:true});} +import { expect, test } from 'vitest'; +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { EventStore, readEvents } from '../src/runner/events.js'; +test('event replay preserves monotonic seq across restart and filters', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-events-')); + const path = join(dir, 'events.jsonl'); + try { + const store = new EventStore(path); + await Promise.all([ + store.append('fill', { tid: 1 }), + store.append('error', { ok: false, code: 'NETWORK_ERROR' }), + ]); + await new EventStore(path).append('deposit', { id: 'deposit' }); + expect((await readEvents(path)).map((e) => e.seq)).toEqual([1, 2, 3]); + expect(await readEvents(path, 1, ['deposit'])).toHaveLength(1); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); +test('independent event writers serialize sequences and expose exact fee units', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-events-parallel-')); + const path = join(dir, 'events.jsonl'); + try { + await Promise.all([ + new EventStore(path).append('fill', { tid: 1, fee: '0.00000001' }), + new EventStore(path).append('deposit', { id: 'one' }), + ]); + const entries = await readEvents(path); + expect(entries.map((e) => e.seq)).toEqual([1, 2]); + expect(entries.find((e) => e.type === 'fill')?.data).toMatchObject({ + feeUnits: '1', + }); + } finally { + await rm(dir, { recursive: true, force: true }); + } }); diff --git a/test/expiry.test.ts b/test/expiry.test.ts index 8489c1a..8010e69 100644 --- a/test/expiry.test.ts +++ b/test/expiry.test.ts @@ -1,10 +1,28 @@ -import {expect,test} from 'vitest'; -import {mkdtemp,rm} from 'node:fs/promises'; -import {tmpdir} from 'node:os'; -import {join} from 'node:path'; -import {assertAgentValid,expiryWarnings} from '../src/wallet/expiry.js'; -import {migrate} from '../src/core/config.js'; -test('mocked clock blocks expiry and emits each threshold once',async()=>{ - const dir=await mkdtemp(join(tmpdir(),'bloxwap-expiry-'));const config=migrate({agentExpiry:15*86400000});const data:unknown[]=[]; - try{const options={path:join(dir,'warnings.json'),now:12*86400000,emit:async(value:unknown)=>{data.push(value);}};expect(await expiryWarnings(config,options)).toHaveLength(2);expect(await expiryWarnings(config,options)).toHaveLength(0);assertAgentValid(config,14*86400000);expect(()=>assertAgentValid(config,15*86400000)).toThrow(expect.objectContaining({code:'AGENT_EXPIRED',retryable:false}));}finally{await rm(dir,{recursive:true,force:true});} +import { expect, test } from 'vitest'; +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { assertAgentValid, expiryWarnings } from '../src/wallet/expiry.js'; +import { migrate } from '../src/core/config.js'; +test('mocked clock blocks expiry and emits each threshold once', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-expiry-')); + const config = migrate({ agentExpiry: 15 * 86400000 }); + const data: unknown[] = []; + try { + const options = { + path: join(dir, 'warnings.json'), + now: 12 * 86400000, + emit: async (value: unknown) => { + data.push(value); + }, + }; + expect(await expiryWarnings(config, options)).toHaveLength(2); + expect(await expiryWarnings(config, options)).toHaveLength(0); + assertAgentValid(config, 14 * 86400000); + expect(() => assertAgentValid(config, 15 * 86400000)).toThrow( + expect.objectContaining({ code: 'AGENT_EXPIRED', retryable: false }), + ); + } finally { + await rm(dir, { recursive: true, force: true }); + } }); diff --git a/test/fixtures.ts b/test/fixtures.ts new file mode 100644 index 0000000..e6626c4 --- /dev/null +++ b/test/fixtures.ts @@ -0,0 +1,64 @@ +import { vi } from 'vitest'; +import type { TradingServices } from '../src/core/trading.js'; +import type { AccountState } from '../src/commands/read.js'; +export const accountFixture: AccountState = { + marginSummary: { + accountValue: '100.12345678', + totalMarginUsed: '4', + totalNtlPos: '20', + }, + withdrawable: '96', + assetPositions: [ + { + position: { + coin: 'BTC', + szi: '0.001', + entryPx: '20000', + unrealizedPnl: '1', + liquidationPx: '10000', + leverage: { type: 'cross', value: 5 }, + }, + }, + ], +}; +export function mockTrading(): TradingServices { + return { + market: async () => ({ + name: 'BTC', + szDecimals: 5, + maxLeverage: 40, + asset: 0, + }), + state: async () => accountFixture, + orders: async () => [], + mids: async () => ({ BTC: '20000' }), + exchange: { + order: vi.fn(async () => ({ status: 'ok' })), + cancel: vi.fn(async () => ({ status: 'ok' })), + cancelByCloid: vi.fn(async () => ({ status: 'ok' })), + batchModify: vi.fn(async () => ({ status: 'ok' })), + updateLeverage: vi.fn(async () => ({ status: 'ok' })), + }, + }; +} +export const quoteFixture = { + steps: [ + { + id: 'deposit', + kind: 'transaction', + requestId: 'deposit-1', + depositAddress: '0x' + 'a'.repeat(40), + items: [{ status: 'incomplete', data: { chainId: 42161 } }], + }, + ], + fees: { + app: { amount: '10' }, + gas: { amount: '20' }, + relayerService: { amount: '30' }, + }, + details: { + currencyOut: { amount: '100000000', currency: { decimals: 8 } }, + minAmount: '1000000', + expiresAt: 2000000000000, + }, +}; diff --git a/test/fund.test.ts b/test/fund.test.ts index 4dce652..231e0a4 100644 --- a/test/fund.test.ts +++ b/test/fund.test.ts @@ -1,15 +1,33 @@ -import {expect,test,vi} from 'vitest'; -import {mkdtemp,rm} from 'node:fs/promises'; -import {join} from 'node:path'; -import {tmpdir} from 'node:os'; -import {fund,fundSchema} from '../src/commands/fund.js'; -import {migrate} from '../src/core/config.js'; -import {RelayClient} from '../src/funding/relay.js'; -import {quoteFixture} from './relay.test.js'; -test('fund persists and reuses unexpired quotes',async()=>{ - const dir=await mkdtemp(join(tmpdir(),'bloxwap-fund-'));const quote=vi.fn(async()=>quoteFixture);const config=migrate({masterAddress:'0x'+'a'.repeat(40)}); - try{const dependencies={path:join(dir,'deposits.json'),relay:{quote} as unknown as RelayClient,baseline:async()=>'0',qr:async()=>({qrAscii:null,qrPath:null})}; - const input=fundSchema.parse({amount:'10',qr:false});const first=await fund(config,input,dependencies);expect(first.expectedReceived).toBe('100000000');expect((await fund(config,input,dependencies)).depositAddress).toBe(first.depositAddress);expect(quote).toHaveBeenCalledTimes(1); - await expect(fund(config,{...input,amount:'0.1'},dependencies)).rejects.toMatchObject({code:'INVALID_SIZE'}); - }finally{await rm(dir,{recursive:true,force:true});} +import { expect, test, vi } from 'vitest'; +import { mkdtemp, rm } from 'node:fs/promises'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { fund, fundSchema } from '../src/commands/fund.js'; +import { migrate } from '../src/core/config.js'; +import { RelayClient } from '../src/funding/relay.js'; +import { quoteFixture } from './fixtures.js'; +test('fund persists and reuses unexpired quotes', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-fund-')); + const quote = vi.fn(async () => quoteFixture); + const config = migrate({ masterAddress: '0x' + 'a'.repeat(40) }); + try { + const dependencies = { + path: join(dir, 'deposits.json'), + relay: { quote } as unknown as RelayClient, + baseline: async () => '0', + qr: async () => ({ qrAscii: null, qrPath: null }), + }; + const input = fundSchema.parse({ amount: '10', qr: false }); + const first = await fund(config, input, dependencies); + expect(first.expectedReceived).toBe('100000000'); + expect((await fund(config, input, dependencies)).depositAddress).toBe( + first.depositAddress, + ); + expect(quote).toHaveBeenCalledTimes(1); + await expect( + fund(config, { ...input, amount: '0.1' }, dependencies), + ).rejects.toMatchObject({ code: 'INVALID_SIZE' }); + } finally { + await rm(dir, { recursive: true, force: true }); + } }); diff --git a/test/health.test.ts b/test/health.test.ts index 69923f9..a143ac2 100644 --- a/test/health.test.ts +++ b/test/health.test.ts @@ -1,9 +1,20 @@ -import {expect,test} from 'vitest'; -import {health} from '../src/runner/health.js'; -import {Runner} from '../src/runner/lifecycle.js'; -import {migrate} from '../src/core/config.js'; -import type {Subscriptions} from '../src/runner/subscriptions.js'; -test('health reports staleness, uptime and signer state',()=>{ - const runner=new Runner(migrate({}));runner.subscriptions={state:'connected',lastMessage:1000,lastFill:500} as Subscriptions; - expect(health(runner,2000)).toMatchObject({wsState:'connected',wsStale:false,lastFillTime:500,signingSession:'unavailable'});expect(health(runner,100000).wsStale).toBe(true); +import { expect, test } from 'vitest'; +import { health } from '../src/runner/health.js'; +import { Runner } from '../src/runner/lifecycle.js'; +import { migrate } from '../src/core/config.js'; +import type { Subscriptions } from '../src/runner/subscriptions.js'; +test('health reports staleness, uptime and signer state', () => { + const runner = new Runner(migrate({})); + runner.subscriptions = { + state: 'connected', + lastMessage: 1000, + lastFill: 500, + } as Subscriptions; + expect(health(runner, 2000)).toMatchObject({ + wsState: 'connected', + wsStale: false, + lastFillTime: 500, + signingSession: 'unavailable', + }); + expect(health(runner, 100000).wsStale).toBe(true); }); diff --git a/test/hlclient.test.ts b/test/hlclient.test.ts index 7a104cd..0a0cf9b 100644 --- a/test/hlclient.test.ts +++ b/test/hlclient.test.ts @@ -1,7 +1,40 @@ -import { expect,test,vi } from 'vitest'; -import { MetaCache,roundPrice,roundSize,createClients } from '../src/core/hlclient.js'; -test('fixture metadata caches and refreshes',async()=>{ - const fixture={universe:[{name:'BTC',szDecimals:5,maxLeverage:40}]};const fetch=vi.fn(async()=>fixture);const cache=new MetaCache(fetch); - expect(await cache.lookup('BTC')).toEqual({...fixture.universe[0],asset:0});await cache.lookup('BTC');expect(fetch).toHaveBeenCalledTimes(1);await cache.refresh();expect(fetch).toHaveBeenCalledTimes(2);await expect(cache.lookup('NO')).rejects.toMatchObject({code:'UNKNOWN_MARKET'}); +import { expect, test, vi } from 'vitest'; +import { + MetaCache, + roundPrice, + roundSize, + createClients, +} from '../src/core/hlclient.js'; +test('fixture metadata caches and refreshes', async () => { + const fixture = { + universe: [{ name: 'BTC', szDecimals: 5, maxLeverage: 40 }], + }; + const fetch = vi.fn(async () => fixture); + const cache = new MetaCache(fetch); + expect(await cache.lookup('BTC')).toEqual({ + ...fixture.universe[0], + asset: 0, + }); + await cache.lookup('BTC'); + expect(fetch).toHaveBeenCalledTimes(1); + await cache.refresh(); + expect(fetch).toHaveBeenCalledTimes(2); + await expect(cache.lookup('NO')).rejects.toMatchObject({ + code: 'UNKNOWN_MARKET', + }); +}); +test('SDK rounds exact decimal fixtures', () => { + expect(roundSize('1.234567', 5)).toBe('1.23456'); + expect(roundPrice('97123.456', 5)).toBe('97123'); + expect(roundPrice('1.23456', 3)).toBe('1.234'); + expect(() => roundSize('0.000001', 5)).toThrow(); + expect(createClients('testnet').exchange).toBeUndefined(); +}); +test('malformed metadata cannot disable leverage or precision limits', async () => { + const cache = new MetaCache(async () => ({ + universe: [{ name: 'BTC', szDecimals: 5, maxLeverage: NaN }], + })); + await expect(cache.lookup('BTC')).rejects.toMatchObject({ + code: 'UPSTREAM_ERROR', + }); }); -test('SDK rounds exact decimal fixtures',()=>{expect(roundSize('1.234567',5)).toBe('1.23456');expect(roundPrice('97123.456',5)).toBe('97123');expect(roundPrice('1.23456',3)).toBe('1.234');expect(()=>roundSize('0.000001',5)).toThrow();expect(createClients('testnet').exchange).toBeUndefined();}); diff --git a/test/leverage.test.ts b/test/leverage.test.ts index 50fe394..8f5d9f1 100644 --- a/test/leverage.test.ts +++ b/test/leverage.test.ts @@ -1,8 +1,47 @@ -import {expect,test} from 'vitest'; -import {setLeverage,leverageSchema} from '../src/commands/leverage.js'; -import {mockTrading} from './order.test.js'; -import {migrate} from '../src/core/config.js'; -test('cross/isolated leverage enforces both ceilings',async()=>{ - const config=migrate({approved:true,masterAddress:'0x'+'1'.repeat(40),agentExpiry:Date.now()+10000,limits:{maxLeverage:5}});const services=mockTrading(); - await setLeverage(config,leverageSchema.parse({coin:'BTC',leverage:5,mode:'isolated'}),services);expect(services.exchange.updateLeverage).toHaveBeenCalledWith({asset:0,isCross:false,leverage:5});await expect(setLeverage(config,leverageSchema.parse({coin:'BTC',leverage:6}),services)).rejects.toMatchObject({code:'POLICY_REJECTED'}); +import { expect, test } from 'vitest'; +import { setLeverage, leverageSchema } from '../src/commands/leverage.js'; +import { mockTrading } from './fixtures.js'; +import { migrate } from '../src/core/config.js'; +test('cross/isolated leverage enforces both ceilings', async () => { + const config = migrate({ + approved: true, + masterAddress: '0x' + '1'.repeat(40), + agentExpiry: Date.now() + 10000, + limits: { maxLeverage: 5 }, + }); + const services = mockTrading(); + await setLeverage( + config, + leverageSchema.parse({ coin: 'BTC', leverage: 5, mode: 'isolated' }), + services, + ); + expect(services.exchange.updateLeverage).toHaveBeenCalledWith({ + asset: 0, + isCross: false, + leverage: 5, + }); + await expect( + setLeverage( + config, + leverageSchema.parse({ coin: 'BTC', leverage: 6 }), + services, + ), + ).rejects.toMatchObject({ code: 'POLICY_REJECTED' }); +}); +test('leverage updates respect configured daily loss', async () => { + const config = migrate({ + approved: true, + masterAddress: '0x' + '1'.repeat(40), + agentExpiry: Date.now() + 10000, + limits: { dailyLoss: 5 }, + }); + const services = mockTrading(); + services.dailyLoss = async () => '6'; + await expect( + setLeverage( + config, + leverageSchema.parse({ coin: 'BTC', leverage: 2 }), + services, + ), + ).rejects.toMatchObject({ code: 'POLICY_REJECTED' }); }); diff --git a/test/lifecycle.test.ts b/test/lifecycle.test.ts index db38b3e..dd64e26 100644 --- a/test/lifecycle.test.ts +++ b/test/lifecycle.test.ts @@ -1,13 +1,68 @@ -import {afterEach,beforeEach,expect,test,vi} from 'vitest'; -import {mkdir,rm} from 'node:fs/promises'; -import {join} from 'node:path'; -const environment=vi.hoisted(()=>({home:`/tmp/bloxwap-e2e-${process.pid}-${Math.random().toString(16).slice(2)}`,wallets:0,orders:[] as {coin:string;oid:number;side:string;sz:string;limitPx:string}[],agents:[] as {address:string;validUntil:number}[],balance:'0'})); -vi.mock('node:os',async importOriginal=>({...await importOriginal(),homedir:()=>environment.home})); -vi.mock('../src/wallet/privy.js',async importOriginal=>{ - const original=await importOriginal();return {...original,privyClient:async()=>({}),createAgent:async(config:import('../src/core/config.js').Config,_client:unknown,path?:string)=>{const {saveConfig}=await import('../src/core/config.js');config.agentAddress='0x'+String(++environment.wallets).repeat(40);config.privyWalletId='wallet-'+environment.wallets;config.authorizationKeyRef='env:BLOXWAP_E2E_AUTH';await saveConfig(config,path);return {id:config.privyWalletId,address:config.agentAddress};},privySigner:async(config:import('../src/core/config.js').Config)=>({address:config.agentAddress,signTypedData:async()=>('0x'+'11'.repeat(64)+'1b')})}; +import { afterEach, beforeEach, expect, test, vi } from 'vitest'; +import { mkdir, rm } from 'node:fs/promises'; +import { join } from 'node:path'; +const environment = vi.hoisted(() => ({ + home: `/tmp/bloxwap-e2e-${process.pid}-${Math.random().toString(16).slice(2)}`, + wallets: 0, + orders: [] as { + coin: string; + oid: number; + side: string; + sz: string; + limitPx: string; + }[], + agents: [] as { address: string; validUntil: number }[], + balance: '0', +})); +vi.mock('node:os', async (importOriginal) => ({ + ...(await importOriginal()), + homedir: () => environment.home, +})); +vi.mock('../src/wallet/privy.js', async (importOriginal) => { + const original = + await importOriginal(); + return { + ...original, + privyClient: async () => ({ + wallets: () => ({ + ethereum: () => ({ + signTypedData: async () => ({ + signature: '0x' + '11'.repeat(64) + '1b', + }), + }), + }), + }), + createAgent: async ( + config: import('../src/core/config.js').Config, + _client: unknown, + path?: string, + ) => { + const { saveConfig } = await import('../src/core/config.js'); + config.agentAddress = '0x' + String(++environment.wallets).repeat(40); + config.privyWalletId = 'wallet-' + environment.wallets; + config.authorizationKeyRef = 'env:BLOXWAP_E2E_AUTH'; + await saveConfig(config, path); + return { id: config.privyWalletId, address: config.agentAddress }; + }, + privySigner: original.privySigner, + }; }); -vi.mock('../src/wallet/policy.js',async importOriginal=>({...await importOriginal(),applyPolicy:async(config:import('../src/core/config.js').Config)=>{config.policyId='policy';config.policyVersion=1;return 'policy';}})); -vi.mock('../src/runner/subscriptions.js',async importOriginal=>({...await importOriginal(),startSubscriptions:async()=>({state:'connected',lastMessage:Date.now(),lastFill:null})})); +vi.mock('../src/wallet/policy.js', async (importOriginal) => ({ + ...(await importOriginal()), + applyPolicy: async (config: import('../src/core/config.js').Config) => { + config.policyId = 'policy'; + config.policyVersion = 1; + return 'policy'; + }, +})); +vi.mock('../src/runner/subscriptions.js', async (importOriginal) => ({ + ...(await importOriginal()), + startSubscriptions: async () => ({ + state: 'connected', + lastMessage: Date.now(), + lastFill: null, + }), +})); import '../src/commands/init.js'; import '../src/commands/fund.js'; import '../src/commands/read.js'; @@ -20,37 +75,181 @@ import '../src/commands/rotate.js'; import '../src/commands/revoke.js'; import '../src/commands/events.js'; import '../src/commands/audit.js'; -import {dispatch} from '../src/core/router.js'; -import {saveConfig,migrate} from '../src/core/config.js'; -import {activeRunner,setActiveRunner} from '../src/runner/lifecycle.js'; -import {rpcCall,socketPath} from '../src/runner/rpc.js'; -import {readAudit,verifyAudit} from '../src/core/audit.js'; -let path:string; -beforeEach(async()=>{ - await mkdir(environment.home,{recursive:true,mode:0o700});path=join(environment.home,'.bloxwap','config.json');environment.wallets=0;environment.orders=[];environment.agents=[];environment.balance='0'; - await saveConfig(migrate({privyAppId:'mock',masterAddress:'0x'+'a'.repeat(40)}),path); - vi.stubGlobal('fetch',vi.fn(async(url:unknown,options?:RequestInit)=>{ - const body=JSON.parse(String(options?.body??'{}'));const action=body.action; - if(String(url).includes('/quote/v2')) return Response.json({steps:[{id:'deposit',kind:'transaction',requestId:'deposit-1',depositAddress:'0x'+'b'.repeat(40),items:[{status:'incomplete',data:{}}]}],fees:{},details:{currencyOut:{amount:'1000000000',currency:{decimals:8}},expiresAt:Date.now()+60000}}); - if(String(url).includes('/intents/status')) return Response.json({status:'success'}); - if(action?.type==='approveAgent'){if(action.agentAddress==='0x'+'0'.repeat(40)) environment.agents=[];else environment.agents.push({address:action.agentAddress,validUntil:Number(action.agentName.match(/valid_until (\d+)/)?.[1])});return Response.json({status:'ok',response:{type:'default'}});} - if(action){const statuses=action.type==='order'?[{resting:{oid:1}}]:['success'];if(action.type==='order') environment.orders=[{coin:'BTC',oid:1,side:'B',sz:'0.001',limitPx:'20000'}];if(action.type==='cancel') environment.orders=[];return Response.json({status:'ok',response:{type:action.type==='batchModify'?'order':action.type,data:{statuses}}});} - if(body.type==='meta') return Response.json({universe:[{name:'BTC',szDecimals:5,maxLeverage:40}],marginTables:[]}); - if(body.type==='clearinghouseState') return Response.json({marginSummary:{accountValue:environment.balance,totalMarginUsed:'0',totalNtlPos:'0'},withdrawable:environment.balance,assetPositions:[]}); - if(body.type==='extraAgents') return Response.json(environment.agents); - if(body.type==='frontendOpenOrders') return Response.json(environment.orders); - if(body.type==='allMids') return Response.json({BTC:'20000'}); - throw new Error('Unexpected mocked request: '+JSON.stringify(body)); - })); +import '../src/commands/market.js'; +import { dispatch } from '../src/core/router.js'; +import { saveConfig, migrate } from '../src/core/config.js'; +import { activeRunner, setActiveRunner } from '../src/runner/lifecycle.js'; +import { rpcCall, socketPath } from '../src/runner/rpc.js'; +import { readAudit, verifyAudit } from '../src/core/audit.js'; +let path: string; +beforeEach(async () => { + await mkdir(environment.home, { recursive: true, mode: 0o700 }); + path = join(environment.home, '.bloxwap', 'config.json'); + process.env.BLOXWAP_E2E_AUTH = 'mock-authorization'; + environment.wallets = 0; + environment.orders = []; + environment.agents = []; + environment.balance = '0'; + await saveConfig( + migrate({ privyAppId: 'mock', masterAddress: '0x' + 'a'.repeat(40) }), + path, + ); + vi.stubGlobal( + 'fetch', + vi.fn(async (url: unknown, options?: RequestInit) => { + const body = JSON.parse(String(options?.body ?? '{}')); + const action = body.action; + if (String(url).includes('/quote/v2')) + return Response.json({ + steps: [ + { + id: 'deposit', + kind: 'transaction', + requestId: 'deposit-1', + depositAddress: '0x' + 'b'.repeat(40), + items: [{ status: 'incomplete', data: {} }], + }, + ], + fees: {}, + details: { + currencyOut: { amount: '1000000000', currency: { decimals: 8 } }, + expiresAt: Date.now() + 60000, + }, + }); + if (String(url).includes('/intents/status')) + return Response.json({ status: 'success' }); + if (action?.type === 'approveAgent') { + if (action.agentAddress === '0x' + '0'.repeat(40)) + environment.agents = []; + else + environment.agents.push({ + address: action.agentAddress, + validUntil: Number( + action.agentName.match(/valid_until (\d+)/)?.[1], + ), + }); + return Response.json({ status: 'ok', response: { type: 'default' } }); + } + if (action) { + const statuses = + action.type === 'order' ? [{ resting: { oid: 1 } }] : ['success']; + if (action.type === 'order') + environment.orders = [ + { coin: 'BTC', oid: 1, side: 'B', sz: '0.001', limitPx: '20000' }, + ]; + if (action.type === 'cancel') environment.orders = []; + return Response.json({ + status: 'ok', + response: { + type: action.type === 'batchModify' ? 'order' : action.type, + data: { statuses }, + }, + }); + } + if (body.type === 'meta') + return Response.json({ + universe: [{ name: 'BTC', szDecimals: 5, maxLeverage: 40 }], + marginTables: [], + }); + if (body.type === 'clearinghouseState') + return Response.json({ + marginSummary: { + accountValue: environment.balance, + totalMarginUsed: '0', + totalNtlPos: '0', + }, + withdrawable: environment.balance, + assetPositions: [], + }); + if (body.type === 'extraAgents') return Response.json(environment.agents); + if (body.type === 'frontendOpenOrders') + return Response.json(environment.orders); + if (body.type === 'allMids') return Response.json({ BTC: '20000' }); + throw new Error('Unexpected mocked request: ' + JSON.stringify(body)); + }), + ); }); -afterEach(async()=>{await activeRunner?.shutdown();setActiveRunner(undefined);vi.unstubAllGlobals();await rm(environment.home,{recursive:true,force:true});}); -async function command(name:string,input:Record={}){const result=await dispatch([name,'--config',path,'--json',JSON.stringify(input)]);expect(result.body,JSON.stringify(result.body)).toMatchObject({ok:true});return (result.body as {data:Record}).data;} -test('mocked full account lifecycle through real CLI router, SDK and runner RPC',async()=>{ - const initialized=await command('init',{qr:false});expect(initialized.requiresSignature).toBe(true);await command('init',{qr:false,signature:'0x'+'11'.repeat(64)+'1b'}); - const deposit=await command('fund',{qr:false,amount:'10'});expect(deposit.depositId).toBe('deposit-1');environment.balance='10';const status=await command('status');expect(status.deposits).toEqual([{id:'deposit-1',state:'settled'}]); - await command('run');expect(await rpcCall('balance',{},socketPath())).toMatchObject({accountValue:'10'}); - await command('order',{coin:'BTC',side:'buy',size:'0.001',price:'20000',intentId:'lifecycle'});await command('modify',{oid:1,price:'20100'});await command('cancel',{all:true}); - const rotation=await command('rotate');expect(rotation.requiresSignature).toBe(true);expect((await command('rotate',{signature:'0x'+'11'.repeat(64)+'1b'})).swapped).toBe(true); - await command('revoke',{cancelOrders:true});const rejected=await dispatch(['order','--config',path,'--json','{"coin":"BTC","side":"buy","size":"0.001","price":"20000"}']);expect(rejected.body).toMatchObject({ok:false,code:'AGENT_NOT_APPROVED'}); - const entries=await readAudit(join(environment.home,'.bloxwap','audit.jsonl'));expect(entries.some(entry=>entry.intentId==='lifecycle')).toBe(true);expect(verifyAudit(entries)).toBe(true);expect(await command('events')).toBeDefined(); -},20000); +afterEach(async () => { + await activeRunner?.shutdown(); + setActiveRunner(undefined); + delete process.env.BLOXWAP_E2E_AUTH; + vi.unstubAllGlobals(); + await rm(environment.home, { recursive: true, force: true }); +}); +async function command(name: string, input: Record = {}) { + const result = await dispatch([ + name, + '--config', + path, + '--json', + JSON.stringify(input), + ]); + expect(result.body, JSON.stringify(result.body)).toMatchObject({ ok: true }); + return (result.body as { data: Record }).data; +} +test('mocked full account lifecycle through real CLI router, SDK and runner RPC', async () => { + const initialized = await command('init', { qr: false }); + expect(initialized.requiresSignature).toBe(true); + await command('init', { + qr: false, + signature: '0x' + '11'.repeat(64) + '1b', + }); + const deposit = await command('fund', { qr: false, amount: '10' }); + expect(deposit.depositId).toBe('deposit-1'); + environment.balance = '10'; + const status = await command('status'); + expect(status.deposits).toEqual([{ id: 'deposit-1', state: 'settled' }]); + expect(await command('market', { coin: 'BTC' })).toMatchObject({ + mid: '20000', + szDecimals: '5', + asset: '0', + }); + await command('run'); + expect(await rpcCall('balance', {}, socketPath())).toMatchObject({ + accountValue: '10', + }); + await command('order', { + coin: 'BTC', + side: 'buy', + size: '0.001', + price: '20000', + intentId: 'lifecycle', + }); + await command('modify', { oid: 1, price: '20100' }); + await command('cancel', { all: true }); + const rotation = await command('rotate'); + expect(rotation.requiresSignature).toBe(true); + expect( + (await command('rotate', { signature: '0x' + '11'.repeat(64) + '1b' })) + .swapped, + ).toBe(true); + await command('revoke', { cancelOrders: true }); + const rejected = await dispatch([ + 'order', + '--config', + path, + '--json', + '{"coin":"BTC","side":"buy","size":"0.001","price":"20000"}', + ]); + expect(rejected.body).toMatchObject({ + ok: false, + code: 'AGENT_NOT_APPROVED', + }); + const { events } = await import('../src/runner/events.js'); + await events.append('fill', { tid: 7, oid: 1, fee: '0.01' }); + const entries = await readAudit( + join(environment.home, '.bloxwap', 'audit.jsonl'), + ); + expect( + entries.some( + (entry) => entry.intentId === 'lifecycle' && entry.stage === 'signature', + ), + ).toBe(true); + expect( + entries.some( + (entry) => entry.intentId === 'lifecycle' && entry.stage === 'fill', + ), + ).toBe(true); + expect(verifyAudit(entries)).toBe(true); + expect(await command('events')).toBeDefined(); +}, 20000); diff --git a/test/logger.test.ts b/test/logger.test.ts index a21bc61..19f778a 100644 --- a/test/logger.test.ts +++ b/test/logger.test.ts @@ -1,15 +1,46 @@ -import { expect,test } from 'vitest'; -import { mkdtemp,readdir,readFile,rm } from 'node:fs/promises'; +import { expect, test } from 'vitest'; +import { mkdtemp, readdir, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { Logger } from '../src/core/logger.js'; -test('nested secrets are redacted and rotated lines retain correlation',async()=>{ - const dir=await mkdtemp(join(tmpdir(),'bloxwap-logs-'));const lines:string[]=[]; - try { - const logger=new Logger({directory:dir,maxBytes:100,requestId:'request',stderr:line=>lines.push(line)}); - logger.log('info','safe',{nested:{authorizationKey:'secret'},raw:'0x'+'ab'.repeat(32),token:'sensitive'});logger.log('warn','next');await logger.flush(); - const output=lines.join('');expect(output).not.toContain('secret');expect(output).not.toContain('sensitive');expect(output).not.toContain('ab'.repeat(32)); - expect(lines.every(line=>JSON.parse(line).requestId==='request')).toBe(true); - const files=await readdir(dir);expect(files.length).toBe(2);expect(await readFile(join(dir,files[0]),'utf8')).toContain('request'); - }finally{await rm(dir,{recursive:true,force:true});} +test('nested secrets are redacted and rotated lines retain correlation', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-logs-')); + const lines: string[] = []; + try { + const logger = new Logger({ + directory: dir, + maxBytes: 100, + requestId: 'request', + stderr: (line) => lines.push(line), + }); + logger.log('info', 'safe', { + nested: { authorizationKey: 'secret' }, + raw: '0x' + 'ab'.repeat(32), + token: 'sensitive', + }); + logger.log('warn', 'next'); + await logger.flush(); + const output = lines.join(''); + expect(output).not.toContain('secret'); + expect(output).not.toContain('sensitive'); + expect(output).not.toContain('ab'.repeat(32)); + expect( + lines.every((line) => JSON.parse(line).requestId === 'request'), + ).toBe(true); + const files = await readdir(dir); + expect(files.length).toBe(2); + expect(await readFile(join(dir, files[0]), 'utf8')).toContain('request'); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); +test('short opaque API and authorization keys are redacted by field name', async () => { + const { redact } = await import('../src/core/logger.js'); + expect( + redact({ apiKey: 'opaque', api_key: 'opaque', authKey: 'opaque' }), + ).toEqual({ + apiKey: '[REDACTED]', + api_key: '[REDACTED]', + authKey: '[REDACTED]', + }); }); diff --git a/test/logs.test.ts b/test/logs.test.ts index 2cba073..8a911a9 100644 --- a/test/logs.test.ts +++ b/test/logs.test.ts @@ -1,10 +1,41 @@ -import {expect,test} from 'vitest'; -import {mkdtemp,rm,writeFile} from 'node:fs/promises'; -import {tmpdir} from 'node:os'; -import {join} from 'node:path'; -import {readLogs} from '../src/commands/logs.js'; -test('logs filter level, date and request while preserving redaction',async()=>{ - const directory=await mkdtemp(join(tmpdir(),'bloxwap-logread-'));try{ - await writeFile(join(directory,'runner.jsonl'),[{level:'info',time:'2026-01-01',requestId:'a'},{level:'warn',time:'2026-01-02',requestId:'b',token:'sensitive'}].map(value=>JSON.stringify(value)).join('\n')+'\n'); - expect(await readLogs({directory,level:'warn',since:'2026-01-02',requestId:'b'})).toEqual([{level:'warn',time:'2026-01-02',requestId:'b',token:'[REDACTED]'}]);}finally{await rm(directory,{recursive:true,force:true});} +import { expect, test } from 'vitest'; +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { readLogs } from '../src/commands/logs.js'; +test('logs filter level, date and request while preserving redaction', async () => { + const directory = await mkdtemp(join(tmpdir(), 'bloxwap-logread-')); + try { + await writeFile( + join(directory, 'runner.jsonl'), + [ + { level: 'info', time: '2026-01-01', requestId: 'a' }, + { + level: 'warn', + time: '2026-01-02', + requestId: 'b', + token: 'sensitive', + }, + ] + .map((value) => JSON.stringify(value)) + .join('\n') + '\n', + ); + expect( + await readLogs({ + directory, + level: 'warn', + since: '2026-01-02', + requestId: 'b', + }), + ).toEqual([ + { + level: 'warn', + time: '2026-01-02', + requestId: 'b', + token: '[REDACTED]', + }, + ]); + } finally { + await rm(directory, { recursive: true, force: true }); + } }); diff --git a/test/mappers.test.ts b/test/mappers.test.ts index 3ec3b92..ebec8a0 100644 --- a/test/mappers.test.ts +++ b/test/mappers.test.ts @@ -1,6 +1,29 @@ -import {expect,test} from 'vitest'; -import {mapHyperliquidError,mapPrivyError,mapRelayError,mapNetworkError} from '../src/errors/mappers.js'; -test.each([['Insufficient margin','INSUFFICIENT_MARGIN',false],['Order must have minimum value of $10','INVALID_SIZE',false],['Invalid price tick','INVALID_PRICE',false],['HTTP 429','RATE_LIMIT',true],['Nonce already used','NONCE_COLLISION',true]])('maps fixture %s',(message,code,retryable)=>{expect(mapHyperliquidError(message).toJSON()).toMatchObject({code,retryable});}); -test('policy, relay unknown and network flags redact details',()=>{ - expect(mapPrivyError('Policy denied').code).toBe('POLICY_REJECTED');expect(mapRelayError({message:'unexpected',token:'secret'}).toJSON()).toMatchObject({code:'UPSTREAM_ERROR',details:{upstream:{token:'[REDACTED]'}}});expect(mapNetworkError(new Error('fetch failed')).retryable).toBe(true); +import { expect, test } from 'vitest'; +import { + mapHyperliquidError, + mapPrivyError, + mapRelayError, + mapNetworkError, +} from '../src/errors/mappers.js'; +test.each([ + ['Insufficient margin', 'INSUFFICIENT_MARGIN', false], + ['Order must have minimum value of $10', 'INVALID_SIZE', false], + ['Invalid price tick', 'INVALID_PRICE', false], + ['HTTP 429', 'RATE_LIMIT', true], + ['Nonce already used', 'NONCE_COLLISION', true], +])('maps fixture %s', (message, code, retryable) => { + expect(mapHyperliquidError(message).toJSON()).toMatchObject({ + code, + retryable, + }); +}); +test('policy, relay unknown and network flags redact details', () => { + expect(mapPrivyError('Policy denied').code).toBe('POLICY_REJECTED'); + expect( + mapRelayError({ message: 'unexpected', token: 'secret' }).toJSON(), + ).toMatchObject({ + code: 'UPSTREAM_ERROR', + details: { upstream: { token: '[REDACTED]' } }, + }); + expect(mapNetworkError(new Error('fetch failed')).retryable).toBe(true); }); diff --git a/test/modify.test.ts b/test/modify.test.ts index f898177..22b0937 100644 --- a/test/modify.test.ts +++ b/test/modify.test.ts @@ -1,9 +1,114 @@ -import {expect,test} from 'vitest'; -import {modifyOrders,modifySchema} from '../src/commands/modify.js'; -import {mockTrading} from './order.test.js'; -import {migrate} from '../src/core/config.js'; -test('modify rounds replacements and sends batch only after validation',async()=>{ - const config=migrate({approved:true,masterAddress:'0x'+'1'.repeat(40),agentExpiry:Date.now()+10000});const services=mockTrading();services.orders=async()=>[{coin:'BTC',oid:1,sz:'0.001',limitPx:'20000',side:'B'}]; - await modifyOrders(config,modifySchema.parse({modifications:[{oid:1,price:'20000.5',size:'0.0012345'}]}),services);expect(services.exchange.batchModify).toHaveBeenCalledWith({modifies:[{oid:1,order:{a:0,b:true,p:'20000',s:'0.00123',r:false,t:{limit:{tif:'Gtc'}}}}]}); - await expect(modifyOrders(config,modifySchema.parse({oid:4}),services)).rejects.toMatchObject({code:'ORDER_NOT_FOUND'}); +import { expect, test } from 'vitest'; +import { modifyOrders, modifySchema } from '../src/commands/modify.js'; +import { mockTrading } from './fixtures.js'; +import { migrate } from '../src/core/config.js'; +test('modify rounds replacements and sends batch only after validation', async () => { + const config = migrate({ + approved: true, + masterAddress: '0x' + '1'.repeat(40), + agentExpiry: Date.now() + 10000, + }); + const services = mockTrading(); + services.orders = async () => [ + { coin: 'BTC', oid: 1, sz: '0.001', limitPx: '20000', side: 'B' }, + ]; + await modifyOrders( + config, + modifySchema.parse({ + modifications: [{ oid: 1, price: '20000.5', size: '0.0012345' }], + }), + services, + ); + expect(services.exchange.batchModify).toHaveBeenCalledWith({ + modifies: [ + { + oid: 1, + order: { + a: 0, + b: true, + p: '20000', + s: '0.00123', + r: false, + t: { limit: { tif: 'Gtc' } }, + }, + }, + ], + }); + await expect( + modifyOrders(config, modifySchema.parse({ oid: 4 }), services), + ).rejects.toMatchObject({ code: 'ORDER_NOT_FOUND' }); +}); +test('modify preserves reduce-only and tif', async () => { + const config = migrate({ + approved: true, + masterAddress: '0x' + '1'.repeat(40), + agentExpiry: Date.now() + 10000, + }); + const services = mockTrading(); + services.orders = async () => [ + { + coin: 'BTC', + oid: 1, + sz: '0.001', + limitPx: '20000', + side: 'A', + reduceOnly: true, + tif: 'Alo', + }, + ]; + await modifyOrders( + config, + modifySchema.parse({ oid: 1, price: '20001' }), + services, + ); + expect(services.exchange.batchModify).toHaveBeenCalledWith( + expect.objectContaining({ + modifies: [ + expect.objectContaining({ + order: expect.objectContaining({ + r: true, + t: { limit: { tif: 'Alo' } }, + }), + }), + ], + }), + ); +}); +test('trigger modifications retain TP/SL semantics', async () => { + const config = migrate({ + approved: true, + masterAddress: '0x' + '1'.repeat(40), + agentExpiry: Date.now() + 10000, + }); + const services = mockTrading(); + services.orders = async () => [ + { + coin: 'BTC', + oid: 1, + sz: '0.001', + limitPx: '20000', + side: 'A', + reduceOnly: true, + isTrigger: true, + triggerPx: '19900', + orderType: 'Stop Market', + }, + ]; + await modifyOrders( + config, + modifySchema.parse({ oid: 1, size: '0.0005' }), + services, + ); + expect(services.exchange.batchModify).toHaveBeenCalledWith( + expect.objectContaining({ + modifies: [ + expect.objectContaining({ + order: expect.objectContaining({ + r: true, + t: { trigger: { triggerPx: '19900', isMarket: true, tpsl: 'sl' } }, + }), + }), + ], + }), + ); }); diff --git a/test/order.test.ts b/test/order.test.ts index 3c0d053..401e473 100644 --- a/test/order.test.ts +++ b/test/order.test.ts @@ -1,11 +1,73 @@ -import {expect,test,vi} from 'vitest'; -import {prepareOrder,orderSchema} from '../src/commands/order.js'; -import {migrate} from '../src/core/config.js'; -import type {TradingServices} from '../src/core/trading.js'; -import {accountFixture} from './read.test.js'; -export function mockTrading():TradingServices{return {market:async()=>({name:'BTC',szDecimals:5,maxLeverage:40,asset:0}),state:async()=>accountFixture,orders:async()=>[],mids:async()=>({BTC:'20000'}),exchange:{order:vi.fn(async()=>({status:'ok'})),cancel:vi.fn(async()=>({status:'ok'})),cancelByCloid:vi.fn(async()=>({status:'ok'})),batchModify:vi.fn(async()=>({status:'ok'})),updateLeverage:vi.fn(async()=>({status:'ok'}))}};} -test('order rounds limit and bounds market IOC',async()=>{ - const clients=mockTrading();expect(await prepareOrder(migrate({}),orderSchema.parse({coin:'BTC',side:'buy',size:'0.0012345',price:'20000.1'}),clients)).toMatchObject({a:0,s:'0.00123',p:'20000',t:{limit:{tif:'Gtc'}}}); - expect(await prepareOrder(migrate({}),orderSchema.parse({coin:'BTC',side:'sell',size:'0.001',type:'market'}),clients)).toMatchObject({p:'19800',t:{limit:{tif:'Ioc'}}}); - await expect(prepareOrder(migrate({}),orderSchema.parse({coin:'BTC',side:'buy',size:'0',price:'1'}),clients)).rejects.toMatchObject({code:'INVALID_SIZE'}); +import { expect, test } from 'vitest'; +import { prepareOrder, orderSchema } from '../src/commands/order.js'; +import { migrate } from '../src/core/config.js'; +import { mockTrading } from './fixtures.js'; + +test('order rounds limit and bounds market IOC', async () => { + const clients = mockTrading(); + expect( + await prepareOrder( + migrate({}), + orderSchema.parse({ + coin: 'BTC', + side: 'buy', + size: '0.0012345', + price: '20000.1', + }), + clients, + ), + ).toMatchObject({ + a: 0, + s: '0.00123', + p: '20000', + t: { limit: { tif: 'Gtc' } }, + }); + expect( + await prepareOrder( + migrate({}), + orderSchema.parse({ + coin: 'BTC', + side: 'sell', + size: '0.001', + type: 'market', + }), + clients, + ), + ).toMatchObject({ p: '19800', t: { limit: { tif: 'Ioc' } } }); + await expect( + prepareOrder( + migrate({}), + orderSchema.parse({ coin: 'BTC', side: 'buy', size: '0', price: '1' }), + clients, + ), + ).rejects.toMatchObject({ code: 'INVALID_SIZE' }); +}); +test('sell IOC rounding stays inside the slippage floor', async () => { + const clients = mockTrading(); + clients.mids = async () => ({ BTC: '123.456' }); + const order = await prepareOrder( + migrate({}), + orderSchema.parse({ + coin: 'BTC', + side: 'sell', + size: '0.001', + type: 'market', + slippage: 0.01, + }), + clients, + ); + expect(Number(order.p)).toBeGreaterThanOrEqual(123.456 * 0.99); + const trigger = await prepareOrder( + migrate({}), + orderSchema.parse({ + coin: 'BTC', + side: 'sell', + size: '0.001', + type: 'sl', + triggerPrice: '100', + reduceOnly: true, + }), + clients, + ); + expect(trigger.t).toMatchObject({ trigger: { isMarket: true, tpsl: 'sl' } }); }); diff --git a/test/policy.test.ts b/test/policy.test.ts index 58ca2f8..ff3e17a 100644 --- a/test/policy.test.ts +++ b/test/policy.test.ts @@ -1,6 +1,18 @@ -import {expect,test} from 'vitest'; -import {assertTradeAction,policyFor} from '../src/wallet/policy.js'; -test('trade-only action whitelist rejects withdrawal and approval',()=>{ - for(const action of ['withdraw','withdraw3','usdSend','spotSend','approveAgent','approveBuilderFee','usdClassTransfer']) expect(()=>assertTradeAction(action)).toThrow(expect.objectContaining({code:'POLICY_REJECTED'})); - assertTradeAction('order');expect(policyFor('mainnet').rules[0].conditions[2].value).toBe('a'); +import { expect, test } from 'vitest'; +import { assertTradeAction, policyFor } from '../src/wallet/policy.js'; +test('trade-only action whitelist rejects withdrawal and approval', () => { + for (const action of [ + 'withdraw', + 'withdraw3', + 'usdSend', + 'spotSend', + 'approveAgent', + 'approveBuilderFee', + 'usdClassTransfer', + ]) + expect(() => assertTradeAction(action)).toThrow( + expect.objectContaining({ code: 'POLICY_REJECTED' }), + ); + assertTradeAction('order'); + expect(policyFor('mainnet').rules[0].conditions[2].value).toBe('a'); }); diff --git a/test/privy.integration.test.ts b/test/privy.integration.test.ts new file mode 100644 index 0000000..8567f3f --- /dev/null +++ b/test/privy.integration.test.ts @@ -0,0 +1,12 @@ +import { expect, test } from 'vitest'; +import { privyClient } from '../src/wallet/privy.js'; +import { migrate } from '../src/core/config.js'; +test.skipIf(!process.env.BLOXWAP_PRIVY_SANDBOX)( + 'Privy sandbox credentials', + async () => { + const client = await privyClient( + migrate({ privyAppId: process.env.BLOXWAP_PRIVY_APP_ID }), + ); + expect(await client.wallets().list()).toBeDefined(); + }, +); diff --git a/test/privy.test.ts b/test/privy.test.ts index 48209f5..6e02b3d 100644 --- a/test/privy.test.ts +++ b/test/privy.test.ts @@ -1,14 +1,45 @@ -import {expect,test,vi} from 'vitest'; -import type {PrivyClient} from '@privy-io/node'; -import {privySigner,resolveSecret,privyClient} from '../src/wallet/privy.js'; -import {migrate} from '../src/core/config.js'; -test('remote typed-data signing preserves EIP-712 and maps denial',async()=>{ - const signTypedData=vi.fn().mockResolvedValue({signature:'0x1234'});const client={wallets:()=>({ethereum:()=>({signTypedData})})} as unknown as PrivyClient; - process.env.BLOXWAP_TEST_AUTH='test-key'; - try{const signer=await privySigner(migrate({agentAddress:'0x'+'a'.repeat(40),privyWalletId:'wallet',authorizationKeyRef:'env:BLOXWAP_TEST_AUTH'}),client); - const data={domain:{name:'Exchange',version:'1',chainId:1337,verifyingContract:'0x'+'0'.repeat(40) as `0x${string}`},types:{Agent:[{name:'source',type:'string'}]},primaryType:'Agent',message:{source:'b'}}; - expect(await signer.signTypedData(data)).toBe('0x1234');expect(signTypedData.mock.calls[0][1].params.typed_data.primary_type).toBe('Agent');signTypedData.mockRejectedValue(new Error('Policy denied'));await expect(signer.signTypedData(data)).rejects.toMatchObject({code:'POLICY_REJECTED'}); - }finally{delete process.env.BLOXWAP_TEST_AUTH;} - expect(await resolveSecret('env:BLOXWAP_SECRET',{BLOXWAP_SECRET:'ok'})).toBe('ok'); +import { expect, test, vi } from 'vitest'; +import type { PrivyClient } from '@privy-io/node'; +import { privySigner, resolveSecret } from '../src/wallet/privy.js'; +import { migrate } from '../src/core/config.js'; +test('remote typed-data signing preserves EIP-712 and maps denial', async () => { + const signTypedData = vi.fn().mockResolvedValue({ signature: '0x1234' }); + const client = { + wallets: () => ({ ethereum: () => ({ signTypedData }) }), + } as unknown as PrivyClient; + process.env.BLOXWAP_TEST_AUTH = 'test-key'; + try { + const signer = await privySigner( + migrate({ + agentAddress: '0x' + 'a'.repeat(40), + privyWalletId: 'wallet', + authorizationKeyRef: 'env:BLOXWAP_TEST_AUTH', + }), + client, + ); + const data = { + domain: { + name: 'Exchange', + version: '1', + chainId: 1337, + verifyingContract: ('0x' + '0'.repeat(40)) as `0x${string}`, + }, + types: { Agent: [{ name: 'source', type: 'string' }] }, + primaryType: 'Agent', + message: { source: 'b' }, + }; + expect(await signer.signTypedData(data)).toBe('0x1234'); + expect(signTypedData.mock.calls[0][1].params.typed_data.primary_type).toBe( + 'Agent', + ); + signTypedData.mockRejectedValue(new Error('Policy denied')); + await expect(signer.signTypedData(data)).rejects.toMatchObject({ + code: 'POLICY_REJECTED', + }); + } finally { + delete process.env.BLOXWAP_TEST_AUTH; + } + expect( + await resolveSecret('env:BLOXWAP_SECRET', { BLOXWAP_SECRET: 'ok' }), + ).toBe('ok'); }); -test.skipIf(!process.env.BLOXWAP_PRIVY_SANDBOX)('Privy sandbox credentials',async()=>{const client=await privyClient(migrate({privyAppId:process.env.BLOXWAP_PRIVY_APP_ID}));expect(await client.wallets().list()).toBeDefined();}); diff --git a/test/qr.test.ts b/test/qr.test.ts index e0b1c64..feb7a4f 100644 --- a/test/qr.test.ts +++ b/test/qr.test.ts @@ -1,6 +1,16 @@ -import {expect,test} from 'vitest'; -import {renderQr} from '../src/funding/qr.js'; -test('QR deterministic terminal snapshot stays off stdout',async()=>{ - let stderr='';const result=await renderQr('bloxwap:test',{stderr:value=>{stderr=value;}}); - expect(result.qrAscii).toMatchSnapshot();expect(stderr).toContain(result.qrAscii);expect(await renderQr('disabled',{enabled:false})).toEqual({qrAscii:null,qrPath:null}); +import { expect, test } from 'vitest'; +import { renderQr } from '../src/funding/qr.js'; +test('QR deterministic terminal snapshot stays off stdout', async () => { + let stderr = ''; + const result = await renderQr('bloxwap:test', { + stderr: (value) => { + stderr = value; + }, + }); + expect(result.qrAscii).toMatchSnapshot(); + expect(stderr).toContain(result.qrAscii); + expect(await renderQr('disabled', { enabled: false })).toEqual({ + qrAscii: null, + qrPath: null, + }); }); diff --git a/test/read.test.ts b/test/read.test.ts index 633b569..b97f4d4 100644 --- a/test/read.test.ts +++ b/test/read.test.ts @@ -1,5 +1,20 @@ -import {expect,test} from 'vitest'; -import {accountBalance,positions,expiryStatus,type AccountState} from '../src/commands/read.js'; -import {migrate} from '../src/core/config.js'; -export const accountFixture:AccountState={marginSummary:{accountValue:'100.12345678',totalMarginUsed:'4',totalNtlPos:'20'},withdrawable:'96',assetPositions:[{position:{coin:'BTC',szi:'0.001',entryPx:'20000',unrealizedPnl:'1',liquidationPx:'10000',leverage:{type:'cross',value:5}}}]}; -test('read fixtures retain numeric strings and exact balances',()=>{expect(accountBalance(accountFixture).accountValueUnits).toBe('10012345678');expect(positions(accountFixture)[0]).toMatchObject({size:'0.001',leverage:'5',entryPrice:'20000'});expect(expiryStatus(migrate({agentExpiry:86400000}),0).warnings).toHaveLength(3);}); +import { accountFixture } from './fixtures.js'; +import { expect, test } from 'vitest'; +import { + accountBalance, + positions, + expiryStatus, +} from '../src/commands/read.js'; +import { migrate } from '../src/core/config.js'; + +test('read fixtures retain numeric strings and exact balances', () => { + expect(accountBalance(accountFixture).accountValueUnits).toBe('10012345678'); + expect(positions(accountFixture)[0]).toMatchObject({ + size: '0.001', + leverage: '5', + entryPrice: '20000', + }); + expect( + expiryStatus(migrate({ agentExpiry: 86400000 }), 0).warnings, + ).toHaveLength(3); +}); diff --git a/test/relay.test.ts b/test/relay.test.ts index da650ea..fc446d2 100644 --- a/test/relay.test.ts +++ b/test/relay.test.ts @@ -1,8 +1,30 @@ -import {expect,test,vi} from 'vitest'; -import {RelayClient,PERPS_USDC,parseQuote} from '../src/funding/relay.js'; -export const quoteFixture={steps:[{id:'deposit',kind:'transaction',requestId:'deposit-1',depositAddress:'0x'+'a'.repeat(40),items:[{status:'incomplete',data:{chainId:42161}}]}],fees:{app:{amount:'10'},gas:{amount:'20'},relayerService:{amount:'30'}},details:{currencyOut:{amount:'100000000',currency:{decimals:8}},minAmount:'1000000',expiresAt:2000000000000}}; -test('Relay request uses perps destination and parses fees/address',async()=>{ - const fetcher=vi.fn(async()=>new Response(JSON.stringify(quoteFixture))) as unknown as typeof fetch; - const client=new RelayClient('https://api.relay.link',fetcher);const quote=await client.quote({user:'master',recipient:'master',originChainId:42161,originCurrency:'USDC',amount:'1000000',destinationChainId:1337,destinationCurrency:PERPS_USDC,useDepositAddress:true,tradeType:'EXACT_INPUT'}); - expect(parseQuote(quote)).toMatchObject({depositId:'deposit-1',expectedReceived:'100000000',fees:{app:{amount:'10'},gas:{amount:'20'},relayer:{amount:'30'}}}); +import { quoteFixture } from './fixtures.js'; +import { expect, test, vi } from 'vitest'; +import { RelayClient, PERPS_USDC, parseQuote } from '../src/funding/relay.js'; + +test('Relay request uses perps destination and parses fees/address', async () => { + const fetcher = vi.fn( + async () => new Response(JSON.stringify(quoteFixture)), + ) as unknown as typeof fetch; + const client = new RelayClient('https://api.relay.link', fetcher); + const quote = await client.quote({ + user: 'master', + recipient: 'master', + originChainId: 42161, + originCurrency: 'USDC', + amount: '1000000', + destinationChainId: 1337, + destinationCurrency: PERPS_USDC, + useDepositAddress: true, + tradeType: 'EXACT_INPUT', + }); + expect(parseQuote(quote)).toMatchObject({ + depositId: 'deposit-1', + expectedReceived: '100000000', + fees: { + app: { amount: '10' }, + gas: { amount: '20' }, + relayer: { amount: '30' }, + }, + }); }); diff --git a/test/retry.test.ts b/test/retry.test.ts index aab8bbe..f73c5e5 100644 --- a/test/retry.test.ts +++ b/test/retry.test.ts @@ -1,16 +1,45 @@ -import {expect,test} from 'vitest'; -import {mkdtemp,rm} from 'node:fs/promises'; -import {tmpdir} from 'node:os'; -import {join} from 'node:path'; -import {retry} from '../src/core/retry.js'; -import {nextNonce} from '../src/core/nonce.js'; -import {AgentCliError} from '../src/errors/index.js'; -test('bounded backoff only retries temporary errors',async()=>{ - let attempts=0;const sleeps:number[]=[]; - expect(await retry(async()=>{if(++attempts<3) throw new AgentCliError('RATE_LIMIT','wait');return 7;},{baseMs:100,random:()=>0.5,sleep:async ms=>{sleeps.push(ms);}})).toBe(7);expect(sleeps).toEqual([100,200]); - await expect(retry(async()=>{throw new AgentCliError('POLICY_REJECTED','stop');})).rejects.toMatchObject({retryable:false}); +import { expect, test } from 'vitest'; +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { retry } from '../src/core/retry.js'; +import { nextNonce } from '../src/core/nonce.js'; +import { AgentCliError } from '../src/errors/index.js'; +test('bounded backoff only retries temporary errors', async () => { + let attempts = 0; + const sleeps: number[] = []; + expect( + await retry( + async () => { + if (++attempts < 3) throw new AgentCliError('RATE_LIMIT', 'wait'); + return 7; + }, + { + baseMs: 100, + random: () => 0.5, + sleep: async (ms) => { + sleeps.push(ms); + }, + }, + ), + ).toBe(7); + expect(sleeps).toEqual([100, 200]); + await expect( + retry(async () => { + throw new AgentCliError('POLICY_REJECTED', 'stop'); + }), + ).rejects.toMatchObject({ retryable: false }); }); -test('persisted nonces serialize concurrent callers and restarts',async()=>{ - const dir=await mkdtemp(join(tmpdir(),'bloxwap-nonce-'));const signer='0x'+'a'.repeat(40); - try{const values=await Promise.all(Array.from({length:8},()=>nextNonce(signer,dir,()=>1000)));expect(new Set(values).size).toBe(8);expect(await nextNonce(signer,dir,()=>500)).toBe(1008);}finally{await rm(dir,{recursive:true,force:true});} +test('persisted nonces serialize concurrent callers and restarts', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-nonce-')); + const signer = '0x' + 'a'.repeat(40); + try { + const values = await Promise.all( + Array.from({ length: 8 }, () => nextNonce(signer, dir, () => 1000)), + ); + expect(new Set(values).size).toBe(8); + expect(await nextNonce(signer, dir, () => 500)).toBe(1008); + } finally { + await rm(dir, { recursive: true, force: true }); + } }); diff --git a/test/revoke.test.ts b/test/revoke.test.ts index d65fbc6..d407b81 100644 --- a/test/revoke.test.ts +++ b/test/revoke.test.ts @@ -1,5 +1,15 @@ -import {expect,test} from 'vitest'; -import {markRevoked} from '../src/commands/revoke.js'; -import {assertCanTrade} from '../src/core/trading.js'; -import {migrate} from '../src/core/config.js'; -test('revocation immediately blocks every trading entrypoint',async()=>{const config=migrate({approved:true,agentExpiry:Date.now()+10000,masterAddress:'0x'+'a'.repeat(40)});markRevoked(config);await expect(assertCanTrade(config)).rejects.toMatchObject({code:'AGENT_NOT_APPROVED'});}); +import { expect, test } from 'vitest'; +import { markRevoked } from '../src/commands/revoke.js'; +import { assertCanTrade } from '../src/core/trading.js'; +import { migrate } from '../src/core/config.js'; +test('revocation immediately blocks every trading entrypoint', async () => { + const config = migrate({ + approved: true, + agentExpiry: Date.now() + 10000, + masterAddress: '0x' + 'a'.repeat(40), + }); + markRevoked(config); + await expect(assertCanTrade(config)).rejects.toMatchObject({ + code: 'AGENT_NOT_APPROVED', + }); +}); diff --git a/test/risk.test.ts b/test/risk.test.ts index c61ba3e..f89e44d 100644 --- a/test/risk.test.ts +++ b/test/risk.test.ts @@ -1,6 +1,51 @@ -import {expect,test} from 'vitest'; -import {evaluateRisk} from '../src/core/risk.js'; -import {migrate} from '../src/core/config.js'; -const intent={coin:'BTC',side:'buy' as const,size:'2',price:'10',leverage:2};const state={positionSize:'1',dailyLoss:'5',openOrders:2,marketMaxLeverage:40}; -test.each([{allowedMarkets:['ETH']},{limits:{maxOrderSize:1}},{limits:{maxNotional:19}},{limits:{maxPositionNotional:29}},{limits:{dailyLoss:4}},{limits:{maxOpenOrders:2}},{limits:{maxLeverage:1}}])('enforces each risk bound %j',config=>{expect(()=>evaluateRisk(migrate(config),intent,state)).toThrow(expect.objectContaining({code:'POLICY_REJECTED'}));}); -test('reduce-only cannot increase exposure and decimals remain exact',()=>{expect(evaluateRisk(migrate({}),intent,state).allowed).toBe(true);expect(()=>evaluateRisk(migrate({}),{...intent,reduceOnly:true},state)).toThrow();expect(evaluateRisk(migrate({}),{...intent,side:'sell',reduceOnly:true},state).allowed).toBe(true);}); +import { expect, test } from 'vitest'; +import { evaluateRisk } from '../src/core/risk.js'; +import { migrate } from '../src/core/config.js'; +const intent = { + coin: 'BTC', + side: 'buy' as const, + size: '2', + price: '10', + leverage: 2, +}; +const state = { + positionSize: '1', + dailyLoss: '5', + openOrders: 2, + marketMaxLeverage: 40, +}; +test.each([ + { allowedMarkets: ['ETH'] }, + { limits: { maxOrderSize: 1 } }, + { limits: { maxNotional: 19 } }, + { limits: { maxPositionNotional: 29 } }, + { limits: { dailyLoss: 4 } }, + { limits: { maxOpenOrders: 2 } }, + { limits: { maxLeverage: 1 } }, +])('enforces each risk bound %j', (config) => { + expect(() => evaluateRisk(migrate(config), intent, state)).toThrow( + expect.objectContaining({ code: 'POLICY_REJECTED' }), + ); +}); +test('reduce-only cannot increase exposure and decimals remain exact', () => { + expect(evaluateRisk(migrate({}), intent, state).allowed).toBe(true); + expect(() => + evaluateRisk(migrate({}), { ...intent, reduceOnly: true }, state), + ).toThrow(); + expect( + evaluateRisk( + migrate({}), + { ...intent, side: 'sell', reduceOnly: true }, + state, + ).allowed, + ).toBe(true); +}); +test('opposing pending orders cannot offset worst-case exposure', () => { + expect(() => + evaluateRisk(migrate({ limits: { maxPositionNotional: 50 } }), intent, { + ...state, + pendingBuySize: '10', + pendingSellSize: '10', + }), + ).toThrow(expect.objectContaining({ code: 'POLICY_REJECTED' })); +}); diff --git a/test/rotate.test.ts b/test/rotate.test.ts index 9586de6..d3bb6c7 100644 --- a/test/rotate.test.ts +++ b/test/rotate.test.ts @@ -1,7 +1,24 @@ -import {expect,test,vi} from 'vitest'; -import {rotationHandoff,type Rotation} from '../src/commands/rotate.js'; -test('rotation never exposes old revocation until approval verification and swap',async()=>{ - const rotation={next:{},swapped:false,revoke:{action:{type:'approveAgent'}}} as unknown as Rotation;const swap=vi.fn(async()=>{}),persist=vi.fn(async()=>{}); - await expect(rotationHandoff(rotation,{verify:async()=>false,swap,persist})).rejects.toMatchObject({code:'AGENT_NOT_APPROVED'});expect(swap).not.toHaveBeenCalled(); - expect(await rotationHandoff(rotation,{verify:async()=>true,swap,persist})).toBe(rotation.revoke);expect(rotation.swapped).toBe(true);expect(persist).toHaveBeenCalledOnce(); +import { expect, test, vi } from 'vitest'; +import { rotationHandoff, type Rotation } from '../src/commands/rotate.js'; +test('rotation never exposes old revocation until approval verification and swap', async () => { + const rotation = { + next: {}, + swapped: false, + revoke: { action: { type: 'approveAgent' } }, + } as unknown as Rotation; + const swap = vi.fn(async () => {}), + persist = vi.fn(async () => {}); + await expect( + rotationHandoff(rotation, { verify: async () => false, swap, persist }), + ).rejects.toMatchObject({ code: 'AGENT_NOT_APPROVED' }); + expect(swap).not.toHaveBeenCalled(); + expect( + await rotationHandoff(rotation, { + verify: async () => true, + swap, + persist, + }), + ).toBe(rotation.revoke); + expect(rotation.swapped).toBe(true); + expect(persist).toHaveBeenCalledOnce(); }); diff --git a/test/router.test.ts b/test/router.test.ts index 8032a9f..4ac0653 100644 --- a/test/router.test.ts +++ b/test/router.test.ts @@ -1,15 +1,65 @@ import { expect, test } from 'vitest'; import { z } from 'zod'; import { dispatch, register } from '../src/core/router.js'; -register('echo',{description:'test',schema:z.object({value:z.string()}),execute:async input=>input}); -register('crash',{description:'test',schema:z.object({}),execute:async()=>{throw new Error('unexpected');}}); -test('flags and stdin give one success envelope',async()=>{ - expect((await dispatch(['echo','--value','hello'])).body).toEqual({ok:true,data:{value:'hello'}}); - expect((await dispatch(['echo'],'{"value":"stdin"}')).exitCode).toBe(0); - expect((await dispatch(['echo','--json','{"value":"json"}'])).exitCode).toBe(0); -}); -test('validation and unexpected failures normalize',async()=>{ - expect((await dispatch(['echo'])).body).toMatchObject({ok:false,code:'CONFIG_INVALID'}); - expect((await dispatch(['crash'])).body).toMatchObject({ok:false,code:'INTERNAL'}); - expect((await dispatch(['schema','echo'])).exitCode).toBe(0); +register('echo', { + description: 'test', + schema: z.object({ value: z.string() }), + execute: async (input) => input, +}); +register('crash', { + description: 'test', + schema: z.object({}), + execute: async () => { + throw new Error('unexpected'); + }, +}); +test('flags and stdin give one success envelope', async () => { + expect((await dispatch(['echo', '--value', 'hello'])).body).toEqual({ + ok: true, + data: { value: 'hello' }, + }); + expect((await dispatch(['echo'], '{"value":"stdin"}')).exitCode).toBe(0); + expect( + (await dispatch(['echo', '--json', '{"value":"json"}'])).exitCode, + ).toBe(0); +}); +test('validation and unexpected failures normalize', async () => { + expect((await dispatch(['echo'])).body).toMatchObject({ + ok: false, + code: 'CONFIG_INVALID', + }); + expect((await dispatch(['crash'])).body).toMatchObject({ + ok: false, + code: 'INTERNAL', + }); + expect((await dispatch(['schema', 'echo'])).exitCode).toBe(0); +}); +test('output schemas expose data fields and the fixed error catalog', async () => { + const { schemas } = await import('../src/core/router.js'); + register('balance', { + description: 'balance', + schema: z.object({}), + execute: async () => ({}), + }); + const schema = schemas('balance'); + expect(schema.output.oneOf[0].properties.data).toMatchObject({ + required: [ + 'accountValue', + 'accountValueUnits', + 'withdrawable', + 'marginUsed', + ], + }); +}); +test('unknown fields are rejected instead of silently changing trade intent', async () => { + expect( + (await dispatch(['echo', '--json', '{"value":"hello","typo":true}'])).body, + ).toMatchObject({ ok: false, code: 'CONFIG_INVALID' }); +}); +test('JSON prototype fields cannot conceal master secrets during parsing', async () => { + const payload = '{"__proto__":{"privateKey":"' + 'ab'.repeat(32) + '"}}'; + expect((await dispatch(['echo', '--json', payload])).body).toMatchObject({ + ok: false, + code: 'POLICY_REJECTED', + }); }); diff --git a/test/rpc.test.ts b/test/rpc.test.ts index 77ded29..92bc9cc 100644 --- a/test/rpc.test.ts +++ b/test/rpc.test.ts @@ -1,13 +1,48 @@ -import {expect,test} from 'vitest'; -import {mkdtemp,rm,stat} from 'node:fs/promises'; -import {tmpdir} from 'node:os'; -import {join} from 'node:path'; -import {Runner} from '../src/runner/lifecycle.js'; -import {rpcServer,rpcCall,socketPath} from '../src/runner/rpc.js'; -import {migrate} from '../src/core/config.js'; -test('private RPC framing, multi-client serialization and normalized errors',async()=>{ - const dir=await mkdtemp(join(tmpdir(),'bloxwap-rpc-'));const runner=new Runner(migrate({}),dir); - try{await rpcServer(runner,{pretty:false},async(name,input)=>({name,input}));const path=socketPath(dir);if(process.platform!=='win32') expect((await stat(path)).mode&0o777).toBe(0o600); - expect(await Promise.all([rpcCall('status.get',{},path),rpcCall('balance',{},path)])).toEqual([{name:'status',input:{}},{name:'balance',input:{}}]);await expect(rpcCall('withdraw',{},path)).rejects.toMatchObject({code:'CONFIG_INVALID'}); - }finally{await runner.shutdown();await rm(dir,{recursive:true,force:true});} +import { expect, test } from 'vitest'; +import { mkdtemp, rm, stat } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { Runner } from '../src/runner/lifecycle.js'; +import { rpcServer, rpcCall, socketPath } from '../src/runner/rpc.js'; +import { migrate } from '../src/core/config.js'; +test('private RPC framing, multi-client serialization and normalized errors', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-rpc-')); + const runner = new Runner(migrate({}), dir); + try { + await rpcServer(runner, { pretty: false }, async (name, input) => ({ + name, + input, + })); + const path = socketPath(dir); + if (process.platform !== 'win32') + expect((await stat(path)).mode & 0o777).toBe(0o600); + expect( + await Promise.all([ + rpcCall('status.get', {}, path), + rpcCall('balance', {}, path), + ]), + ).toEqual([ + { name: 'status', input: {} }, + { name: 'balance', input: {} }, + ]); + await expect(rpcCall('withdraw', {}, path)).rejects.toMatchObject({ + code: 'CONFIG_INVALID', + }); + } finally { + await runner.shutdown(); + await rm(dir, { recursive: true, force: true }); + } +}); +test('RPC refuses a client targeting another network', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-rpc-network-')); + const runner = new Runner(migrate({ network: 'testnet' }), dir); + try { + await rpcServer(runner, { pretty: false }, async () => ({ ok: true })); + await expect( + rpcCall('order', { __context: { network: 'mainnet' } }, socketPath(dir)), + ).rejects.toMatchObject({ code: 'CONFIG_INVALID' }); + } finally { + await runner.shutdown(); + await rm(dir, { recursive: true, force: true }); + } }); diff --git a/test/runner.test.ts b/test/runner.test.ts index 1311d15..3cb44ba 100644 --- a/test/runner.test.ts +++ b/test/runner.test.ts @@ -1,11 +1,56 @@ -import {expect,test} from 'vitest'; -import {mkdtemp,rm,readFile} from 'node:fs/promises'; -import {join} from 'node:path'; -import {tmpdir} from 'node:os'; -import {Runner,runningInfo} from '../src/runner/lifecycle.js'; -import {migrate} from '../src/core/config.js'; -test('single instance and graceful cleanup preserve external state',async()=>{ - const dir=await mkdtemp(join(tmpdir(),'bloxwap-runner-'));const signer={address:'0x'+'a'.repeat(40) as `0x${string}`,signTypedData:async()=>'0x123' as `0x${string}`}; - const first=new Runner(migrate({}),dir);const second=new Runner(migrate({}),dir); - try{expect((await first.start(async()=>signer)).existing).toBe(false);expect((await second.start(async()=>signer)).existing).toBe(true);expect(await runningInfo(join(dir,'runner.pid'))).toMatchObject({pid:process.pid});await first.shutdown();expect(await runningInfo(join(dir,'runner.pid'))).toBeNull();expect(await readFile(join(dir,'logs','runner.jsonl'),'utf8')).toContain('Runner stopped');}finally{await first.shutdown();await rm(dir,{recursive:true,force:true});} +import { expect, test } from 'vitest'; +import { mkdtemp, rm, readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { Runner, runningInfo } from '../src/runner/lifecycle.js'; +import { migrate } from '../src/core/config.js'; +test('single instance and graceful cleanup preserve external state', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-runner-')); + const signer = { + address: ('0x' + 'a'.repeat(40)) as `0x${string}`, + signTypedData: async () => '0x123' as `0x${string}`, + }; + const first = new Runner(migrate({}), dir); + const second = new Runner(migrate({}), dir); + try { + expect((await first.start(async () => signer)).existing).toBe(false); + expect((await second.start(async () => signer)).existing).toBe(true); + expect(await runningInfo(join(dir, 'runner.pid'))).toMatchObject({ + pid: process.pid, + }); + await first.shutdown(); + expect(await runningInfo(join(dir, 'runner.pid'))).toBeNull(); + expect(await readFile(join(dir, 'logs', 'runner.jsonl'), 'utf8')).toContain( + 'Runner stopped', + ); + } finally { + await first.shutdown(); + await rm(dir, { recursive: true, force: true }); + } +}); +test('a dead PID restarts and readiness is explicit', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-runner-restart-')); + const runner = new Runner(migrate({}), dir); + try { + const { writeFile } = await import('node:fs/promises'); + await writeFile( + join(dir, 'runner.pid'), + JSON.stringify({ + pid: 2147483647, + network: 'testnet', + startedAt: 0, + ready: true, + }), + ); + await runner.start(async () => ({ + address: ('0x' + 'a'.repeat(40)) as `0x${string}`, + signTypedData: async () => '0x1', + })); + expect((await runningInfo(join(dir, 'runner.pid')))?.ready).toBe(false); + await runner.markReady(); + expect((await runningInfo(join(dir, 'runner.pid')))?.ready).toBe(true); + } finally { + await runner.shutdown(); + await rm(dir, { recursive: true, force: true }); + } }); diff --git a/test/safety.test.ts b/test/safety.test.ts index 9e2da66..0d63239 100644 --- a/test/safety.test.ts +++ b/test/safety.test.ts @@ -1,10 +1,44 @@ -import {expect,test} from 'vitest'; -import {assertSafeInput} from '../src/wallet/safety.js'; -import {dispatch} from '../src/core/router.js'; -import {redact} from '../src/core/logger.js'; -test('secret detector covers keys, seeds and nested ignored fields',()=>{ - for(const value of ['ab'.repeat(32),'0x'+'12'.repeat(32),'abandon '.repeat(11)+'about',{ignored:{seedPhrase:'secret'}},['abandon '.repeat(12).trim()]]) expect(()=>assertSafeInput(value)).toThrow();assertSafeInput({signature:'0x'+'12'.repeat(65),address:'0x'+'a'.repeat(40)}); +import { expect, test } from 'vitest'; +import { assertSafeInput } from '../src/wallet/safety.js'; +import { dispatch } from '../src/core/router.js'; +import { redact } from '../src/core/logger.js'; +test('secret detector covers keys, seeds and nested ignored fields', () => { + for (const value of [ + 'ab'.repeat(32), + '0x' + '12'.repeat(32), + 'abandon '.repeat(11) + 'about', + { ignored: { seedPhrase: 'secret' } }, + ['abandon '.repeat(12).trim()], + ]) + expect(() => assertSafeInput(value)).toThrow(); + assertSafeInput({ + signature: '0x' + '12'.repeat(65), + address: '0x' + 'a'.repeat(40), + }); }); -test('environment secrets are scrubbed from arbitrary messages',async()=>{ - process.env.BLOXWAP_SECRET_TOKEN='sensitive-value';try{expect(redact('failure: sensitive-value')).toBe('failure: [REDACTED]');expect((await dispatch(['config','--json',JSON.stringify({privateKey:'ab'.repeat(32)})])).body).toMatchObject({ok:false});}finally{delete process.env.BLOXWAP_SECRET_TOKEN;} +test('environment secrets are scrubbed from arbitrary messages', async () => { + process.env.BLOXWAP_SECRET_TOKEN = 'sensitive-value'; + try { + expect(redact('failure: sensitive-value')).toBe('failure: [REDACTED]'); + expect( + ( + await dispatch([ + 'config', + '--json', + JSON.stringify({ privateKey: 'ab'.repeat(32) }), + ]) + ).body, + ).toMatchObject({ ok: false }); + } finally { + delete process.env.BLOXWAP_SECRET_TOKEN; + } +}); +test('secrets embedded in free-form intent text are also refused', () => { + expect(() => + assertSafeInput('key: 0x' + '12'.repeat(32) + ' please use it'), + ).toThrow(); + expect(() => + assertSafeInput('recovery words: ' + 'abandon '.repeat(11) + 'about'), + ).toThrow(); + assertSafeInput('signature: 0x' + '12'.repeat(65)); }); diff --git a/test/setup.ts b/test/setup.ts new file mode 100644 index 0000000..7907761 --- /dev/null +++ b/test/setup.ts @@ -0,0 +1,20 @@ +import { afterAll, beforeAll, vi } from 'vitest'; +import { mkdir, rm } from 'node:fs/promises'; +const testHome = vi.hoisted( + () => + `/tmp/bloxwap-unit-${process.pid}-${Math.random().toString(16).slice(2)}`, +); +vi.mock('node:os', async (importOriginal) => ({ + ...(await importOriginal()), + homedir: () => testHome, +})); +beforeAll(async () => { + await mkdir(testHome, { recursive: true, mode: 0o700 }); +}); +afterAll(async () => { + await rm(testHome, { recursive: true, force: true }); +}); +// Explicit service fixtures may replace this stub; accidental unit-test network fails. +vi.stubGlobal('fetch', async () => { + throw new Error('Unit test attempted an unmocked network request'); +}); diff --git a/test/skill.test.ts b/test/skill.test.ts new file mode 100644 index 0000000..ba3e662 --- /dev/null +++ b/test/skill.test.ts @@ -0,0 +1,42 @@ +import { expect, test } from 'vitest'; +import { mkdtemp, rm, readFile, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { installSkill } from '../src/commands/skill.js'; +import { skillContent, cliVersion } from '../src/core/skill-content.js'; +import packageInfo from '../package.json' with { type: 'json' }; +test('installs correct agent paths with version match and idempotency', async () => { + const home = await mkdtemp(join(tmpdir(), 'bloxwap-skill-')); + try { + expect(cliVersion).toBe(packageInfo.version); + const codex = await installSkill('codex', { + home, + codexHome: join(home, 'custom-codex'), + }); + expect(codex.path).toBe( + join(home, 'custom-codex', 'skills', 'agent-cli', 'SKILL.md'), + ); + expect(await readFile(codex.path, 'utf8')).toBe(skillContent); + expect( + ( + await installSkill('codex', { + home, + codexHome: join(home, 'custom-codex'), + }) + ).changed, + ).toBe(false); + const claude = await installSkill('claude', { home }); + expect(claude.path).toBe( + join(home, '.claude', 'skills', 'agent-cli', 'SKILL.md'), + ); + await writeFile(claude.path, 'custom'); + await expect(installSkill('claude', { home })).rejects.toMatchObject({ + code: 'CONFIG_INVALID', + }); + expect((await installSkill('claude', { home, force: true })).changed).toBe( + true, + ); + } finally { + await rm(home, { recursive: true, force: true }); + } +}); diff --git a/test/subscriptions.test.ts b/test/subscriptions.test.ts index 694510e..185e2aa 100644 --- a/test/subscriptions.test.ts +++ b/test/subscriptions.test.ts @@ -1,12 +1,42 @@ -import {expect,test} from 'vitest'; -import {mkdtemp,rm} from 'node:fs/promises'; -import {tmpdir} from 'node:os'; -import {join} from 'node:path'; -import {Runner} from '../src/runner/lifecycle.js'; -import {Subscriptions,subscriptionSet} from '../src/runner/subscriptions.js'; -import {migrate} from '../src/core/config.js'; -test('reconnect gap-fill dedupes fills and persists subscription cursor',async()=>{ - const dir=await mkdtemp(join(tmpdir(),'bloxwap-ws-'));const runner=new Runner(migrate({allowedMarkets:['BTC']}),dir);const events:unknown[]=[];let state:(value:string)=>void=()=>{}; - const backend={subscribe:async()=>({unsubscribe:async()=>{}}),onState:(listener:typeof state)=>{state=listener;},close:async()=>{},gap:async()=>[{tid:1,time:1},{tid:2,time:2}]};const manager=new Subscriptions(runner,backend,async(type,data)=>{events.push({type,data});}); - try{await manager.start();await manager.fill({tid:1,time:1});state('connected');state('reconnecting');state('connected');await manager.flush();expect(events.filter(e=>(e as {type:string}).type==='fill')).toHaveLength(2);expect(subscriptionSet(runner.config)).toContain('l2Book:BTC');await runner.shutdown();}finally{await rm(dir,{recursive:true,force:true});} +import { expect, test } from 'vitest'; +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { Runner } from '../src/runner/lifecycle.js'; +import { Subscriptions, subscriptionSet } from '../src/runner/subscriptions.js'; +import { migrate } from '../src/core/config.js'; +test('reconnect gap-fill dedupes fills and persists subscription cursor', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-ws-')); + const runner = new Runner(migrate({ allowedMarkets: ['BTC'] }), dir); + const events: unknown[] = []; + let state: (value: string) => void = () => {}; + const backend = { + subscribe: async () => ({ unsubscribe: async () => {} }), + onState: (listener: typeof state) => { + state = listener; + }, + close: async () => {}, + gap: async () => [ + { tid: 1, time: 1 }, + { tid: 2, time: 2 }, + ], + }; + const manager = new Subscriptions(runner, backend, async (type, data) => { + events.push({ type, data }); + }); + try { + await manager.start(); + await manager.fill({ tid: 1, time: 1 }); + state('connected'); + state('reconnecting'); + state('connected'); + await manager.flush(); + expect( + events.filter((e) => (e as { type: string }).type === 'fill'), + ).toHaveLength(2); + expect(subscriptionSet(runner.config)).toContain('l2Book:BTC'); + await runner.shutdown(); + } finally { + await rm(dir, { recursive: true, force: true }); + } }); diff --git a/tsconfig.json b/tsconfig.json index 0827a67..996ddb8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1 +1,13 @@ -{"compilerOptions":{"target":"ES2022","module":"NodeNext","moduleResolution":"NodeNext","strict":true,"esModuleInterop":true,"skipLibCheck":true,"resolveJsonModule":true,"outDir":"dist"},"include":["src","test"]} +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "outDir": "dist" + }, + "include": ["src", "test"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..17ad725 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; +export default defineConfig({ + test: { + setupFiles: ['test/setup.ts'], + exclude: ['test/*.integration.test.ts', 'node_modules/**'], + }, +}); diff --git a/vitest.integration.config.ts b/vitest.integration.config.ts new file mode 100644 index 0000000..44ca262 --- /dev/null +++ b/vitest.integration.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from 'vitest/config'; +export default defineConfig({ + test: { include: ['test/*.integration.test.ts'] }, +}); From 89c3efe3179288d60a6e8e1227e28fa1a96e2677 Mon Sep 17 00:00:00 2001 From: Joe Blau Date: Tue, 13 Oct 2026 15:51:06 -0700 Subject: [PATCH 43/43] feat: default Bloxwap builder code and fee (#13) --- README.md | 6 +++-- SKILL.md | 12 +++++----- docs/ACCEPTANCE.md | 2 +- docs/PROGRESS.md | 4 ++-- docs/commands.md | 8 +++++++ docs/config.md | 16 ++++++++----- docs/issues/ROADMAP.md | 2 +- src/commands/fund.ts | 1 + src/commands/order.ts | 1 + src/core/config.ts | 15 ++++++++++--- src/core/output-schemas.ts | 5 ++++- src/core/skill-content.ts | 2 +- test/config.test.ts | 43 ++++++++++++++++++++++++++++++++++- test/fund.test.ts | 4 ++++ test/lifecycle.test.ts | 46 ++++++++++++++++++++++++++++++++++---- 15 files changed, 141 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index d63c8e1..2aa03ea 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,10 @@ limits. Read [custody limits](docs/custody.md) before using a funded account. State: `~/.bloxwap`; config: `~/.bloxwap/config.json`; IPC: `~/.bloxwap/run.sock`; env prefix: `BLOXWAP_`. All local state is private to the OS -user. Configure the official builder address/rate explicitly when available; -defaults charge zero fees instead of inventing a recipient. +user. The default Bloxwap builder address is +`0x71b09A08257078A4d642F7Dd7315e656c837329a`, with `f=10` (0.01%). +Override via `config set builder.address` / `config set builder.fee` or +`BLOXWAP_BUILDER_ADDRESS` / `BLOXWAP_BUILDER_FEE`; init requests builder approval. ## Development and docs diff --git a/SKILL.md b/SKILL.md index 2c1db1c..86bbd11 100644 --- a/SKILL.md +++ b/SKILL.md @@ -16,12 +16,14 @@ Use `--network testnet` during testing; never silently change to mainnet. 1. Configure Privy app ID and secret reference, e.g. `config set privyAppId APP` and `config set privyAppSecretRef env:BLOXWAP_PRIVY_APP_SECRET`. The user sets - the environment secret outside the conversation. Configure published builder - address/fee if applicable; unset defaults charge no builder fee. + the environment secret outside the conversation. The default Bloxwap builder is + `0x71b09A08257078A4d642F7Dd7315e656c837329a`, with `f=10` (1 bp = 0.01%). + Override with `config set builder.address` / `config set builder.fee` or + `BLOXWAP_BUILDER_ADDRESS` / `BLOXWAP_BUILDER_FEE`. 2. Run `init --master-address 0x... --network testnet`. Show the returned approval payload/deeplink/QR. The user signs in their master wallet; submit only the resulting signature with `init --signature 0x...`. Submit builder approval - separately if requested. Continue only after `approved:true`. + separately with `init --builder-signature 0x...` for the configured 0.01% default. Continue only after `approved:true`. 3. Run `fund --amount 10 --origin-chain 42161`. Show `qrAscii`/deposit address, origin chain/token, minimum, fees, expected received amount and expiry. On testnet, do not treat the default Arbitrum mainnet chain/token as a test route: @@ -55,7 +57,7 @@ Full inputs/outputs are discoverable with `schema`; amounts/prices are strings. | `config set limits.maxLeverage 5` | `{"ok":true,"data":{"version":1,"limits":{"maxLeverage":5}}}` | | `config validate` | `{"ok":true,"data":{"valid":true,"version":1}}` | | `init --master-address 0x...` | `{"ok":true,"data":{"approved":false,"requiresSignature":true,"approvals":{"agent":{"typedData":{}}},"qrAscii":"..."}}` | -| `fund --amount 10 --no-qr` | `{"ok":true,"data":{"depositId":"...","depositAddress":"0x...","expectedReceived":"100000000","builderFeeRate":"0%","state":"awaiting_funds","qrAscii":null}}` | +| `fund --amount 10 --no-qr` | `{"ok":true,"data":{"depositId":"...","depositAddress":"0x...","expectedReceived":"100000000","builderFeeRate":"0.01%","builderAddress":"0x71b09A08257078A4d642F7Dd7315e656c837329a","state":"awaiting_funds","qrAscii":null}}` | | `deposit --deposit-id ID` | `{"ok":true,"data":{"depositId":"ID","steps":[{"id":"step:0","requiresSignature":true,"payload":{}}]}}` | | `deposit --deposit-id ID --step-id step:0 --signature 0x...` | `{"ok":true,"data":{"depositId":"ID","steps":[]}}` | | `status` | `{"ok":true,"data":{"balance":{"accountValue":"100"},"agent":{"approved":true,"daysRemaining":"179"},"deposits":[],"policy":{"id":"...","version":"1"},"runner":{"pid":"123"}}}` | @@ -63,7 +65,7 @@ Full inputs/outputs are discoverable with `schema`; amounts/prices are strings. | `market --coin BTC` | `{"ok":true,"data":{"coin":"BTC","asset":"0","szDecimals":"5","maxLeverage":"40","mid":"20000"}}` | | `positions` | `{"ok":true,"data":[{"coin":"BTC","size":"0.001","entryPrice":"20000","leverage":"5","marginMode":"cross"}]}` | | `orders` | `{"ok":true,"data":[{"coin":"BTC","oid":"123","sz":"0.001","limitPx":"20000","side":"B"}]}` | -| `order --json '{"coin":"BTC","side":"buy","size":"0.001","price":"20000","tif":"Gtc","cloid":"0x11111111111111111111111111111111"}'` | `{"ok":true,"data":{"result":{"status":"ok","response":{"data":{"statuses":[{"resting":{"oid":123}}]}}},"builderFeeRate":"0%","cloid":"0x..."}}` | +| `order --json '{"coin":"BTC","side":"buy","size":"0.001","price":"20000","tif":"Gtc","cloid":"0x11111111111111111111111111111111"}'` | `{"ok":true,"data":{"result":{"status":"ok","response":{"data":{"statuses":[{"resting":{"oid":123}}]}}},"builderFeeRate":"0.01%","builderAddress":"0x71b09A08257078A4d642F7Dd7315e656c837329a","cloid":"0x..."}}` | | `order --json '{"coin":"BTC","side":"sell","size":"0.001","type":"market","slippage":0.01,"reduceOnly":true}'` | `{"ok":true,"data":{"result":{"status":"ok","response":{"data":{"statuses":[{"filled":{"oid":123}}]}}}}}` | | `cancel --oid 123` or `cancel --all` | `{"ok":true,"data":{"cancelled":["123"],"result":{"status":"ok"}}}` | | `modify --json '{"modifications":[{"oid":123,"price":"20100","size":"0.001"}]}'` | `{"ok":true,"data":{"status":"ok"}}` | diff --git a/docs/ACCEPTANCE.md b/docs/ACCEPTANCE.md index 21c14e2..bb68cef 100644 --- a/docs/ACCEPTANCE.md +++ b/docs/ACCEPTANCE.md @@ -11,7 +11,7 @@ items. The final local run uses the SDK-supported Node 22 runtime and Bun. | #3–#4 | Stable append-only error catalog and serialization/exit tests; strict flags/JSON/stdin parser, input/output schemas, typo and uncaught-error tests. Streaming exceptions are explicit. | | #5–#6 | Config validation/migration/atomic private writes/corrupt-file tests; recursive field/env/PEM redaction, request correlation and size/day rotation. | | #7–#9 | Requested SDK, network factories, shared metadata cache/refresh, exact rounding fixtures, service mapping fixtures, bounded retries and exclusive persisted nonce tests. | -| #10–#13 | P-256 managed-wallet creation/linking and typed-data adapter; versioned Privy domain/source policy and local forbidden-action tests; resumable external master approvals with persisted per-master nonces, expiry bounds and builder approval verification. Missing official builder defaults and hash-preimage policy limitations are explicit in PROGRESS/custody docs. Sandbox calls are separately gated. | +| #10–#13 | P-256 managed-wallet creation/linking and typed-data adapter; versioned Privy domain/source policy and local forbidden-action tests; resumable external master approvals with persisted per-master nonces, expiry bounds and builder approval verification. Bloxwap builder defaults to f=10 (0.01%); hash-preimage policy limitations are explicit in PROGRESS/custody docs. Sandbox calls are separately gated. | | #14–#19 | Relay quote/fee/address fixture tests, bigint round trips including signed values, terminal QR snapshot and file renderer, idempotent funding store, ordered/resumable signature tests, terminal deposit transitions and collateral receipt excluding marked PnL. | | #20–#25 | Numeric-string account/order/market projections, approval/policy/deposit/expiry/runner status; every risk bound tested, opposing pending exposure and realized/marked daily PnL checked; SDK order fixture tests cover limit/IOC/slippage/trigger behavior, cancellation, batch modification preserving reduce-only/TIF/trigger semantics and leverage ceilings/loss. | | #26–#28 | Private PID/socket lifecycle, stale dead-PID recovery/readiness, serialized RPC, cross-network/config refusal and direct fallback only on confirmed absence; simulated WS drop, SDK resubscribe, paginated info gap-fill, persisted fill dedupe. Windows ACL limitations are documented. | diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 0defbec..204bd44 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -15,7 +15,7 @@ | #10 | done | Privy server SDK create/link, P-256 owner, private authorization storage and typed-data signer; sandbox test env-gated. | | #11 | partial | Versioned domain/network Privy policy and local trade allowlist; L1 action hash prevents Privy per-action/notional inspection. Exchange agent permissions enforce withdrawal boundary. | | #12 | done | Idempotent managed-wallet onboarding, persisted external master EIP-712 approvals/deeplink, expiry bound, agent/builder verification and master-key rejection. | -| #13 | partial | Builder attachment helper checks approved maximum; fee reporting. No official builder address/rate supplied in specs, so default is zero fee until configured. | +| #13 | done | Default Bloxwap builder address with f=10 (0.01%); config/env overrides, init approval, approved-maximum checks and fund/order address/rate reporting. | | #14 | done | Typed validated Relay quote/status adapter, perps USDC destination, multiple origin routes and fee parsing; mocked fixtures. | | #15 | done | Exact bigint decimal parse/format and loss-detecting 6↔8 conversions with property-style round trips. | | #16 | done | Terminal QR on stderr and JSON field, optional PNG/SVG, no-qr support and snapshot. | @@ -46,4 +46,4 @@ | #41 | partial | Changesets/version PR workflow, gated tag publish with npm provenance, generated release notes and binary/skill assets; publishing not run without registry credentials. | | #42 | done | Final packaged flow audited and corrected (network/config isolation, exact loss/exposure/receipt checks, trigger preservation, readiness and stream errors). Skill shipped/embedded in npm and compiled CLI; Claude/Codex installation with CODEX_HOME, version synchronization, idempotency and overwrite protection. | -Final local verification: supported Node 22, strict build/lint/typecheck and isolated offline tests. Sandbox/testnet scripts skip without credentials. npm audit reports zero vulnerabilities. Official builder defaults remain unspecified; fees stay disabled until configured. +Final local verification: supported Node 22, strict build/lint/typecheck and isolated offline tests. Sandbox/testnet scripts skip without credentials. npm audit reports zero vulnerabilities. Bloxwap builder defaults to f=10 (0.01%), with config/env overrides. diff --git a/docs/commands.md b/docs/commands.md index b1acd4a..c3299a7 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -312,6 +312,9 @@ Quote funding with address, QR and transparent fees "builderFeeRate": { "type": "string" }, + "builderAddress": { + "type": "string" + }, "expiresAt": { "type": "number" }, @@ -341,6 +344,7 @@ Quote funding with address, QR and transparent fees "expectedReceived", "fees", "builderFeeRate", + "builderAddress", "expiresAt", "qrAscii" ] @@ -1141,6 +1145,9 @@ Place limit, IOC market or TP/SL order "builderFeeRate": { "type": "string" }, + "builderAddress": { + "type": "string" + }, "cloid": { "type": [ "string", @@ -1151,6 +1158,7 @@ Place limit, IOC market or TP/SL order "required": [ "result", "builderFeeRate", + "builderAddress", "cloid" ] } diff --git a/docs/config.md b/docs/config.md index eb38c35..9079f46 100644 --- a/docs/config.md +++ b/docs/config.md @@ -11,8 +11,14 @@ persisted values; `config validate` validates them without overrides. Secrets use `env:BLOXWAP_PRIVY_APP_SECRET` and `env:BLOXWAP_AUTHORIZATION_KEY` references; authorization keys may also use `file:/absolute/private/path`. -Builder fees use tenths of a basis point (`fee:10` means 0.01%). Set -`builder.address` and `builder.fee`; each order verifies the master approved maximum. -The specs do not supply a Bloxwap builder address/fee schedule, so defaults disable -fees (`fee:0`, no address) instead of inventing a fee recipient. Deployments must -configure the official recipient before enabling builder fees. +The default builder address is `0x71b09A08257078A4d642F7Dd7315e656c837329a` +(Bloxwap), with `builder.fee:10`. Hyperliquid uses tenths of a basis point: +`f=10` = 1 bp = **0.01%**. Override with `config set builder.address 0x...` +and `config set builder.fee 10`, or `BLOXWAP_BUILDER_ADDRESS` and +`BLOXWAP_BUILDER_FEE` (environment overrides stored values for commands). +Fees must be integers from 0 to 100; `builder.fee:0` disables the fee. +`init` requests an external master-wallet `approveBuilderFee` signature for the +configured address/rate; submit it with `init --builder-signature 0x...`. +Each order checks the master-approved maximum before attaching `{b,f}`. +Fund and order JSON expose `builderAddress` and `builderFeeRate` (`"0.01%"` +by default). Funding reports the trading builder rate separately from Relay fees. diff --git a/docs/issues/ROADMAP.md b/docs/issues/ROADMAP.md index ea85f6e..a047643 100644 --- a/docs/issues/ROADMAP.md +++ b/docs/issues/ROADMAP.md @@ -38,7 +38,7 @@ Locked decisions: hybrid custody (approveAgent once, Privy-managed agent, trade- - [ ] #34 `revoke` command ### Funding -- [ ] #13 Builder code / builder fee configuration +- [x] #13 Builder code / builder fee configuration - [ ] #14 Relay quote client for HyperCore deposit addresses - [ ] #15 USDC decimal conversion: 6 (EVM) vs 8 (HyperCore perps) - [ ] #16 Terminal QR rendering diff --git a/src/commands/fund.ts b/src/commands/fund.ts index e84ecca..b78fe27 100644 --- a/src/commands/fund.ts +++ b/src/commands/fund.ts @@ -108,6 +108,7 @@ export async function fund( originCurrency: input.originCurrency, minDeposit: formatUsdc(BigInt(parsed.minAmount), input.originDecimals), builderFeeRate: builderRate(config), + builderAddress: config.builder.address, state: deposit.state, signatureSteps: signatureSteps(deposit), ...(await (dependencies.qr ?? renderQr)(parsed.depositAddress, { diff --git a/src/commands/order.ts b/src/commands/order.ts index d852d59..c2652fe 100644 --- a/src/commands/order.ts +++ b/src/commands/order.ts @@ -112,6 +112,7 @@ export async function placeOrder( return { result, builderFeeRate: builderRate(config), + builderAddress: config.builder.address, cloid: input.cloid ?? null, }; } diff --git a/src/core/config.ts b/src/core/config.ts index db110fc..5bc574b 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -42,10 +42,13 @@ export const configSchema = z allowedMarkets: z.array(z.string()).default([]), builder: z .object({ - address: address.optional(), - fee: z.number().int().min(0).max(100).default(0), + address: address.default('0x71b09A08257078A4d642F7Dd7315e656c837329a'), + fee: z.number().int().min(0).max(100).default(10), }) - .default({ fee: 0 }), + .default({ + address: '0x71b09A08257078A4d642F7Dd7315e656c837329a', + fee: 10, + }), logLevel: z.enum(['debug', 'info', 'warn', 'error']).default('info'), runnerFallback: z.boolean().default(true), expiryWarningDays: z.array(positive).default([14, 3, 1]), @@ -93,6 +96,12 @@ export async function loadConfig( })) { if (env[name]) Object.assign(config, { [key]: env[name] }); } + if (env.BLOXWAP_BUILDER_ADDRESS !== undefined) + config.builder.address = env.BLOXWAP_BUILDER_ADDRESS; + if (env.BLOXWAP_BUILDER_FEE !== undefined) + config.builder.fee = env.BLOXWAP_BUILDER_FEE.trim() + ? Number(env.BLOXWAP_BUILDER_FEE) + : NaN; return migrate(config); } export async function saveConfig(config: unknown, path = configPath()) { diff --git a/src/core/output-schemas.ts b/src/core/output-schemas.ts index eb34f19..0f06a18 100644 --- a/src/core/output-schemas.ts +++ b/src/core/output-schemas.ts @@ -86,6 +86,7 @@ export const outputSchemas: Record = { expectedReceivedHuman: string, fees: unknown, builderFeeRate: string, + builderAddress: string, expiresAt: { type: 'number' }, state: string, signatureSteps: array(unknown), @@ -98,6 +99,7 @@ export const outputSchemas: Record = { 'expectedReceived', 'fees', 'builderFeeRate', + 'builderAddress', 'expiresAt', 'qrAscii', ], @@ -166,9 +168,10 @@ export const outputSchemas: Record = { { result: unknown, builderFeeRate: string, + builderAddress: string, cloid: { type: ['string', 'null'] }, }, - ['result', 'builderFeeRate', 'cloid'], + ['result', 'builderFeeRate', 'builderAddress', 'cloid'], ), cancel: object({ cancelled: array(string), result: unknown }, [ 'cancelled', diff --git a/src/core/skill-content.ts b/src/core/skill-content.ts index d4519e9..8fd2c33 100644 --- a/src/core/skill-content.ts +++ b/src/core/skill-content.ts @@ -1,3 +1,3 @@ // Generated from SKILL.md and package.json. export const cliVersion="0.1.0"; -export const skillContent="---\nname: agent-cli\ndescription: Operate the agent-cli Hyperliquid trading CLI, including managed-agent onboarding, funding, orders, event monitoring, rotation, and normalized error recovery.\nmetadata:\n version: \"0.1.0\"\n---\n\nUse `agent-cli` for the user's authorized Hyperliquid account operations. Obtain\ncommand schemas with `agent-cli schema COMMAND`; arguments can be flags or\n`--json '{...}'`, or a JSON object on stdin. Ordinary commands print exactly one\nJSON object: `{ok:true,data}` or `{ok:false,code,message,retryable,details}`.\nLogs/QR appear on stderr. `events --follow` and `logs` explicitly stream JSON lines.\nUse `--network testnet` during testing; never silently change to mainnet.\n\n## Onboarding sequence\n\n1. Configure Privy app ID and secret reference, e.g. `config set privyAppId APP`\n and `config set privyAppSecretRef env:BLOXWAP_PRIVY_APP_SECRET`. The user sets\n the environment secret outside the conversation. Configure published builder\n address/fee if applicable; unset defaults charge no builder fee.\n2. Run `init --master-address 0x... --network testnet`. Show the returned approval\n payload/deeplink/QR. The user signs in their master wallet; submit only the\n resulting signature with `init --signature 0x...`. Submit builder approval\n separately if requested. Continue only after `approved:true`.\n3. Run `fund --amount 10 --origin-chain 42161`. Show `qrAscii`/deposit address,\n origin chain/token, minimum, fees, expected received amount and expiry. On\n testnet, do not treat the default Arbitrum mainnet chain/token as a test route:\n use an explicitly supported test chain/token and `originDecimals`, or the\n credential-gated CI harness with mocked Relay and pre-funded test collateral.\n If no test route is known, stop funding and report the missing route. The user\n transfers the quoted origin token. If `signatureSteps` require signatures,\n show the exact payload and submit each via `deposit` in returned order.\n4. Poll `status` until each active deposit is `settled`, for at most 12 polls\n or 5 minutes, whichever comes first. Start with 1-second delays, double to a\n 15-second cap, and stop with an explicit pending report if the budget expires.\n Settlement requires Relay success and Hyperliquid balance receipt. Stop on\n failed/refunded deposits and explain their status. Do not place orders while\n any deposit is awaiting funds or pending.\n5. Run `run --detach`, then monitor `events --follow --since SEQ`. Persist the\n last handled sequence; replay after reconnect. An unfilled or rejected order\n remains incomplete: do not claim the requested trade succeeded. A fill updates the trading\n plan, an error follows the catalog below, and expiry warnings prompt rotation\n using external master-wallet approval.\n\n## Commands and example envelopes\n\nExamples show representative `data`, not live balances or service promises.\nFull inputs/outputs are discoverable with `schema`; amounts/prices are strings.\n\n| Input | Example output |\n| --- | --- |\n| `--help` | `{\"ok\":true,\"data\":{\"commands\":[{\"name\":\"status\",\"description\":\"Read account status\"}],\"globalFlags\":[\"--json\",\"--config\",\"--network\",\"--pretty\"]}}` |\n| `schema order` | `{\"ok\":true,\"data\":{\"input\":{\"type\":\"object\"},\"output\":{\"oneOf\":[]}}}` (actual schemas include properties) |\n| `config get network` | `{\"ok\":true,\"data\":\"testnet\"}` |\n| `config set limits.maxLeverage 5` | `{\"ok\":true,\"data\":{\"version\":1,\"limits\":{\"maxLeverage\":5}}}` |\n| `config validate` | `{\"ok\":true,\"data\":{\"valid\":true,\"version\":1}}` |\n| `init --master-address 0x...` | `{\"ok\":true,\"data\":{\"approved\":false,\"requiresSignature\":true,\"approvals\":{\"agent\":{\"typedData\":{}}},\"qrAscii\":\"...\"}}` |\n| `fund --amount 10 --no-qr` | `{\"ok\":true,\"data\":{\"depositId\":\"...\",\"depositAddress\":\"0x...\",\"expectedReceived\":\"100000000\",\"builderFeeRate\":\"0%\",\"state\":\"awaiting_funds\",\"qrAscii\":null}}` |\n| `deposit --deposit-id ID` | `{\"ok\":true,\"data\":{\"depositId\":\"ID\",\"steps\":[{\"id\":\"step:0\",\"requiresSignature\":true,\"payload\":{}}]}}` |\n| `deposit --deposit-id ID --step-id step:0 --signature 0x...` | `{\"ok\":true,\"data\":{\"depositId\":\"ID\",\"steps\":[]}}` |\n| `status` | `{\"ok\":true,\"data\":{\"balance\":{\"accountValue\":\"100\"},\"agent\":{\"approved\":true,\"daysRemaining\":\"179\"},\"deposits\":[],\"policy\":{\"id\":\"...\",\"version\":\"1\"},\"runner\":{\"pid\":\"123\"}}}` |\n| `balance` | `{\"ok\":true,\"data\":{\"accountValue\":\"100\",\"accountValueUnits\":\"10000000000\",\"withdrawable\":\"96\",\"marginUsed\":\"4\"}}` |\n| `market --coin BTC` | `{\"ok\":true,\"data\":{\"coin\":\"BTC\",\"asset\":\"0\",\"szDecimals\":\"5\",\"maxLeverage\":\"40\",\"mid\":\"20000\"}}` |\n| `positions` | `{\"ok\":true,\"data\":[{\"coin\":\"BTC\",\"size\":\"0.001\",\"entryPrice\":\"20000\",\"leverage\":\"5\",\"marginMode\":\"cross\"}]}` |\n| `orders` | `{\"ok\":true,\"data\":[{\"coin\":\"BTC\",\"oid\":\"123\",\"sz\":\"0.001\",\"limitPx\":\"20000\",\"side\":\"B\"}]}` |\n| `order --json '{\"coin\":\"BTC\",\"side\":\"buy\",\"size\":\"0.001\",\"price\":\"20000\",\"tif\":\"Gtc\",\"cloid\":\"0x11111111111111111111111111111111\"}'` | `{\"ok\":true,\"data\":{\"result\":{\"status\":\"ok\",\"response\":{\"data\":{\"statuses\":[{\"resting\":{\"oid\":123}}]}}},\"builderFeeRate\":\"0%\",\"cloid\":\"0x...\"}}` |\n| `order --json '{\"coin\":\"BTC\",\"side\":\"sell\",\"size\":\"0.001\",\"type\":\"market\",\"slippage\":0.01,\"reduceOnly\":true}'` | `{\"ok\":true,\"data\":{\"result\":{\"status\":\"ok\",\"response\":{\"data\":{\"statuses\":[{\"filled\":{\"oid\":123}}]}}}}}` |\n| `cancel --oid 123` or `cancel --all` | `{\"ok\":true,\"data\":{\"cancelled\":[\"123\"],\"result\":{\"status\":\"ok\"}}}` |\n| `modify --json '{\"modifications\":[{\"oid\":123,\"price\":\"20100\",\"size\":\"0.001\"}]}'` | `{\"ok\":true,\"data\":{\"status\":\"ok\"}}` |\n| `leverage --coin BTC --leverage 5 --mode isolated` | `{\"ok\":true,\"data\":{\"status\":\"ok\"}}` |\n| `run --detach` | `{\"ok\":true,\"data\":{\"pid\":123,\"startedAt\":1700000000000,\"network\":\"testnet\"}}` |\n| `events --since 12 --types '[\"fill\"]'` | `{\"ok\":true,\"data\":[{\"seq\":13,\"timestamp\":\"2026-01-01T00:00:00.000Z\",\"type\":\"fill\",\"data\":{\"tid\":1}}]}` |\n| `events --follow` | `{\"seq\":13,\"timestamp\":\"2026-01-01T00:00:00.000Z\",\"type\":\"fill\",\"data\":{\"tid\":1}}` (one line per event) |\n| `logs --follow --level warn` | `{\"level\":\"warn\",\"requestId\":\"...\",\"message\":\"Risk rejected\",\"time\":\"...\"}` (JSON lines) |\n| `rotate` then `rotate --signature 0x...` | `{\"ok\":true,\"data\":{\"oldAgent\":\"0x...\",\"newAgent\":\"0x...\",\"swapped\":true,\"oldRevoked\":false,\"revokeApproval\":{}}}` |\n| `rotate --revoke-signature 0x...` | `{\"ok\":true,\"data\":{\"swapped\":true,\"oldRevoked\":true}}` |\n| `revoke --cancel-orders` | `{\"ok\":true,\"data\":{\"locallyRevoked\":true,\"exchangeRevoked\":false,\"requiresSignature\":true,\"approval\":{}}}` |\n| `revoke --signature 0x...` | `{\"ok\":true,\"data\":{\"locallyRevoked\":true,\"exchangeRevoked\":true}}` |\n| `audit --intent ID` | `{\"ok\":true,\"data\":[{\"intentId\":\"ID\",\"stage\":\"decision\",\"data\":{\"allowed\":true},\"previousHash\":\"...\",\"hash\":\"...\"}]}` |\n| `skill install --target codex` | `{\"ok\":true,\"data\":{\"target\":\"codex\",\"path\":\".../agent-cli/SKILL.md\",\"version\":\"0.1.0\"}}` |\n\nBefore an order, query `market --coin BTC` for `mid`, `szDecimals`, asset and\nmarket leverage ceiling. Compute size/notional with decimal strings/bigints,\nusing the user-approved limit or the bounded IOC execution price. A valid schema\nalone does not establish that an order is tradable or within the budget.\n\nUse `--intent-id ID --intent \"reason\"` on trading actions to connect the user's\nreasoning to the audit. Keep cloid stable when retrying a known order intent.\nFor TP/SL use `type:\"tp\"|\"sl\"` and `triggerPrice` with size/price/reduceOnly;\nmarket orders use an explicit slippage-bounded IOC, not an unbounded market order.\n\n## Error recovery\n\n| Code | Action |\n| --- | --- |\n| RATE_LIMIT, NETWORK_ERROR | At most 3 read retries with exponential backoff; for uncertain mutations, reconcile order state/cloid before retrying. Stop and report unresolved results |\n| NONCE_COLLISION | Retry using a newly issued nonce; inspect a stale lock if persistent |\n| DEPOSIT_PENDING | Poll status until settled; stop on terminal funding failure |\n| RUNNER_NOT_RUNNING | Start runner or use configured direct fallback |\n| INSUFFICIENT_MARGIN | Reduce exposure or request authorized funding |\n| POLICY_REJECTED | Stop and revise intent within limits; do not bypass policy |\n| AGENT_EXPIRED | Rotate with master-wallet approval |\n| AGENT_NOT_APPROVED | Complete init/approval or explain revocation |\n| INVALID_SIZE, INVALID_PRICE | Correct input using market precision |\n| UNKNOWN_MARKET | Refresh metadata; use an allowed market |\n| ORDER_NOT_FOUND | Refresh orders before changing intent |\n| CONFIG_INVALID | Correct configuration/input; never insert secrets |\n| UPSTREAM_ERROR, INTERNAL | Stop, inspect redacted details/logs and report failure |\n\n## Gotchas\n\nHyperCore perps USDC uses **8 decimals**; EVM USDC uses **6**. Preserve strings and\nbigints. Some deposits require **two distinct signatures**; resume stored steps\nrather than restarting the route. Agent expiry is at most **180 days**. Never\nask for, paste, log or import a master private key or seed. The managed agent is\ntrade-only; local limits include markets, size/notional, leverage, daily loss and\nopen orders. Privy sees an L1 action hash, so it cannot independently inspect\nnotional/action contents; Hyperliquid agent permissions and local checks enforce\nthose boundaries. State is always `~/.bloxwap`, env prefix `BLOXWAP_`.\n"; +export const skillContent="---\nname: agent-cli\ndescription: Operate the agent-cli Hyperliquid trading CLI, including managed-agent onboarding, funding, orders, event monitoring, rotation, and normalized error recovery.\nmetadata:\n version: \"0.1.0\"\n---\n\nUse `agent-cli` for the user's authorized Hyperliquid account operations. Obtain\ncommand schemas with `agent-cli schema COMMAND`; arguments can be flags or\n`--json '{...}'`, or a JSON object on stdin. Ordinary commands print exactly one\nJSON object: `{ok:true,data}` or `{ok:false,code,message,retryable,details}`.\nLogs/QR appear on stderr. `events --follow` and `logs` explicitly stream JSON lines.\nUse `--network testnet` during testing; never silently change to mainnet.\n\n## Onboarding sequence\n\n1. Configure Privy app ID and secret reference, e.g. `config set privyAppId APP`\n and `config set privyAppSecretRef env:BLOXWAP_PRIVY_APP_SECRET`. The user sets\n the environment secret outside the conversation. The default Bloxwap builder is\n `0x71b09A08257078A4d642F7Dd7315e656c837329a`, with `f=10` (1 bp = 0.01%).\n Override with `config set builder.address` / `config set builder.fee` or\n `BLOXWAP_BUILDER_ADDRESS` / `BLOXWAP_BUILDER_FEE`.\n2. Run `init --master-address 0x... --network testnet`. Show the returned approval\n payload/deeplink/QR. The user signs in their master wallet; submit only the\n resulting signature with `init --signature 0x...`. Submit builder approval\n separately with `init --builder-signature 0x...` for the configured 0.01% default. Continue only after `approved:true`.\n3. Run `fund --amount 10 --origin-chain 42161`. Show `qrAscii`/deposit address,\n origin chain/token, minimum, fees, expected received amount and expiry. On\n testnet, do not treat the default Arbitrum mainnet chain/token as a test route:\n use an explicitly supported test chain/token and `originDecimals`, or the\n credential-gated CI harness with mocked Relay and pre-funded test collateral.\n If no test route is known, stop funding and report the missing route. The user\n transfers the quoted origin token. If `signatureSteps` require signatures,\n show the exact payload and submit each via `deposit` in returned order.\n4. Poll `status` until each active deposit is `settled`, for at most 12 polls\n or 5 minutes, whichever comes first. Start with 1-second delays, double to a\n 15-second cap, and stop with an explicit pending report if the budget expires.\n Settlement requires Relay success and Hyperliquid balance receipt. Stop on\n failed/refunded deposits and explain their status. Do not place orders while\n any deposit is awaiting funds or pending.\n5. Run `run --detach`, then monitor `events --follow --since SEQ`. Persist the\n last handled sequence; replay after reconnect. An unfilled or rejected order\n remains incomplete: do not claim the requested trade succeeded. A fill updates the trading\n plan, an error follows the catalog below, and expiry warnings prompt rotation\n using external master-wallet approval.\n\n## Commands and example envelopes\n\nExamples show representative `data`, not live balances or service promises.\nFull inputs/outputs are discoverable with `schema`; amounts/prices are strings.\n\n| Input | Example output |\n| --- | --- |\n| `--help` | `{\"ok\":true,\"data\":{\"commands\":[{\"name\":\"status\",\"description\":\"Read account status\"}],\"globalFlags\":[\"--json\",\"--config\",\"--network\",\"--pretty\"]}}` |\n| `schema order` | `{\"ok\":true,\"data\":{\"input\":{\"type\":\"object\"},\"output\":{\"oneOf\":[]}}}` (actual schemas include properties) |\n| `config get network` | `{\"ok\":true,\"data\":\"testnet\"}` |\n| `config set limits.maxLeverage 5` | `{\"ok\":true,\"data\":{\"version\":1,\"limits\":{\"maxLeverage\":5}}}` |\n| `config validate` | `{\"ok\":true,\"data\":{\"valid\":true,\"version\":1}}` |\n| `init --master-address 0x...` | `{\"ok\":true,\"data\":{\"approved\":false,\"requiresSignature\":true,\"approvals\":{\"agent\":{\"typedData\":{}}},\"qrAscii\":\"...\"}}` |\n| `fund --amount 10 --no-qr` | `{\"ok\":true,\"data\":{\"depositId\":\"...\",\"depositAddress\":\"0x...\",\"expectedReceived\":\"100000000\",\"builderFeeRate\":\"0.01%\",\"builderAddress\":\"0x71b09A08257078A4d642F7Dd7315e656c837329a\",\"state\":\"awaiting_funds\",\"qrAscii\":null}}` |\n| `deposit --deposit-id ID` | `{\"ok\":true,\"data\":{\"depositId\":\"ID\",\"steps\":[{\"id\":\"step:0\",\"requiresSignature\":true,\"payload\":{}}]}}` |\n| `deposit --deposit-id ID --step-id step:0 --signature 0x...` | `{\"ok\":true,\"data\":{\"depositId\":\"ID\",\"steps\":[]}}` |\n| `status` | `{\"ok\":true,\"data\":{\"balance\":{\"accountValue\":\"100\"},\"agent\":{\"approved\":true,\"daysRemaining\":\"179\"},\"deposits\":[],\"policy\":{\"id\":\"...\",\"version\":\"1\"},\"runner\":{\"pid\":\"123\"}}}` |\n| `balance` | `{\"ok\":true,\"data\":{\"accountValue\":\"100\",\"accountValueUnits\":\"10000000000\",\"withdrawable\":\"96\",\"marginUsed\":\"4\"}}` |\n| `market --coin BTC` | `{\"ok\":true,\"data\":{\"coin\":\"BTC\",\"asset\":\"0\",\"szDecimals\":\"5\",\"maxLeverage\":\"40\",\"mid\":\"20000\"}}` |\n| `positions` | `{\"ok\":true,\"data\":[{\"coin\":\"BTC\",\"size\":\"0.001\",\"entryPrice\":\"20000\",\"leverage\":\"5\",\"marginMode\":\"cross\"}]}` |\n| `orders` | `{\"ok\":true,\"data\":[{\"coin\":\"BTC\",\"oid\":\"123\",\"sz\":\"0.001\",\"limitPx\":\"20000\",\"side\":\"B\"}]}` |\n| `order --json '{\"coin\":\"BTC\",\"side\":\"buy\",\"size\":\"0.001\",\"price\":\"20000\",\"tif\":\"Gtc\",\"cloid\":\"0x11111111111111111111111111111111\"}'` | `{\"ok\":true,\"data\":{\"result\":{\"status\":\"ok\",\"response\":{\"data\":{\"statuses\":[{\"resting\":{\"oid\":123}}]}}},\"builderFeeRate\":\"0.01%\",\"builderAddress\":\"0x71b09A08257078A4d642F7Dd7315e656c837329a\",\"cloid\":\"0x...\"}}` |\n| `order --json '{\"coin\":\"BTC\",\"side\":\"sell\",\"size\":\"0.001\",\"type\":\"market\",\"slippage\":0.01,\"reduceOnly\":true}'` | `{\"ok\":true,\"data\":{\"result\":{\"status\":\"ok\",\"response\":{\"data\":{\"statuses\":[{\"filled\":{\"oid\":123}}]}}}}}` |\n| `cancel --oid 123` or `cancel --all` | `{\"ok\":true,\"data\":{\"cancelled\":[\"123\"],\"result\":{\"status\":\"ok\"}}}` |\n| `modify --json '{\"modifications\":[{\"oid\":123,\"price\":\"20100\",\"size\":\"0.001\"}]}'` | `{\"ok\":true,\"data\":{\"status\":\"ok\"}}` |\n| `leverage --coin BTC --leverage 5 --mode isolated` | `{\"ok\":true,\"data\":{\"status\":\"ok\"}}` |\n| `run --detach` | `{\"ok\":true,\"data\":{\"pid\":123,\"startedAt\":1700000000000,\"network\":\"testnet\"}}` |\n| `events --since 12 --types '[\"fill\"]'` | `{\"ok\":true,\"data\":[{\"seq\":13,\"timestamp\":\"2026-01-01T00:00:00.000Z\",\"type\":\"fill\",\"data\":{\"tid\":1}}]}` |\n| `events --follow` | `{\"seq\":13,\"timestamp\":\"2026-01-01T00:00:00.000Z\",\"type\":\"fill\",\"data\":{\"tid\":1}}` (one line per event) |\n| `logs --follow --level warn` | `{\"level\":\"warn\",\"requestId\":\"...\",\"message\":\"Risk rejected\",\"time\":\"...\"}` (JSON lines) |\n| `rotate` then `rotate --signature 0x...` | `{\"ok\":true,\"data\":{\"oldAgent\":\"0x...\",\"newAgent\":\"0x...\",\"swapped\":true,\"oldRevoked\":false,\"revokeApproval\":{}}}` |\n| `rotate --revoke-signature 0x...` | `{\"ok\":true,\"data\":{\"swapped\":true,\"oldRevoked\":true}}` |\n| `revoke --cancel-orders` | `{\"ok\":true,\"data\":{\"locallyRevoked\":true,\"exchangeRevoked\":false,\"requiresSignature\":true,\"approval\":{}}}` |\n| `revoke --signature 0x...` | `{\"ok\":true,\"data\":{\"locallyRevoked\":true,\"exchangeRevoked\":true}}` |\n| `audit --intent ID` | `{\"ok\":true,\"data\":[{\"intentId\":\"ID\",\"stage\":\"decision\",\"data\":{\"allowed\":true},\"previousHash\":\"...\",\"hash\":\"...\"}]}` |\n| `skill install --target codex` | `{\"ok\":true,\"data\":{\"target\":\"codex\",\"path\":\".../agent-cli/SKILL.md\",\"version\":\"0.1.0\"}}` |\n\nBefore an order, query `market --coin BTC` for `mid`, `szDecimals`, asset and\nmarket leverage ceiling. Compute size/notional with decimal strings/bigints,\nusing the user-approved limit or the bounded IOC execution price. A valid schema\nalone does not establish that an order is tradable or within the budget.\n\nUse `--intent-id ID --intent \"reason\"` on trading actions to connect the user's\nreasoning to the audit. Keep cloid stable when retrying a known order intent.\nFor TP/SL use `type:\"tp\"|\"sl\"` and `triggerPrice` with size/price/reduceOnly;\nmarket orders use an explicit slippage-bounded IOC, not an unbounded market order.\n\n## Error recovery\n\n| Code | Action |\n| --- | --- |\n| RATE_LIMIT, NETWORK_ERROR | At most 3 read retries with exponential backoff; for uncertain mutations, reconcile order state/cloid before retrying. Stop and report unresolved results |\n| NONCE_COLLISION | Retry using a newly issued nonce; inspect a stale lock if persistent |\n| DEPOSIT_PENDING | Poll status until settled; stop on terminal funding failure |\n| RUNNER_NOT_RUNNING | Start runner or use configured direct fallback |\n| INSUFFICIENT_MARGIN | Reduce exposure or request authorized funding |\n| POLICY_REJECTED | Stop and revise intent within limits; do not bypass policy |\n| AGENT_EXPIRED | Rotate with master-wallet approval |\n| AGENT_NOT_APPROVED | Complete init/approval or explain revocation |\n| INVALID_SIZE, INVALID_PRICE | Correct input using market precision |\n| UNKNOWN_MARKET | Refresh metadata; use an allowed market |\n| ORDER_NOT_FOUND | Refresh orders before changing intent |\n| CONFIG_INVALID | Correct configuration/input; never insert secrets |\n| UPSTREAM_ERROR, INTERNAL | Stop, inspect redacted details/logs and report failure |\n\n## Gotchas\n\nHyperCore perps USDC uses **8 decimals**; EVM USDC uses **6**. Preserve strings and\nbigints. Some deposits require **two distinct signatures**; resume stored steps\nrather than restarting the route. Agent expiry is at most **180 days**. Never\nask for, paste, log or import a master private key or seed. The managed agent is\ntrade-only; local limits include markets, size/notional, leverage, daily loss and\nopen orders. Privy sees an L1 action hash, so it cannot independently inspect\nnotional/action contents; Hyperliquid agent permissions and local checks enforce\nthose boundaries. State is always `~/.bloxwap`, env prefix `BLOXWAP_`.\n"; diff --git a/test/config.test.ts b/test/config.test.ts index 1a08265..34833a8 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -1,8 +1,10 @@ +import '../src/commands/config.js'; +import { dispatch } from '../src/core/router.js'; import { mkdtemp, stat, writeFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { expect, test } from 'vitest'; -import { loadConfig, saveConfig } from '../src/core/config.js'; +import { loadConfig, saveConfig, migrate } from '../src/core/config.js'; test('atomic private store, migration and corrupt input', async () => { const dir = await mkdtemp(join(tmpdir(), 'bloxwap-')); const path = join(dir, 'state', 'config.json'); @@ -21,3 +23,42 @@ test('atomic private store, migration and corrupt input', async () => { await rm(dir, { recursive: true, force: true }); } }); + +test('builder defaults, persisted overrides and validated environment precedence', async () => { + const dir = await mkdtemp(join(tmpdir(), 'bloxwap-builder-')); + const path = join(dir, 'config.json'); + const address = '0x' + 'b'.repeat(40); + try { + expect((await loadConfig(path, {})).builder).toEqual({ + address: '0x71b09A08257078A4d642F7Dd7315e656c837329a', + fee: 10, + }); + expect(migrate({ builder: {} }).builder.fee).toBe(10); + for (const [key, value] of [ + ['builder.address', address], + ['builder.fee', '0'], + ]) { + expect( + (await dispatch(['config', 'set', key, value, '--config', path])).body, + ).toMatchObject({ ok: true }); + } + expect((await loadConfig(path, {})).builder).toEqual({ address, fee: 0 }); + expect( + ( + await loadConfig(path, { + BLOXWAP_BUILDER_ADDRESS: '0x' + 'c'.repeat(40), + BLOXWAP_BUILDER_FEE: '20', + }) + ).builder, + ).toEqual({ address: '0x' + 'c'.repeat(40), fee: 20 }); + for (const fee of ['bad', '', '-1', '101', '1.5']) + await expect( + loadConfig(path, { BLOXWAP_BUILDER_FEE: fee }), + ).rejects.toMatchObject({ code: 'CONFIG_INVALID' }); + await expect( + loadConfig(path, { BLOXWAP_BUILDER_ADDRESS: 'bad' }), + ).rejects.toMatchObject({ code: 'CONFIG_INVALID' }); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/test/fund.test.ts b/test/fund.test.ts index 231e0a4..8afb1d9 100644 --- a/test/fund.test.ts +++ b/test/fund.test.ts @@ -19,6 +19,10 @@ test('fund persists and reuses unexpired quotes', async () => { }; const input = fundSchema.parse({ amount: '10', qr: false }); const first = await fund(config, input, dependencies); + expect(first).toMatchObject({ + builderFeeRate: '0.01%', + builderAddress: '0x71b09A08257078A4d642F7Dd7315e656c837329a', + }); expect(first.expectedReceived).toBe('100000000'); expect((await fund(config, input, dependencies)).depositAddress).toBe( first.depositAddress, diff --git a/test/lifecycle.test.ts b/test/lifecycle.test.ts index dd64e26..4fea959 100644 --- a/test/lifecycle.test.ts +++ b/test/lifecycle.test.ts @@ -12,6 +12,7 @@ const environment = vi.hoisted(() => ({ limitPx: string; }[], agents: [] as { address: string; validUntil: number }[], + builderFee: 0, balance: '0', })); vi.mock('node:os', async (importOriginal) => ({ @@ -38,6 +39,8 @@ vi.mock('../src/wallet/privy.js', async (importOriginal) => { path?: string, ) => { const { saveConfig } = await import('../src/core/config.js'); + if (config.agentAddress && config.privyWalletId) + return { id: config.privyWalletId, address: config.agentAddress }; config.agentAddress = '0x' + String(++environment.wallets).repeat(40); config.privyWalletId = 'wallet-' + environment.wallets; config.authorizationKeyRef = 'env:BLOXWAP_E2E_AUTH'; @@ -90,6 +93,7 @@ beforeEach(async () => { environment.orders = []; environment.agents = []; environment.balance = '0'; + environment.builderFee = 0; await saveConfig( migrate({ privyAppId: 'mock', masterAddress: '0x' + 'a'.repeat(40) }), path, @@ -130,7 +134,22 @@ beforeEach(async () => { }); return Response.json({ status: 'ok', response: { type: 'default' } }); } + if (action?.type === 'approveBuilderFee') { + expect(action).toMatchObject({ + builder: '0x71b09A08257078A4d642F7Dd7315e656c837329a', + maxFeeRate: '0.01%', + }); + environment.builderFee = 10; + return Response.json({ status: 'ok', response: { type: 'default' } }); + } + if (body.type === 'maxBuilderFee') + return Response.json(environment.builderFee); if (action) { + if (action.type === 'order') + expect(action.builder).toEqual({ + b: '0x71b09a08257078a4d642f7dd7315e656c837329a', + f: 10, + }); const statuses = action.type === 'order' ? [{ resting: { oid: 1 } }] : ['success']; if (action.type === 'order') @@ -190,10 +209,25 @@ async function command(name: string, input: Record = {}) { test('mocked full account lifecycle through real CLI router, SDK and runner RPC', async () => { const initialized = await command('init', { qr: false }); expect(initialized.requiresSignature).toBe(true); - await command('init', { - qr: false, - signature: '0x' + '11'.repeat(64) + '1b', + expect(initialized.approvals).toMatchObject({ + builder: { typedData: { message: { maxFeeRate: '0.01%' } } }, }); + expect( + ( + await command('init', { + qr: false, + signature: '0x' + '11'.repeat(64) + '1b', + }) + ).approved, + ).toBe(false); + expect( + ( + await command('init', { + qr: false, + builderSignature: '0x' + '11'.repeat(64) + '1b', + }) + ).approved, + ).toBe(true); const deposit = await command('fund', { qr: false, amount: '10' }); expect(deposit.depositId).toBe('deposit-1'); environment.balance = '10'; @@ -208,13 +242,17 @@ test('mocked full account lifecycle through real CLI router, SDK and runner RPC' expect(await rpcCall('balance', {}, socketPath())).toMatchObject({ accountValue: '10', }); - await command('order', { + const placed = await command('order', { coin: 'BTC', side: 'buy', size: '0.001', price: '20000', intentId: 'lifecycle', }); + expect(placed).toMatchObject({ + builderFeeRate: '0.01%', + builderAddress: '0x71b09A08257078A4d642F7Dd7315e656c837329a', + }); await command('modify', { oid: 1, price: '20100' }); await command('cancel', { all: true }); const rotation = await command('rotate');