diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b09c30e..7a9c03f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,6 +79,14 @@ jobs: working-directory: agent-002-governance-analyst run: npx tsc --noEmit + - name: Install agent-003-market-monitor/ dependencies + working-directory: agent-003-market-monitor + run: npm ci + + - name: Typecheck agent-003-market-monitor/ + working-directory: agent-003-market-monitor + run: npx tsc --noEmit + # ── Spec verification: schemas, reference impls, datasets ─────────── verify-specs: name: Verify Specs diff --git a/agent-003-market-monitor/.env.example b/agent-003-market-monitor/.env.example new file mode 100644 index 0000000..f7da725 --- /dev/null +++ b/agent-003-market-monitor/.env.example @@ -0,0 +1,14 @@ +# Required: Anthropic API key for Claude-powered narrative reports +ANTHROPIC_API_KEY=sk-ant-... + +# Regen LCD endpoint (defaults to public endpoint if not set) +REGEN_LCD_URL=https://regen.api.chandrastation.com + +# Optional: Discord webhook for posting market reports and alerts +DISCORD_WEBHOOK_URL= + +# Optional: polling interval in seconds (default: 300 = 5 minutes) +POLL_INTERVAL_SECONDS=300 + +# Optional: Claude model to use (default: claude-sonnet-4-5-20250929) +ANTHROPIC_MODEL=claude-sonnet-4-5-20250929 diff --git a/agent-003-market-monitor/.gitignore b/agent-003-market-monitor/.gitignore new file mode 100644 index 0000000..467cd39 --- /dev/null +++ b/agent-003-market-monitor/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +*.db +*.db-shm +*.db-wal +.env diff --git a/agent-003-market-monitor/README.md b/agent-003-market-monitor/README.md new file mode 100644 index 0000000..f69c6dc --- /dev/null +++ b/agent-003-market-monitor/README.md @@ -0,0 +1,103 @@ +# AGENT-003: Regen Market Monitor + +**Layer 1 (fully automated, read-only, informational) agent that watches the Regen ecocredit marketplace, detects price anomalies, tracks liquidity health, and summarizes retirement demand.** + +Mirrors the AGENT-002 Governance Analyst structure: the same OODA executor, the same standalone Node.js process shape, the same SQLite-backed local state. The scope is marketplace intelligence rather than governance. + +## What it does + +| Workflow | Trigger | Output | +|----------|---------|--------| +| **WF-MM-01** Price Anomaly Detection | New sell orders (SellOrderCreated / SellOrderFilled) | z-score anomaly alerts per severity, deduped | +| **WF-MM-02** Liquidity Monitoring | Periodic (every poll cycle) | Per-class liquidity health snapshot + trend vs previous | +| **WF-MM-03** Retirement Pattern Analysis | Periodic (every poll cycle) | Retirement volume, demand index, compliance metadata | + +Each workflow is an **OODA loop** (Observe → Orient → Decide → Act) and persists both executions and domain state to SQLite. Numeric decisions (median, z-score, severity classification, health tier) are computed **deterministically**; Claude is only used for the narrative layer. + +## Architecture + +``` +Regen Ledger (LCD REST API) + ↓ observe +AGENT-003 (OODA engine) + ↓ orient (deterministic) + ↓ decide (deterministic) +Local SQLite (state) + ↓ act (narrative via Claude) +Console / Discord webhook +``` + +**No MCP dependency.** Talks directly to any Cosmos LCD endpoint. When Ledger MCP becomes available, the `LedgerClient` can be swapped behind the same interface. + +**No ElizaOS dependency.** Standalone Node.js process. The ElizaOS character for AGENT-003 lives at `agents/packages/agents/src/characters/market-monitor.ts`; this package shares the same system prompt text and the same threshold constants so downstream tooling has a single source of truth. + +## Quick start + +```bash +# 1. Install +cd agent-003-market-monitor +npm install + +# 2. Configure +cp .env.example .env +# Edit .env — at minimum set ANTHROPIC_API_KEY + +# 3. Run (single analysis pass) +npm run analyze + +# 4. Run (continuous polling) +npm start + +# 5. Run (dev mode with auto-reload) +npm run dev +``` + +## Configuration + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `ANTHROPIC_API_KEY` | Yes | — | Claude API key | +| `REGEN_LCD_URL` | No | `https://regen.api.chandrastation.com` | Cosmos LCD endpoint | +| `DISCORD_WEBHOOK_URL` | No | — | Discord webhook for posting reports | +| `POLL_INTERVAL_SECONDS` | No | `300` | Polling interval (seconds) | +| `ANTHROPIC_MODEL` | No | `claude-sonnet-4-5-20250929` | Claude model to use | + +Thresholds live in `src/config.ts` under `market.*` and mirror the character definition at `agents/packages/agents/src/characters/market-monitor.ts`. + +## How it maps to the framework specs + +| Framework Spec | Implementation | +|----------------|---------------| +| Phase 2.2 WF-MM-01 | `src/workflows/price-anomaly-detection.ts` | +| Phase 2.2 WF-MM-02 | `src/workflows/liquidity-monitor.ts` | +| Phase 2.2 WF-MM-03 | `src/workflows/retirement-tracking.ts` | +| Phase 2.4 OODA executor | `src/ooda.ts` | +| Phase 2.4 Agent character | `agents/packages/agents/src/characters/market-monitor.ts` | +| Phase 2.5 Workflow executions table | `src/store.ts` (SQLite) | +| Phase 3.2 Ledger MCP client | `src/ledger.ts` (direct LCD) | + +## Design decisions + +1. **Deterministic numbers, narrative-only LLM calls.** Anomaly classification, liquidity health scoring, and demand index computation are all deterministic. Claude is only invoked to write the narrative report from those numbers. This keeps the agent cheap, reproducible, and auditable. + +2. **Sell-order-as-trade proxy (MVP).** Until Ledger MCP exposes filled-trade events, the agent treats open sell orders as trade observations for the z-score baseline. An unusually high or low ask price is still a market structure signal worth surfacing, and the code path swaps cleanly when real trade events become available. + +3. **Batch supply delta as retirement source (MVP).** The MVP reads `retired_amount` from each batch supply and aggregates per class. A follow-up PR will plug in a real tx-stream client for `MsgRetire` once the LCD event endpoint is available. + +4. **Batch fan-out capped at 100.** WF-MM-03 caps the per-cycle batch fan-out to the most recent 100 batches so the agent doesn't hammer the LCD on a mainnet with thousands of historical batches. New retirement activity lands in new batches and will be picked up next cycle. + +5. **Dedupe by trade + severity.** WF-MM-01 only alerts once per `(trade_id, severity)` tuple. A trade that later escalates from WARNING to CRITICAL still fires a new alert; a trade that stays at the same severity does not. + +6. **Standalone over ElizaOS.** ElizaOS plugin API may change. A standalone process proves the workflow logic works independently of any runtime framework, matching AGENT-002's approach. + +## Governance layer + +This agent operates at **Layer 1 only**: + +- Read-only access to on-chain state +- Cannot submit proposals +- Cannot create, modify, or cancel sell orders +- Cannot execute transactions +- Informational output only + +This matches the framework's principle of starting with the lowest-risk, highest-value capability. Raising the automation layer is a separate governance decision. diff --git a/agent-003-market-monitor/package-lock.json b/agent-003-market-monitor/package-lock.json new file mode 100644 index 0000000..fda4fd4 --- /dev/null +++ b/agent-003-market-monitor/package-lock.json @@ -0,0 +1,2860 @@ +{ + "name": "@regen/agent-003-market-monitor", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@regen/agent-003-market-monitor", + "version": "0.1.0", + "dependencies": { + "@anthropic-ai/sdk": "^0.39.0", + "better-sqlite3": "^11.7.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.12", + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0", + "vitest": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.39.0.tgz", + "integrity": "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg==", + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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, + "license": "MIT", + "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/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, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.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, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "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==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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, + "license": "MIT", + "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/estree-walker": { + "version": "3.0.3", + "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" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "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, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/agent-003-market-monitor/package.json b/agent-003-market-monitor/package.json new file mode 100644 index 0000000..6f5a4af --- /dev/null +++ b/agent-003-market-monitor/package.json @@ -0,0 +1,30 @@ +{ + "name": "@regen/agent-003-market-monitor", + "version": "0.1.0", + "description": "Regen Network Market Monitor Agent — Layer 1 read-only price anomaly detection, liquidity monitoring, and retirement tracking", + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node --import tsx src/index.ts", + "dev": "node --import tsx --watch src/index.ts", + "analyze": "node --import tsx src/index.ts --once", + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@anthropic-ai/sdk": "^0.39.0", + "better-sqlite3": "^11.7.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.12", + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0", + "vitest": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/agent-003-market-monitor/src/config.ts b/agent-003-market-monitor/src/config.ts new file mode 100644 index 0000000..b65c526 --- /dev/null +++ b/agent-003-market-monitor/src/config.ts @@ -0,0 +1,51 @@ +export const config = { + // Regen LCD endpoint + lcdUrl: process.env.REGEN_LCD_URL || "https://regen.api.chandrastation.com", + + // Anthropic + anthropicApiKey: process.env.ANTHROPIC_API_KEY || "", + model: process.env.ANTHROPIC_MODEL || "claude-sonnet-4-5-20250929", + + // Discord webhook (optional) + discordWebhookUrl: process.env.DISCORD_WEBHOOK_URL || "", + + // Polling — market monitor runs faster than governance analyst + // because trade/retirement events move quickly. + pollIntervalMs: (parseInt(process.env.POLL_INTERVAL_SECONDS || "300", 10)) * 1000, + + // Market monitor thresholds. These mirror the character thresholds in + // agents/packages/agents/src/characters/market-monitor.ts so downstream + // tooling has a single source of truth. Keep them in sync if either + // file changes. + market: { + /** z-score at or above which anomaly severity becomes CRITICAL */ + criticalZScore: 3.5, + /** z-score at which anomaly is flagged for watchlist (WARNING) */ + warningZScore: 2.0, + /** minimum sample size before computing a z-score */ + minSamples: 5, + /** trailing window for class median (days) */ + classMedianWindowDays: 30, + /** trailing window for batch median (days) */ + batchMedianWindowDays: 30, + /** liquidity depth threshold (USD) below which health is DEGRADED */ + liquidityDepthFloor: 5_000, + /** large trade volume (USD) that triggers an off-cycle liquidity report */ + largeTradeVolumeUsd: 10_000, + /** retirement tx window (hours) for demand signal extraction */ + retirementWindowHours: 168, // 7 days + }, + + // Agent identity + agentId: "AGENT-003", + agentName: "RegenMarketMonitor", + governanceLayer: 1 as const, +} as const; + +export function validateConfig(): void { + if (!config.anthropicApiKey) { + throw new Error( + "ANTHROPIC_API_KEY is required. Copy .env.example to .env and set it." + ); + } +} diff --git a/agent-003-market-monitor/src/index.ts b/agent-003-market-monitor/src/index.ts new file mode 100644 index 0000000..213fe1a --- /dev/null +++ b/agent-003-market-monitor/src/index.ts @@ -0,0 +1,102 @@ +#!/usr/bin/env node +import { config, validateConfig } from "./config.js"; +import { LedgerClient } from "./ledger.js"; +import { executeOODA } from "./ooda.js"; +import { store } from "./store.js"; +import { createPriceAnomalyDetectionWorkflow } from "./workflows/price-anomaly-detection.js"; +import { createLiquidityMonitorWorkflow } from "./workflows/liquidity-monitor.js"; +import { createRetirementTrackingWorkflow } from "./workflows/retirement-tracking.js"; + +// ── Banner ──────────────────────────────────────────────────── + +function banner() { + console.log(` + ╔══════════════════════════════════════════════════════════════╗ + ║ REGEN MARKET MONITOR (AGENT-003) ║ + ║ ║ + ║ Layer 1 — Fully Automated, Informational Only ║ + ║ Workflows: WF-MM-01, WF-MM-02, WF-MM-03 ║ + ║ ║ + ║ Regen Agentic Tokenomics Framework ║ + ╚══════════════════════════════════════════════════════════════╝ +`); +} + +// ── Main loop ───────────────────────────────────────────────── + +async function runCycle(ledger: LedgerClient): Promise { + const ts = new Date().toISOString(); + console.log(`\n[${ts}] ═══ Starting market monitor cycle ═══\n`); + + // WF-MM-01: Detect price anomalies in open sell orders + const wf01 = createPriceAnomalyDetectionWorkflow(ledger); + await executeOODA(wf01); + + // WF-MM-02: Snapshot liquidity health per credit class + const wf02 = createLiquidityMonitorWorkflow(ledger); + await executeOODA(wf02); + + // WF-MM-03: Summarize retirement activity + demand signal + const wf03 = createRetirementTrackingWorkflow(ledger); + await executeOODA(wf03); + + const execCount = store.getExecutionCount(); + console.log( + `[${new Date().toISOString()}] ═══ Cycle complete (${execCount} total executions logged) ═══\n` + ); +} + +async function main() { + banner(); + validateConfig(); + + const runOnce = process.argv.includes("--once"); + const ledger = new LedgerClient(); + + console.log(`Configuration:`); + console.log(` LCD endpoint: ${config.lcdUrl}`); + console.log(` LLM model: ${config.model}`); + console.log(` Discord: ${config.discordWebhookUrl ? "configured" : "not configured"}`); + console.log(` Mode: ${runOnce ? "single run" : `polling every ${config.pollIntervalMs / 1000}s`}`); + console.log(); + + try { + const { blockHeight } = await ledger.checkConnection(); + console.log(`Connected to Regen Ledger at block ${blockHeight}\n`); + } catch (err) { + console.error( + `Failed to connect to Regen Ledger at ${config.lcdUrl}:`, + err + ); + process.exit(1); + } + + if (runOnce) { + await runCycle(ledger); + } else { + await runCycle(ledger); + + const interval = setInterval(() => { + runCycle(ledger).catch((err) => + console.error(`Cycle failed:`, err) + ); + }, config.pollIntervalMs); + + const shutdown = () => { + console.log("\nShutting down gracefully..."); + clearInterval(interval); + store.close(); + process.exit(0); + }; + + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); + + console.log("Agent running. Press Ctrl+C to stop.\n"); + } +} + +main().catch((err) => { + console.error("Fatal error:", err); + process.exit(1); +}); diff --git a/agent-003-market-monitor/src/ledger.ts b/agent-003-market-monitor/src/ledger.ts new file mode 100644 index 0000000..a6fb729 --- /dev/null +++ b/agent-003-market-monitor/src/ledger.ts @@ -0,0 +1,157 @@ +import { config } from "./config.js"; +import type { + CreditClass, + CreditBatch, + BatchSupply, + SellOrder, +} from "./types.js"; + +/** + * Regen Ledger LCD (REST) client — ecocredit marketplace endpoints. + * + * Talks directly to a Cosmos LCD endpoint — no MCP dependency. + * When Ledger MCP becomes available, this can be swapped out behind + * the same interface. Matches the pattern used by AGENT-001 + * (registry-reviewer) and AGENT-002 (governance-analyst). + */ +export class LedgerClient { + private baseUrl: string; + + constructor(baseUrl?: string) { + this.baseUrl = (baseUrl || config.lcdUrl).replace(/\/$/, ""); + } + + // ── Credit Classes ───────────────────────────────────────── + + async getCreditClasses(): Promise { + const params = new URLSearchParams(); + params.set("pagination.limit", "200"); + + const data = await this.get( + `/regen/ecocredit/v1/classes?${params.toString()}` + ); + return (data.classes || []) as CreditClass[]; + } + + async getCreditClass(classId: string): Promise { + try { + const data = await this.get( + `/regen/ecocredit/v1/classes/${classId}` + ); + return (data.class || null) as CreditClass | null; + } catch (err) { + console.error(`LedgerClient.getCreditClass(${classId}) failed:`, err); + return null; + } + } + + // ── Credit Batches ───────────────────────────────────────── + + async getCreditBatches(): Promise { + const params = new URLSearchParams(); + params.set("pagination.limit", "200"); + params.set("pagination.reverse", "true"); + + const data = await this.get( + `/regen/ecocredit/v1/batches?${params.toString()}` + ); + return (data.batches || []) as CreditBatch[]; + } + + async getCreditBatch(denom: string): Promise { + try { + const data = await this.get( + `/regen/ecocredit/v1/batches/${denom}` + ); + return (data.batch || null) as CreditBatch | null; + } catch (err) { + console.error(`LedgerClient.getCreditBatch(${denom}) failed:`, err); + return null; + } + } + + async getBatchSupply(denom: string): Promise { + try { + const data = await this.get( + `/regen/ecocredit/v1/batches/${denom}/supply` + ); + return (data.supply || null) as BatchSupply | null; + } catch (err) { + console.error(`LedgerClient.getBatchSupply(${denom}) failed:`, err); + return null; + } + } + + // ── Marketplace: Sell Orders ─────────────────────────────── + + async getSellOrders(limit = 200): Promise { + try { + const params = new URLSearchParams(); + params.set("pagination.limit", String(limit)); + + const data = await this.get( + `/regen/ecocredit/marketplace/v1/sell-orders?${params.toString()}` + ); + return (data.sell_orders || []) as SellOrder[]; + } catch (err) { + console.error(`LedgerClient.getSellOrders(limit=${limit}) failed:`, err); + return []; + } + } + + async getSellOrdersByBatch(denom: string): Promise { + try { + const params = new URLSearchParams(); + params.set("pagination.limit", "200"); + + const data = await this.get( + `/regen/ecocredit/marketplace/v1/sell-orders/batch/${denom}?${params.toString()}` + ); + return (data.sell_orders || []) as SellOrder[]; + } catch (err) { + console.error( + `LedgerClient.getSellOrdersByBatch(${denom}) failed:`, + err + ); + return []; + } + } + + async getSellOrder(orderId: string): Promise { + try { + const data = await this.get( + `/regen/ecocredit/marketplace/v1/sell-orders/${orderId}` + ); + return (data.sell_order || null) as SellOrder | null; + } catch (err) { + console.error(`LedgerClient.getSellOrder(${orderId}) failed:`, err); + return null; + } + } + + // ── Connectivity check ──────────────────────────────────── + + async checkConnection(): Promise<{ blockHeight: string }> { + const data = await this.get(`/cosmos/base/tendermint/v1beta1/blocks/latest`); + const block = data.block as Record | undefined; + const header = block?.header as Record | undefined; + const height = (header?.height as string) || "unknown"; + return { blockHeight: height }; + } + + // ── HTTP ──────────────────────────────────────────────────── + + private async get(path: string): Promise> { + const url = `${this.baseUrl}${path}`; + const res = await fetch(url, { + headers: { Accept: "application/json" }, + signal: AbortSignal.timeout(15_000), + }); + + if (!res.ok) { + throw new Error(`LCD ${res.status}: ${res.statusText} — ${url}`); + } + + return (await res.json()) as Record; + } +} diff --git a/agent-003-market-monitor/src/monitor.ts b/agent-003-market-monitor/src/monitor.ts new file mode 100644 index 0000000..e91d2b2 --- /dev/null +++ b/agent-003-market-monitor/src/monitor.ts @@ -0,0 +1,205 @@ +import Anthropic from "@anthropic-ai/sdk"; +import { config } from "./config.js"; +import type { + PriceAnomaly, + LiquiditySnapshot, + RetirementSummary, +} from "./types.js"; + +const client = new Anthropic({ apiKey: config.anthropicApiKey }); + +/** + * System prompt mirrors the AGENT-003 character definition at + * agents/packages/agents/src/characters/market-monitor.ts. Thresholds + * are injected from config so a single source of truth drives both the + * deterministic pipeline AND the narrative layer. + */ +const SYSTEM_PROMPT = `You are the Regen Market Monitor Agent (AGENT-003). + +Your responsibilities: +1. Monitoring ecological credit prices across all credit classes +2. Tracking marketplace liquidity and order book health +3. Analyzing retirement patterns and demand signals +4. Detecting price anomalies and potential manipulation + +Core Principles: +- Prioritize market integrity above all else +- Minimize false positives — verify anomalies against multiple data sources +- Present data with precision — include units, timeframes, and confidence intervals +- Never provide trading advice or price predictions +- Cite the deterministic numbers passed to you; do not invent values + +Workflows: +- WF-MM-01 (Price Impact Alert): z-score analysis (CRITICAL >= ${config.market.criticalZScore}, WARNING >= ${config.market.warningZScore}) +- WF-MM-02 (Liquidity Monitor): hourly liquidity health checks +- WF-MM-03 (Retirement Tracking): demand signals and impact quantification + +Alert Severity Levels: +- INFO: Normal activity, logged for trend analysis +- WARNING: z-score between ${config.market.warningZScore} and ${config.market.criticalZScore}, added to watchlist +- CRITICAL: z-score >= ${config.market.criticalZScore}, escalate for investigation + +Output Format: +- Use markdown with explicit tables +- Include severity level in the title of every alert +- Include units, timeframes, and sample sizes +- Quantify confidence when relevant`; + +// ============================================================ +// WF-MM-01: Price anomaly narrative +// ============================================================ + +export async function describePriceAnomaly( + anomaly: PriceAnomaly +): Promise { + const prompt = `Generate a Price Impact Alert report for this anomaly detection. + +## Deterministic Pipeline Output +- Trade ID: ${anomaly.tradeId} +- Credit Class: ${anomaly.classId} +- Batch: ${anomaly.batchDenom} +- Seller: ${anomaly.seller} +- Quantity: ${anomaly.quantity} +- Trade Price: $${anomaly.pricePerCredit.toFixed(4)}/credit +- Class Median (${config.market.classMedianWindowDays}d): $${anomaly.classMedian.toFixed(4)}/credit (n=${anomaly.sampleSizeClass}) +- Batch Median (${config.market.batchMedianWindowDays}d): $${anomaly.batchMedian.toFixed(4)}/credit (n=${anomaly.sampleSizeBatch}) +- Z-score vs class: ${anomaly.zScoreVsClass.toFixed(2)} +- Z-score vs batch: ${anomaly.zScoreVsBatch.toFixed(2)} +- Severity: ${anomaly.severity} +- Confidence: ${anomaly.confidence.toFixed(2)} +- Detected: ${anomaly.detectedAt} + +Generate a structured Markdown report with: +1. A header "Price Impact Alert" followed by severity, class, batch, timestamp +2. An "Anomaly Details" table using ONLY the numbers above +3. A "Context" section (bullet points) — note the relative deviation, sample size adequacy, and any concerns about class vs batch z-score divergence +4. An "Action" section — what the agent has done (watchlist add / escalation) and what the next review is + +Do not invent any numbers beyond what's provided. Do not recommend trading positions.`; + + const response = await client.messages.create({ + model: config.model, + max_tokens: 1000, + system: SYSTEM_PROMPT, + messages: [{ role: "user", content: prompt }], + }); + + return extractText(response); +} + +// ============================================================ +// WF-MM-02: Liquidity narrative +// ============================================================ + +export async function describeLiquidity( + snapshot: LiquiditySnapshot, + previousSnapshot: LiquiditySnapshot | null +): Promise { + const prev = previousSnapshot + ? ` +## Previous Snapshot (${previousSnapshot.capturedAt}) +- Listed value: $${previousSnapshot.totalListedValueUsd.toFixed(2)} +- Median ask: $${previousSnapshot.medianAskUsd.toFixed(4)} +- Depth (top-10): $${previousSnapshot.depthUsd.toFixed(2)} +- Health: ${previousSnapshot.health} (${previousSnapshot.healthScore.toFixed(2)}) +` + : "## Previous Snapshot\nNone — this is the first snapshot for this class."; + + const prompt = `Generate a Liquidity Report for the Regen ecocredit marketplace. + +## Deterministic Pipeline Output +- Class: ${snapshot.classId} +- Sell orders: ${snapshot.sellOrderCount} +- Listed quantity: ${snapshot.totalListedQuantity} +- Listed value: $${snapshot.totalListedValueUsd.toFixed(2)} +- Lowest ask: $${snapshot.lowestAskUsd.toFixed(4)} +- Highest ask: $${snapshot.highestAskUsd.toFixed(4)} +- Median ask: $${snapshot.medianAskUsd.toFixed(4)} +- Mean ask: $${snapshot.meanAskUsd.toFixed(4)} +- Depth (top-10 sum): $${snapshot.depthUsd.toFixed(2)} +- Health score: ${snapshot.healthScore.toFixed(2)} +- Health: ${snapshot.health} +- Captured: ${snapshot.capturedAt} + +${prev} + +Generate a structured Markdown report with: +1. Header "Liquidity Report — ${snapshot.classId}" with health badge +2. A "Current Snapshot" table with the numbers above +3. A "Trend" section comparing to the previous snapshot (or noting this is the first) +4. A "Health Assessment" section — interpret the depth floor vs the configured liquidity_depth_floor +5. A one-line "Alert" line if health is DEGRADED or CRITICAL + +Use ONLY the numbers provided. Do not invent trade volumes, slippage estimates, or predictions.`; + + const response = await client.messages.create({ + model: config.model, + max_tokens: 1000, + system: SYSTEM_PROMPT, + messages: [{ role: "user", content: prompt }], + }); + + return extractText(response); +} + +// ============================================================ +// WF-MM-03: Retirement narrative +// ============================================================ + +export async function describeRetirementSummary( + summary: RetirementSummary, + baselineDemand: number +): Promise { + const trend = + baselineDemand === 0 + ? "no prior baseline" + : summary.demandIndex > baselineDemand + ? `up from ${baselineDemand.toFixed(0)}` + : summary.demandIndex < baselineDemand + ? `down from ${baselineDemand.toFixed(0)}` + : `flat vs ${baselineDemand.toFixed(0)}`; + + const prompt = `Generate a Retirement Pattern Summary for the Regen ecocredit marketplace. + +## Deterministic Pipeline Output +- Class: ${summary.classId} +- Window: trailing ${summary.windowHours} hours +- Retirements (count): ${summary.retirementCount} +- Total quantity: ${summary.totalQuantity} +- Total value: $${summary.totalValueUsd.toFixed(2)} +- Unique retirees: ${summary.uniqueRetirees} +- Top retiree: ${summary.topRetiree ?? "(none)"} +- Top retiree quantity: ${summary.topRetireeQuantity} +- % with jurisdiction metadata: ${summary.pctWithJurisdiction.toFixed(1)}% +- Demand index (0-100): ${summary.demandIndex.toFixed(0)} (${trend}) +- Captured: ${summary.capturedAt} + +Generate a structured Markdown report with: +1. Header "Retirement Summary — ${summary.classId}" +2. "Volume" table with counts, quantity, value +3. "Demand Signals" section — interpret the demand index and trend +4. "Concentration" section — note if one retiree accounts for an unusual share +5. "Metadata Compliance" note — if % with jurisdiction is high, that usually implies compliance-driven demand + +Use ONLY the numbers provided. Do not extrapolate future demand.`; + + const response = await client.messages.create({ + model: config.model, + max_tokens: 1000, + system: SYSTEM_PROMPT, + messages: [{ role: "user", content: prompt }], + }); + + return extractText(response); +} + +// ============================================================ +// Helpers +// ============================================================ + +function extractText(response: Anthropic.Message): string { + return response.content + .filter((b): b is Anthropic.TextBlock => b.type === "text") + .map((b) => b.text) + .join("\n"); +} diff --git a/agent-003-market-monitor/src/ooda.ts b/agent-003-market-monitor/src/ooda.ts new file mode 100644 index 0000000..e7f3219 --- /dev/null +++ b/agent-003-market-monitor/src/ooda.ts @@ -0,0 +1,83 @@ +import { config } from "./config.js"; +import { store } from "./store.js"; +import type { OODAExecution } from "./types.js"; + +/** + * Generic OODA loop executor. + * + * Identical shape to the AGENT-002 executor — kept locally so that each + * agent process can run as a standalone Node program without a shared + * package. If the agents converge on a common runtime later, this is a + * candidate to lift into a shared package. + */ +export interface OODAWorkflow { + id: string; + name: string; + observe: () => Promise; + orient: (observations: TObserve) => Promise; + decide: (orientation: TOrient) => Promise; + act: (decision: TDecide) => Promise; +} + +export async function executeOODA( + workflow: OODAWorkflow +): Promise> { + const executionId = crypto.randomUUID(); + const execution: OODAExecution = { + executionId, + workflowId: workflow.id, + status: "running", + observations: null as unknown as TObserve, + orientation: null, + decision: null, + actions: null, + startedAt: new Date(), + completedAt: null, + error: null, + }; + + const log = (phase: string, msg: string) => + console.log( + `[${new Date().toISOString()}] [${workflow.id}] [${phase}] ${msg}` + ); + + try { + log("OBSERVE", "Gathering data..."); + execution.observations = await workflow.observe(); + + log("ORIENT", "Analyzing context..."); + execution.orientation = await workflow.orient(execution.observations); + + log("DECIDE", "Making decision..."); + execution.decision = await workflow.decide(execution.orientation); + + log("ACT", "Executing actions..."); + execution.actions = await workflow.act(execution.decision); + + execution.status = "completed"; + log("DONE", "Workflow completed successfully."); + } catch (err) { + execution.status = "failed"; + execution.error = err instanceof Error ? err.message : String(err); + log("ERROR", execution.error); + } + + execution.completedAt = new Date(); + + store.logExecution({ + executionId: execution.executionId, + workflowId: execution.workflowId, + agentId: config.agentId, + status: execution.status, + startedAt: execution.startedAt.toISOString(), + completedAt: execution.completedAt.toISOString(), + result: JSON.stringify({ + orientation: execution.orientation, + decision: execution.decision, + actions: execution.actions, + error: execution.error, + }), + }); + + return execution; +} diff --git a/agent-003-market-monitor/src/output.ts b/agent-003-market-monitor/src/output.ts new file mode 100644 index 0000000..6b3b790 --- /dev/null +++ b/agent-003-market-monitor/src/output.ts @@ -0,0 +1,59 @@ +import { config } from "./config.js"; +import type { OutputMessage } from "./types.js"; + +/** + * Output dispatcher. + * + * Sends agent outputs to configured channels: + * - Console (always) + * - Discord webhook (if configured) + * + * Extend this to add Telegram, Twitter, KOI object creation, etc. + * Mirrors the AGENT-002 dispatcher shape. + */ +export async function output(msg: OutputMessage): Promise { + logToConsole(msg); + + if (config.discordWebhookUrl) { + await postToDiscord(msg).catch((err) => + console.error(` Discord post failed: ${err}`) + ); + } +} + +function logToConsole(msg: OutputMessage): void { + const prefix = + msg.alertLevel === "CRITICAL" + ? "!!! CRITICAL" + : msg.alertLevel === "HIGH" + ? "!! HIGH" + : "--"; + + console.log(`\n${"=".repeat(72)}`); + console.log( + `${prefix} [${msg.workflow}] ${msg.subjectId}: ${msg.title}` + ); + console.log(`${"=".repeat(72)}`); + console.log(msg.content); + console.log(`${"=".repeat(72)}\n`); +} + +async function postToDiscord(msg: OutputMessage): Promise { + const maxLen = 1900; + let content = `**[${msg.workflow}]** ${msg.subjectId}: **${msg.title}**\n\n`; + const remaining = maxLen - content.length; + content += + msg.content.length > remaining + ? msg.content.slice(0, remaining - 20) + "\n\n*...truncated*" + : msg.content; + + await fetch(config.discordWebhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + username: config.agentName, + content, + }), + signal: AbortSignal.timeout(10_000), + }); +} diff --git a/agent-003-market-monitor/src/store.ts b/agent-003-market-monitor/src/store.ts new file mode 100644 index 0000000..3db460a --- /dev/null +++ b/agent-003-market-monitor/src/store.ts @@ -0,0 +1,288 @@ +import Database from "better-sqlite3"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const DB_PATH = path.join(__dirname, "..", "agent-003.db"); + +/** + * Local SQLite store for AGENT-003 state. + * + * Tracks trade observations (for trailing median + z-score), liquidity + * snapshots, retirement summaries, and workflow execution history. + * Intentionally lightweight — can be replaced with PostgreSQL later. + */ +class Store { + private db: Database.Database; + + constructor() { + this.db = new Database(DB_PATH); + this.db.pragma("journal_mode = WAL"); + this.migrate(); + } + + private migrate() { + this.db.exec(` + CREATE TABLE IF NOT EXISTS workflow_executions ( + execution_id TEXT PRIMARY KEY, + workflow_id TEXT NOT NULL, + agent_id TEXT NOT NULL, + status TEXT NOT NULL, + started_at TEXT NOT NULL, + completed_at TEXT, + result TEXT + ); + + -- Trailing trade observations, used as the sample set for + -- class/batch median + z-score in WF-MM-01. Each row is one + -- observed trade; the analyzer reads rows matching the batch or + -- class within the trailing window. + CREATE TABLE IF NOT EXISTS trade_observations ( + trade_id TEXT PRIMARY KEY, + class_id TEXT NOT NULL, + batch_denom TEXT NOT NULL, + price_per_credit REAL NOT NULL, + quantity REAL NOT NULL, + executed_at TEXT NOT NULL + ); + + -- Deduplication record for WF-MM-01 — we only alert once per + -- (trade_id, severity). Re-alerts only escalate to a higher + -- severity (enforced in the workflow). + CREATE TABLE IF NOT EXISTS anomaly_alerts ( + trade_id TEXT PRIMARY KEY, + severity TEXT NOT NULL, + z_class REAL NOT NULL, + z_batch REAL NOT NULL, + detected_at TEXT NOT NULL, + report TEXT NOT NULL + ); + + -- Snapshots of liquidity health per class, used by WF-MM-02 + -- to compare against the previous snapshot and surface trends. + CREATE TABLE IF NOT EXISTS liquidity_snapshots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + class_id TEXT NOT NULL, + snapshot TEXT NOT NULL, + health TEXT NOT NULL, + captured_at TEXT NOT NULL + ); + + -- Summaries of retirement activity per class, used by WF-MM-03 + -- to compute a rolling demand index. + CREATE TABLE IF NOT EXISTS retirement_summaries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + class_id TEXT NOT NULL, + window_hours INTEGER NOT NULL, + total_quantity REAL NOT NULL, + total_value_usd REAL NOT NULL, + retirement_count INTEGER NOT NULL, + demand_index REAL NOT NULL, + summary TEXT NOT NULL, + captured_at TEXT NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_trade_class ON trade_observations(class_id); + CREATE INDEX IF NOT EXISTS idx_trade_batch ON trade_observations(batch_denom); + CREATE INDEX IF NOT EXISTS idx_trade_executed ON trade_observations(executed_at); + CREATE INDEX IF NOT EXISTS idx_liquidity_class ON liquidity_snapshots(class_id); + CREATE INDEX IF NOT EXISTS idx_retirement_class ON retirement_summaries(class_id); + `); + } + + // ── Trade observations (for z-score baseline) ────────────── + + recordTrade(obs: { + tradeId: string; + classId: string; + batchDenom: string; + pricePerCredit: number; + quantity: number; + executedAt: string; + }): void { + this.db + .prepare( + `INSERT OR IGNORE INTO trade_observations + (trade_id, class_id, batch_denom, price_per_credit, quantity, executed_at) + VALUES (?, ?, ?, ?, ?, ?)` + ) + .run( + obs.tradeId, + obs.classId, + obs.batchDenom, + obs.pricePerCredit, + obs.quantity, + obs.executedAt + ); + } + + getTradesByClass(classId: string, windowDays: number): number[] { + const cutoff = new Date(Date.now() - windowDays * 86_400_000).toISOString(); + const rows = this.db + .prepare( + `SELECT price_per_credit as p FROM trade_observations + WHERE class_id = ? AND executed_at >= ?` + ) + .all(classId, cutoff) as { p: number }[]; + return rows.map((r) => r.p); + } + + getTradesByBatch(batchDenom: string, windowDays: number): number[] { + const cutoff = new Date(Date.now() - windowDays * 86_400_000).toISOString(); + const rows = this.db + .prepare( + `SELECT price_per_credit as p FROM trade_observations + WHERE batch_denom = ? AND executed_at >= ?` + ) + .all(batchDenom, cutoff) as { p: number }[]; + return rows.map((r) => r.p); + } + + // ── Anomaly alerts ───────────────────────────────────────── + + hasAnomalyAlert(tradeId: string): boolean { + const row = this.db + .prepare("SELECT severity FROM anomaly_alerts WHERE trade_id = ?") + .get(tradeId) as { severity: string } | undefined; + return !!row; + } + + getAnomalyAlertSeverity(tradeId: string): string | null { + const row = this.db + .prepare("SELECT severity FROM anomaly_alerts WHERE trade_id = ?") + .get(tradeId) as { severity: string } | undefined; + return row?.severity || null; + } + + saveAnomalyAlert(alert: { + tradeId: string; + severity: string; + zClass: number; + zBatch: number; + report: string; + }): void { + this.db + .prepare( + `INSERT OR REPLACE INTO anomaly_alerts + (trade_id, severity, z_class, z_batch, detected_at, report) + VALUES (?, ?, ?, ?, ?, ?)` + ) + .run( + alert.tradeId, + alert.severity, + alert.zClass, + alert.zBatch, + new Date().toISOString(), + alert.report + ); + } + + // ── Liquidity snapshots ──────────────────────────────────── + + saveLiquiditySnapshot(classId: string, snapshot: string, health: string): void { + this.db + .prepare( + `INSERT INTO liquidity_snapshots (class_id, snapshot, health, captured_at) + VALUES (?, ?, ?, ?)` + ) + .run(classId, snapshot, health, new Date().toISOString()); + } + + getLatestLiquiditySnapshot( + classId: string + ): { snapshot: string; health: string; captured_at: string } | null { + const row = this.db + .prepare( + `SELECT snapshot, health, captured_at FROM liquidity_snapshots + WHERE class_id = ? ORDER BY id DESC LIMIT 1` + ) + .get(classId) as + | { snapshot: string; health: string; captured_at: string } + | undefined; + return row || null; + } + + // ── Retirement summaries ─────────────────────────────────── + + saveRetirementSummary(row: { + classId: string; + windowHours: number; + totalQuantity: number; + totalValueUsd: number; + retirementCount: number; + demandIndex: number; + summary: string; + }): void { + this.db + .prepare( + `INSERT INTO retirement_summaries + (class_id, window_hours, total_quantity, total_value_usd, + retirement_count, demand_index, summary, captured_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)` + ) + .run( + row.classId, + row.windowHours, + row.totalQuantity, + row.totalValueUsd, + row.retirementCount, + row.demandIndex, + row.summary, + new Date().toISOString() + ); + } + + getBaselineDemand(classId: string, lookbackDays: number): number { + // Average of the demand_index values captured in the prior window. + // Used as the reference point for the new demand_index computation. + const cutoff = new Date(Date.now() - lookbackDays * 86_400_000).toISOString(); + const row = this.db + .prepare( + `SELECT AVG(demand_index) as avg FROM retirement_summaries + WHERE class_id = ? AND captured_at >= ?` + ) + .get(classId, cutoff) as { avg: number | null }; + return row.avg ?? 0; + } + + // ── Workflow executions ──────────────────────────────────── + + logExecution(exec: { + executionId: string; + workflowId: string; + agentId: string; + status: string; + startedAt: string; + completedAt: string; + result: string; + }): void { + this.db + .prepare( + `INSERT INTO workflow_executions + (execution_id, workflow_id, agent_id, status, started_at, completed_at, result) + VALUES (?, ?, ?, ?, ?, ?, ?)` + ) + .run( + exec.executionId, + exec.workflowId, + exec.agentId, + exec.status, + exec.startedAt, + exec.completedAt, + exec.result + ); + } + + getExecutionCount(): number { + const row = this.db + .prepare("SELECT COUNT(*) as cnt FROM workflow_executions") + .get() as { cnt: number }; + return row.cnt; + } + + close(): void { + this.db.close(); + } +} + +export const store = new Store(); diff --git a/agent-003-market-monitor/src/types.ts b/agent-003-market-monitor/src/types.ts new file mode 100644 index 0000000..317ca5f --- /dev/null +++ b/agent-003-market-monitor/src/types.ts @@ -0,0 +1,158 @@ +// ============================================================ +// Regen Ledger ecocredit marketplace types +// ============================================================ + +export interface CreditClass { + id: string; + admin: string; + credit_type: CreditType; + metadata: string; +} + +export interface CreditType { + abbreviation: string; + name: string; + unit: string; + precision: number; +} + +export interface CreditBatch { + denom: string; + project_id: string; + issuer: string; + start_date: string; + end_date: string; + total_amount: string; + metadata: string; + open: boolean; +} + +export interface BatchSupply { + batch_denom: string; + tradable_amount: string; + retired_amount: string; + cancelled_amount: string; +} + +export interface SellOrder { + id: string; + seller: string; + batch_denom: string; + quantity: string; + ask_denom: string; + ask_amount: string; + disable_auto_retire: boolean; + expiration: string; +} + +/** Synthesized trade record (from filled sell orders or tx logs) */ +export interface Trade { + id: string; + batchDenom: string; + classId: string; + seller: string; + buyer: string; + quantity: number; + pricePerCredit: number; // USD-equivalent + askDenom: string; + executedAt: string; +} + +/** Synthesized retirement record */ +export interface Retirement { + txHash: string; + batchDenom: string; + classId: string; + retiree: string; + quantity: number; + jurisdiction: string | null; + reason: string | null; + retiredAt: string; +} + +// ============================================================ +// OODA loop types (shared shape mirrors agent-002) +// ============================================================ + +export interface OODAExecution { + executionId: string; + workflowId: string; + status: "running" | "completed" | "failed" | "escalated"; + observations: TObserve; + orientation: TOrient | null; + decision: TDecide | null; + actions: TAct | null; + startedAt: Date; + completedAt: Date | null; + error: string | null; +} + +// ============================================================ +// Workflow-specific types +// ============================================================ + +export type AlertLevel = "NORMAL" | "HIGH" | "CRITICAL"; +export type AnomalySeverity = "INFO" | "WARNING" | "CRITICAL"; + +/** Price anomaly detection (WF-MM-01) */ +export interface PriceAnomaly { + tradeId: string; + batchDenom: string; + classId: string; + seller: string; + quantity: number; + pricePerCredit: number; + classMedian: number; + batchMedian: number; + zScoreVsClass: number; + zScoreVsBatch: number; + sampleSizeClass: number; + sampleSizeBatch: number; + severity: AnomalySeverity; + detectedAt: string; + confidence: number; +} + +/** Liquidity monitoring (WF-MM-02) */ +export interface LiquiditySnapshot { + classId: string; + totalListedQuantity: number; + totalListedValueUsd: number; + sellOrderCount: number; + lowestAskUsd: number; + highestAskUsd: number; + medianAskUsd: number; + meanAskUsd: number; + depthUsd: number; // sum of top-10 sell orders + healthScore: number; // 0-1 + health: "HEALTHY" | "DEGRADED" | "CRITICAL"; + capturedAt: string; +} + +/** Retirement tracking (WF-MM-03) */ +export interface RetirementSummary { + classId: string; + windowHours: number; + retirementCount: number; + totalQuantity: number; + totalValueUsd: number; + uniqueRetirees: number; + topRetiree: string | null; + topRetireeQuantity: number; + pctWithJurisdiction: number; + demandIndex: number; // 0-100, relative to trailing baseline + capturedAt: string; +} + +// ============================================================ +// Output types +// ============================================================ + +export interface OutputMessage { + workflow: string; + subjectId: string; // trade id / class id / batch denom + title: string; + content: string; + alertLevel: AlertLevel; + timestamp: Date; +} diff --git a/agent-003-market-monitor/src/utils.ts b/agent-003-market-monitor/src/utils.ts new file mode 100644 index 0000000..eb53ab3 --- /dev/null +++ b/agent-003-market-monitor/src/utils.ts @@ -0,0 +1,78 @@ +/** + * Shared numeric and parsing helpers used by more than one workflow. + * + * These started out duplicated across the three WF-MM workflows. + * Centralizing them here is the DRY fix asked for by the PR #99 + * Gemini review, and it gives the unit-test suite a single source of + * truth to pin so a regression in one workflow cannot silently diverge + * from the rest. + */ + +/** + * Deterministic statistical median. Returns 0 for empty input so + * callers do not need a special-case branch upstream — an empty + * sample set unambiguously means "no signal" in every callsite. + */ +export function median(values: number[]): number { + if (values.length === 0) return 0; + const sorted = [...values].sort((a, b) => a - b); + const mid = Math.floor(sorted.length / 2); + return sorted.length % 2 === 0 + ? (sorted[mid - 1]! + sorted[mid]!) / 2 + : sorted[mid]!; +} + +/** + * Sample standard deviation (n-1 denominator). Returns 0 for + * sample sizes below 2 because a single observation carries no + * dispersion information. + */ +export function stddev(values: number[], mean: number): number { + if (values.length <= 1) return 0; + const sumSq = values.reduce((acc, v) => acc + (v - mean) ** 2, 0); + return Math.sqrt(sumSq / (values.length - 1)); +} + +/** + * Extract the credit class identifier from a Regen batch denom. + * + * Regen batch denoms follow the shape + * `----` + * (for example, `C01-001-20240101-20241231-001`). The class id is + * always the leading token before the first dash. Strings without a + * dash are returned unchanged. + */ +export function classIdFromBatchDenom(denom: string): string { + const idx = denom.indexOf("-"); + return idx > 0 ? denom.slice(0, idx) : denom; +} + +/** + * Whether a Cosmos denom corresponds to a USD-pegged stablecoin that + * this MVP is willing to treat as 1:1 USD for pricing. Used to + * filter out non-USD sell orders so they do not pollute the baseline + * with arbitrary 1:1-valued noise. + */ +export function isUsdStableDenom(denom: string): boolean { + const d = denom.toLowerCase(); + return d.includes("usdc") || d.includes("usdt") || d.includes("dai"); +} + +/** + * Demand index on a bounded 0-100 scale. Inputs are rolling and a + * class with no trailing activity gets 0. The index is intentionally + * simple — it exists so the narrative layer has a single number to + * anchor the "demand up / demand down" story. Defined here so the + * unit-test suite can import it without pulling in the workflow's + * Store singleton (which opens SQLite on load). + */ +export function computeDemandIndex( + totalQuantity: number, + retirementCount: number, + uniqueRetirees: number +): number { + const volumeComponent = Math.min(60, Math.log10(Math.max(1, totalQuantity)) * 20); + const countComponent = Math.min(20, retirementCount * 2); + const breadthComponent = Math.min(20, uniqueRetirees * 4); + return Math.round(volumeComponent + countComponent + breadthComponent); +} diff --git a/agent-003-market-monitor/src/workflows/liquidity-monitor.test.ts b/agent-003-market-monitor/src/workflows/liquidity-monitor.test.ts new file mode 100644 index 0000000..2e8e705 --- /dev/null +++ b/agent-003-market-monitor/src/workflows/liquidity-monitor.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect } from "vitest"; +import { askUsd, scoreHealth } from "./liquidity-monitor.js"; +import type { SellOrder } from "../types.js"; +import { config } from "../config.js"; + +// Small helper to build a well-typed SellOrder for a test case. +function makeOrder(overrides: Partial = {}): SellOrder { + return { + id: "order-test", + seller: "regen1seller", + batch_denom: "C-001-20240101-20241231-001", + quantity: "100", + ask_denom: "uusdc", + ask_amount: "5000", + disable_auto_retire: false, + expiration: "2030-01-01T00:00:00Z", + ...overrides, + }; +} + +// ============================================================ +// askUsd — per-unit ask price in USD +// ============================================================ + +describe("askUsd", () => { + it("divides ask_amount by quantity for a normal order", () => { + const order = makeOrder({ ask_amount: "5000", quantity: "100" }); + expect(askUsd(order)).toBe(50); + }); + + it("returns 0 when quantity is zero", () => { + expect(askUsd(makeOrder({ quantity: "0" }))).toBe(0); + }); + + it("returns 0 when quantity is negative", () => { + expect(askUsd(makeOrder({ quantity: "-10" }))).toBe(0); + }); + + it("returns 0 when ask_amount is not a finite number", () => { + expect(askUsd(makeOrder({ ask_amount: "not-a-number" }))).toBe(0); + }); + + it("returns 0 when quantity is not a finite number", () => { + expect(askUsd(makeOrder({ quantity: "NaN" }))).toBe(0); + }); + + it("handles fractional prices correctly", () => { + const order = makeOrder({ ask_amount: "125", quantity: "50" }); + expect(askUsd(order)).toBe(2.5); + }); +}); + +// ============================================================ +// scoreHealth — liquidity health tier classifier +// ============================================================ + +describe("scoreHealth", () => { + const floor = config.market.liquidityDepthFloor; // 5000 in the default config + + it("returns CRITICAL when depth is below half the floor", () => { + // depthUsd = 2000, floor = 5000 → 2000 < 2500 → CRITICAL + const result = scoreHealth(floor * 0.4, 5); + expect(result.tier).toBe("CRITICAL"); + }); + + it("returns DEGRADED when depth is below the floor but above half", () => { + // depthUsd = 3000 (floor 5000 → half 2500, so above half), + // 10 orders so countScore = 0.5, depthRatio = 3000/10000 = 0.3, + // score = 0.3*0.7 + 0.5*0.3 = 0.21 + 0.15 = 0.36 — not CRITICAL + // (0.36 >= 0.3), not HEALTHY (3000 < 5000), so DEGRADED. + const result = scoreHealth(floor * 0.6, 10); + expect(result.tier).toBe("DEGRADED"); + }); + + it("returns HEALTHY when depth clears the floor and order count is strong", () => { + // depthUsd = 15000, 20 orders → depthRatio clamped to 1.0, countScore = 1.0, score = 1.0 + const result = scoreHealth(floor * 3, 20); + expect(result.tier).toBe("HEALTHY"); + expect(result.score).toBeCloseTo(1.0, 10); + }); + + it("returns DEGRADED when depth is above the floor but score drops below 0.6", () => { + // depth = exactly floor, sellOrderCount = 2 → depthRatio = 0.5, countScore = 0.1, score = 0.38 + // 0.38 < 0.6 → DEGRADED + const result = scoreHealth(floor, 2); + expect(result.tier).toBe("DEGRADED"); + expect(result.score).toBeLessThan(0.6); + }); + + it("caps the depth score at 1.0 even for very high depth", () => { + const result = scoreHealth(floor * 100, 50); + expect(result.score).toBeLessThanOrEqual(1.0); + }); + + it("caps the count score at 20 orders", () => { + const low = scoreHealth(floor * 2, 20); + const high = scoreHealth(floor * 2, 200); + expect(low.score).toBeCloseTo(high.score, 10); + }); + + it("scores zero depth and zero orders at 0.0", () => { + const result = scoreHealth(0, 0); + expect(result.score).toBe(0); + expect(result.tier).toBe("CRITICAL"); + }); +}); diff --git a/agent-003-market-monitor/src/workflows/liquidity-monitor.ts b/agent-003-market-monitor/src/workflows/liquidity-monitor.ts new file mode 100644 index 0000000..4ba4a5b --- /dev/null +++ b/agent-003-market-monitor/src/workflows/liquidity-monitor.ts @@ -0,0 +1,232 @@ +import { LedgerClient } from "../ledger.js"; +import { store } from "../store.js"; +import { config } from "../config.js"; +import { describeLiquidity } from "../monitor.js"; +import { output } from "../output.js"; +import type { OODAWorkflow } from "../ooda.js"; +import type { + LiquiditySnapshot, + SellOrder, + CreditClass, + AlertLevel, +} from "../types.js"; +import { median, classIdFromBatchDenom } from "../utils.js"; + +/** + * WF-MM-02: Liquidity Monitoring & Reporting + * + * Trigger: periodic (hourly) OR significant_trade (volume > $10k) + * Layer: 1 (Fully Automated) + * + * OODA: + * Observe — Fetch all open sell orders, group by credit class + * Orient — Compute liquidity metrics per class (listed value, + * bid-ask spread proxy, top-10 depth, health score) + * Decide — Generate per-class liquidity reports. Flag classes + * whose depth falls below the configured floor. + * Act — Persist snapshot, output report, alert on degradation. + */ + +interface Observations { + classes: CreditClass[]; + ordersByClass: Map; +} + +interface Orientation { + snapshots: LiquiditySnapshot[]; +} + +interface Decision { + reports: { + snapshot: LiquiditySnapshot; + previous: LiquiditySnapshot | null; + report: string; + alertLevel: AlertLevel; + }[]; +} + +interface Actions { + saved: number; + alertsSent: number; +} + +export function askUsd(order: SellOrder): number { + const ask = Number(order.ask_amount); + const qty = Number(order.quantity); + if (!Number.isFinite(ask) || !Number.isFinite(qty) || qty <= 0) return 0; + // MVP: treat ask as already-USD; price oracle integration is future work. + return ask / qty; +} + +export function scoreHealth( + depthUsd: number, + sellOrderCount: number +): { score: number; tier: LiquiditySnapshot["health"] } { + const depthRatio = Math.min(1, depthUsd / (config.market.liquidityDepthFloor * 2)); + const countScore = Math.min(1, sellOrderCount / 20); + const score = depthRatio * 0.7 + countScore * 0.3; + + let tier: LiquiditySnapshot["health"]; + if (depthUsd < config.market.liquidityDepthFloor * 0.5 || score < 0.3) { + tier = "CRITICAL"; + } else if (depthUsd < config.market.liquidityDepthFloor || score < 0.6) { + tier = "DEGRADED"; + } else { + tier = "HEALTHY"; + } + + return { score, tier }; +} + +export function createLiquidityMonitorWorkflow( + ledger: LedgerClient +): OODAWorkflow { + return { + id: "WF-MM-02", + name: "Liquidity Monitoring & Reporting", + + async observe(): Promise { + const [classes, sellOrders] = await Promise.all([ + ledger.getCreditClasses(), + ledger.getSellOrders(500), + ]); + + const ordersByClass = new Map(); + for (const order of sellOrders) { + const classId = classIdFromBatchDenom(order.batch_denom); + const bucket = ordersByClass.get(classId) || []; + bucket.push(order); + ordersByClass.set(classId, bucket); + } + + return { classes, ordersByClass }; + }, + + async orient(obs: Observations): Promise { + const snapshots: LiquiditySnapshot[] = []; + const capturedAt = new Date().toISOString(); + + // Snapshot every class that currently has any open sell orders. + // Classes with zero open orders are intentionally skipped — + // that's a "no market" state, distinct from a degraded market. + for (const [classId, orders] of obs.ordersByClass) { + if (orders.length === 0) continue; + + const prices = orders + .map((o) => askUsd(o)) + .filter((p) => p > 0) + .sort((a, b) => a - b); + if (prices.length === 0) continue; + + const quantities = orders + .map((o) => Number(o.quantity)) + .filter((q) => Number.isFinite(q) && q > 0); + const totalListedQuantity = quantities.reduce((a, b) => a + b, 0); + + const listedValues = orders.map((o) => { + const p = askUsd(o); + const q = Number(o.quantity); + return Number.isFinite(p) && Number.isFinite(q) ? p * q : 0; + }); + const totalListedValueUsd = listedValues.reduce((a, b) => a + b, 0); + + // Pre-compute askUsd once per order so the comparator is a + // plain number subtraction instead of recomputing the ratio on + // every sort invocation (sort can call the comparator O(n log n) + // times). + const pricedOrders = orders + .map((order) => ({ order, price: askUsd(order) })) + .sort((a, b) => a.price - b.price); + const top10 = pricedOrders.slice(0, 10); + const depthUsd = top10.reduce((acc, { order, price }) => { + const q = Number(order.quantity); + return acc + (Number.isFinite(price) && Number.isFinite(q) ? price * q : 0); + }, 0); + + const lowestAskUsd = prices[0]!; + const highestAskUsd = prices[prices.length - 1]!; + const medianAskUsd = median(prices); + const meanAskUsd = prices.reduce((a, b) => a + b, 0) / prices.length; + + const { score, tier } = scoreHealth(depthUsd, orders.length); + + snapshots.push({ + classId, + totalListedQuantity, + totalListedValueUsd, + sellOrderCount: orders.length, + lowestAskUsd, + highestAskUsd, + medianAskUsd, + meanAskUsd, + depthUsd, + healthScore: score, + health: tier, + capturedAt, + }); + } + + return { snapshots }; + }, + + async decide(orientation: Orientation): Promise { + const reports: Decision["reports"] = []; + + for (const snapshot of orientation.snapshots) { + const prevRow = store.getLatestLiquiditySnapshot(snapshot.classId); + let previous: LiquiditySnapshot | null = null; + if (prevRow) { + try { + previous = JSON.parse(prevRow.snapshot) as LiquiditySnapshot; + } catch { + previous = null; + } + } + + const report = await describeLiquidity(snapshot, previous); + + const alertLevel: AlertLevel = + snapshot.health === "CRITICAL" + ? "CRITICAL" + : snapshot.health === "DEGRADED" + ? "HIGH" + : "NORMAL"; + + reports.push({ snapshot, previous, report, alertLevel }); + } + + return { reports }; + }, + + async act(decision: Decision): Promise { + let saved = 0; + let alertsSent = 0; + + for (const { snapshot, report, alertLevel } of decision.reports) { + store.saveLiquiditySnapshot( + snapshot.classId, + JSON.stringify(snapshot), + snapshot.health + ); + saved++; + + await output({ + workflow: "WF-MM-02", + subjectId: snapshot.classId, + title: `Liquidity ${snapshot.health} (score ${snapshot.healthScore.toFixed(2)})`, + content: report, + alertLevel, + timestamp: new Date(), + }); + + if (alertLevel !== "NORMAL") alertsSent++; + } + + if (decision.reports.length === 0) { + console.log(" No credit classes with open sell orders."); + } + + return { saved, alertsSent }; + }, + }; +} diff --git a/agent-003-market-monitor/src/workflows/price-anomaly-detection.test.ts b/agent-003-market-monitor/src/workflows/price-anomaly-detection.test.ts new file mode 100644 index 0000000..36a8435 --- /dev/null +++ b/agent-003-market-monitor/src/workflows/price-anomaly-detection.test.ts @@ -0,0 +1,187 @@ +import { describe, it, expect } from "vitest"; +import { + median, + stddev, + classifyAnomaly, + classIdFromBatchDenom, + isUsdStableDenom, +} from "./price-anomaly-detection.js"; + +// ============================================================ +// median — deterministic statistical median +// ============================================================ + +describe("median", () => { + it("returns 0 for an empty list", () => { + expect(median([])).toBe(0); + }); + + it("returns the single value for a one-element list", () => { + expect(median([42])).toBe(42); + }); + + it("returns the middle value for an odd-length list", () => { + expect(median([1, 5, 3])).toBe(3); + expect(median([7, 2, 9, 4, 5])).toBe(5); + }); + + it("returns the mean of the two middle values for an even-length list", () => { + expect(median([1, 2, 3, 4])).toBe(2.5); + expect(median([10, 20])).toBe(15); + }); + + it("does not mutate the input array", () => { + const input = [3, 1, 2]; + median(input); + expect(input).toEqual([3, 1, 2]); + }); + + it("handles negative numbers correctly", () => { + expect(median([-5, -1, -3])).toBe(-3); + expect(median([-4, -2, 2, 4])).toBe(0); + }); + + it("handles floating point values", () => { + expect(median([0.1, 0.2, 0.3])).toBeCloseTo(0.2, 10); + expect(median([1.5, 2.5, 3.5, 4.5])).toBe(3); + }); +}); + +// ============================================================ +// stddev — sample standard deviation +// ============================================================ + +describe("stddev", () => { + it("returns 0 for an empty list", () => { + expect(stddev([], 0)).toBe(0); + }); + + it("returns 0 for a single value (no variance possible)", () => { + expect(stddev([42], 42)).toBe(0); + }); + + it("computes sample stddev for a two-element list", () => { + // [1, 3], mean 2, sum of sq = 1 + 1 = 2, divide by n-1 = 2, sqrt = sqrt(2) + expect(stddev([1, 3], 2)).toBeCloseTo(Math.SQRT2, 10); + }); + + it("computes sample stddev for a known five-element list", () => { + // [2, 4, 4, 4, 5, 5, 7, 9] has a textbook stddev of 2 (sample) + // but let's use a cleaner one: [1, 2, 3, 4, 5] mean 3 + // sum of sq dev = 4 + 1 + 0 + 1 + 4 = 10, / 4 = 2.5, sqrt ≈ 1.5811 + const values = [1, 2, 3, 4, 5]; + const mean = values.reduce((a, b) => a + b, 0) / values.length; + expect(stddev(values, mean)).toBeCloseTo(Math.sqrt(2.5), 10); + }); + + it("uses n-1 denominator (sample, not population)", () => { + // Population stddev of [1, 2] with mean 1.5 would be 0.5 + // Sample stddev is sqrt(0.5/1) ≈ 0.7071 + expect(stddev([1, 2], 1.5)).toBeCloseTo(Math.SQRT1_2, 10); + }); +}); + +// ============================================================ +// classifyAnomaly — severity classifier +// ============================================================ + +describe("classifyAnomaly", () => { + it("returns INFO for zero z-scores", () => { + expect(classifyAnomaly(0, 0)).toBe("INFO"); + }); + + it("returns INFO for z-scores below the WARNING threshold (2.0)", () => { + expect(classifyAnomaly(1.9, 1.5)).toBe("INFO"); + expect(classifyAnomaly(-1.9, 1.5)).toBe("INFO"); + }); + + it("returns WARNING at the exact WARNING threshold", () => { + expect(classifyAnomaly(2.0, 0)).toBe("WARNING"); + expect(classifyAnomaly(0, 2.0)).toBe("WARNING"); + }); + + it("returns WARNING between 2.0 and CRITICAL", () => { + expect(classifyAnomaly(2.5, 1.0)).toBe("WARNING"); + expect(classifyAnomaly(3.4, 0)).toBe("WARNING"); + }); + + it("returns CRITICAL at the exact CRITICAL threshold (3.5)", () => { + expect(classifyAnomaly(3.5, 0)).toBe("CRITICAL"); + expect(classifyAnomaly(0, 3.5)).toBe("CRITICAL"); + }); + + it("returns CRITICAL above the CRITICAL threshold", () => { + expect(classifyAnomaly(5, 1)).toBe("CRITICAL"); + expect(classifyAnomaly(10, 0)).toBe("CRITICAL"); + }); + + it("picks the larger absolute z-score from the two inputs", () => { + expect(classifyAnomaly(1.5, 4.0)).toBe("CRITICAL"); + expect(classifyAnomaly(2.8, 1.0)).toBe("WARNING"); + }); + + it("treats negative z-scores symmetrically with positive", () => { + expect(classifyAnomaly(-3.6, 0)).toBe("CRITICAL"); + expect(classifyAnomaly(-2.1, 0)).toBe("WARNING"); + }); +}); + +// ============================================================ +// classIdFromBatchDenom — class ID extractor +// ============================================================ + +describe("classIdFromBatchDenom", () => { + it("extracts the class ID from a canonical Regen batch denom", () => { + expect(classIdFromBatchDenom("C01-001-20240101-20241231-001")).toBe("C01"); + }); + + it("handles single-letter class codes", () => { + expect(classIdFromBatchDenom("C-001-20240101-20241231-001")).toBe("C"); + expect(classIdFromBatchDenom("BT-001-20240101-20241231-001")).toBe("BT"); + }); + + it("returns the whole string when no dash is present", () => { + expect(classIdFromBatchDenom("UNKNOWN")).toBe("UNKNOWN"); + }); + + it("handles an empty string", () => { + expect(classIdFromBatchDenom("")).toBe(""); + }); + + it("returns the whole string when the first character is a dash", () => { + // Degenerate but deterministic — the slice is empty before the dash. + expect(classIdFromBatchDenom("-leading-dash")).toBe("-leading-dash"); + }); +}); + +// ============================================================ +// isUsdStableDenom — stablecoin denom check +// ============================================================ + +describe("isUsdStableDenom", () => { + it("recognizes USDC denoms case-insensitively", () => { + expect(isUsdStableDenom("usdc")).toBe(true); + expect(isUsdStableDenom("USDC")).toBe(true); + expect(isUsdStableDenom("ibc/USDC-regen")).toBe(true); + }); + + it("recognizes USDT", () => { + expect(isUsdStableDenom("usdt")).toBe(true); + expect(isUsdStableDenom("ibc/USDT-axelar")).toBe(true); + }); + + it("recognizes DAI", () => { + expect(isUsdStableDenom("dai")).toBe(true); + expect(isUsdStableDenom("ibc/DAI-cosmos")).toBe(true); + }); + + it("rejects REGEN and other non-stable denoms", () => { + expect(isUsdStableDenom("uregen")).toBe(false); + expect(isUsdStableDenom("uatom")).toBe(false); + expect(isUsdStableDenom("eco-credits")).toBe(false); + }); + + it("rejects the empty string", () => { + expect(isUsdStableDenom("")).toBe(false); + }); +}); diff --git a/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts b/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts new file mode 100644 index 0000000..5e90d22 --- /dev/null +++ b/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts @@ -0,0 +1,249 @@ +import { LedgerClient } from "../ledger.js"; +import { store } from "../store.js"; +import { config } from "../config.js"; +import { describePriceAnomaly } from "../monitor.js"; +import { output } from "../output.js"; +import type { OODAWorkflow } from "../ooda.js"; +import type { + Trade, + PriceAnomaly, + AnomalySeverity, + SellOrder, +} from "../types.js"; +import { + median, + stddev, + classIdFromBatchDenom, + isUsdStableDenom, +} from "../utils.js"; + +// Re-exported so the existing unit-test suite can import helpers from +// this workflow file (the tests pre-date the utils.ts extraction). +export { median, stddev, classIdFromBatchDenom, isUsdStableDenom }; + +/** + * WF-MM-01: Price Anomaly Detection + * + * Trigger: SellOrderCreated OR SellOrderFilled + * Layer: 1 (alerts) / Layer 2 (if action needed) + * SLA: Alert within one polling cycle of the trade landing on-chain + * + * OODA: + * Observe — Fetch current sell orders + record as synthetic trade + * observations. When Ledger MCP provides real fill data, + * the observe phase swaps in real trades. + * Orient — For each new observation, compute z-score vs class + * median and batch median using the trailing window. + * Decide — Classify severity (INFO / WARNING / CRITICAL) per the + * thresholds in config.market. Only surface WARNING or + * CRITICAL. Dedupe against previous alerts for this trade. + * Act — Persist, generate narrative via Claude, output to channels. + * + * Determinism: all numeric decisions (median, z-score, severity) are + * computed locally. Claude is only used for the narrative report. + */ + +interface Observations { + newTrades: Trade[]; +} + +interface Orientation { + anomalies: PriceAnomaly[]; +} + +interface Decision { + alertsToPublish: PriceAnomaly[]; +} + +interface Actions { + alerted: number; + skipped: number; +} + +/** + * Convert a sell order into a synthetic "trade observation" for this + * agent. Until Ledger MCP provides filled-trade events, the agent uses + * the ask price as a proxy signal — an unusually high/low ask price + * still indicates a market structure anomaly worth surfacing. + */ +function sellOrderToTrade(order: SellOrder, classId: string): Trade | null { + const qty = Number(order.quantity); + const ask = Number(order.ask_amount); + if (!Number.isFinite(qty) || !Number.isFinite(ask) || qty <= 0) return null; + + // Only USD stablecoin asks are priced — anything else would require + // an oracle we don't have yet, and letting non-USD orders into the + // baseline would pollute the class/batch medians with 1:1-priced + // garbage. Downstream code can plug in a real price oracle. + if (!isUsdStableDenom(order.ask_denom)) return null; + const pricePerCredit = ask / qty; + + return { + id: order.id, + batchDenom: order.batch_denom, + classId, + seller: order.seller, + buyer: "", + quantity: qty, + pricePerCredit, + askDenom: order.ask_denom, + executedAt: new Date().toISOString(), + }; +} + +export function classifyAnomaly( + zClass: number, + zBatch: number +): AnomalySeverity { + const maxZ = Math.max(Math.abs(zClass), Math.abs(zBatch)); + if (maxZ >= config.market.criticalZScore) return "CRITICAL"; + if (maxZ >= config.market.warningZScore) return "WARNING"; + return "INFO"; +} + +export function createPriceAnomalyDetectionWorkflow( + ledger: LedgerClient +): OODAWorkflow { + return { + id: "WF-MM-01", + name: "Price Anomaly Detection", + + async observe(): Promise { + const sellOrders = await ledger.getSellOrders(200); + + const newTrades: Trade[] = []; + for (const order of sellOrders) { + const classId = classIdFromBatchDenom(order.batch_denom); + const trade = sellOrderToTrade(order, classId); + if (trade) { + newTrades.push(trade); + store.recordTrade({ + tradeId: trade.id, + classId: trade.classId, + batchDenom: trade.batchDenom, + pricePerCredit: trade.pricePerCredit, + quantity: trade.quantity, + executedAt: trade.executedAt, + }); + } + } + + return { newTrades }; + }, + + async orient(obs: Observations): Promise { + const anomalies: PriceAnomaly[] = []; + + for (const trade of obs.newTrades) { + const classSamples = store.getTradesByClass( + trade.classId, + config.market.classMedianWindowDays + ); + const batchSamples = store.getTradesByBatch( + trade.batchDenom, + config.market.batchMedianWindowDays + ); + + if (classSamples.length < config.market.minSamples) continue; + + const classMedian = median(classSamples); + const classMean = classSamples.reduce((a, b) => a + b, 0) / classSamples.length; + const classStd = stddev(classSamples, classMean); + // Z-score is (value - mean) / stddev; using median in the + // numerator while dividing by stddev (computed off the mean) + // would be statistically inconsistent. Median is still + // published in the alert payload as a context signal. + const zClass = classStd > 0 ? (trade.pricePerCredit - classMean) / classStd : 0; + + const batchMedian = batchSamples.length > 0 ? median(batchSamples) : classMedian; + const batchMean = + batchSamples.length > 0 + ? batchSamples.reduce((a, b) => a + b, 0) / batchSamples.length + : classMean; + const batchStd = batchSamples.length > 1 ? stddev(batchSamples, batchMean) : classStd; + const zBatch = batchStd > 0 ? (trade.pricePerCredit - batchMean) / batchStd : 0; + + const severity = classifyAnomaly(zClass, zBatch); + if (severity === "INFO") continue; + + // Confidence rises with sample size and drops when class and + // batch z-scores disagree (which often indicates the batch is + // just thinly traded rather than a real anomaly). + const sampleConfidence = Math.min(1, classSamples.length / 30); + const agreement = + Math.sign(zClass) === Math.sign(zBatch) && Math.abs(zBatch) > 0 + ? 1 + : 0.6; + const confidence = Math.max(0, Math.min(1, sampleConfidence * agreement)); + + anomalies.push({ + tradeId: trade.id, + batchDenom: trade.batchDenom, + classId: trade.classId, + seller: trade.seller, + quantity: trade.quantity, + pricePerCredit: trade.pricePerCredit, + classMedian, + batchMedian, + zScoreVsClass: zClass, + zScoreVsBatch: zBatch, + sampleSizeClass: classSamples.length, + sampleSizeBatch: batchSamples.length, + severity, + detectedAt: new Date().toISOString(), + confidence, + }); + } + + return { anomalies }; + }, + + async decide(orientation: Orientation): Promise { + const alertsToPublish: PriceAnomaly[] = []; + + for (const anomaly of orientation.anomalies) { + const existing = store.getAnomalyAlertSeverity(anomaly.tradeId); + // Dedupe: don't re-alert at the same or lower severity. + if (existing === "CRITICAL") continue; + if (existing === "WARNING" && anomaly.severity !== "CRITICAL") continue; + + alertsToPublish.push(anomaly); + } + + return { alertsToPublish }; + }, + + async act(decision: Decision): Promise { + let alerted = 0; + let skipped = 0; + + for (const anomaly of decision.alertsToPublish) { + const report = await describePriceAnomaly(anomaly); + + store.saveAnomalyAlert({ + tradeId: anomaly.tradeId, + severity: anomaly.severity, + zClass: anomaly.zScoreVsClass, + zBatch: anomaly.zScoreVsBatch, + report, + }); + + await output({ + workflow: "WF-MM-01", + subjectId: anomaly.tradeId, + title: `${anomaly.severity} — ${anomaly.classId} / ${anomaly.batchDenom}`, + content: report, + alertLevel: anomaly.severity === "CRITICAL" ? "CRITICAL" : "HIGH", + timestamp: new Date(), + }); + alerted++; + } + + if (decision.alertsToPublish.length === 0) { + console.log(" No new price anomalies above threshold."); + } + + return { alerted, skipped }; + }, + }; +} diff --git a/agent-003-market-monitor/src/workflows/retirement-tracking.test.ts b/agent-003-market-monitor/src/workflows/retirement-tracking.test.ts new file mode 100644 index 0000000..b396ace --- /dev/null +++ b/agent-003-market-monitor/src/workflows/retirement-tracking.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect } from "vitest"; +// Import directly from utils.ts so the test does not transitively +// pull in the workflow module, which constructs the SQLite-backed +// Store singleton at module load. +import { computeDemandIndex } from "../utils.js"; + +// ============================================================ +// computeDemandIndex — bounded 0-100 demand signal +// ============================================================ + +describe("computeDemandIndex", () => { + it("returns 0 for zero-activity inputs", () => { + // volume component: log10(max(1, 0)) * 20 = 0, count: 0, breadth: 0 + expect(computeDemandIndex(0, 0, 0)).toBe(0); + }); + + it("caps the volume component at 60", () => { + // log10(1e100) * 20 = 2000, clamped to 60 + expect(computeDemandIndex(1e100, 0, 0)).toBe(60); + }); + + it("caps the count component at 20 (10 retirements)", () => { + // 50 retirements → 50 * 2 = 100, clamped to 20 + const big = computeDemandIndex(0, 50, 0); + const exact = computeDemandIndex(0, 10, 0); + expect(big).toBe(20); + expect(exact).toBe(20); + }); + + it("caps the breadth component at 20 (5 unique retirees)", () => { + // 100 unique retirees → 100 * 4 = 400, clamped to 20 + const big = computeDemandIndex(0, 0, 100); + const exact = computeDemandIndex(0, 0, 5); + expect(big).toBe(20); + expect(exact).toBe(20); + }); + + it("caps the total at 100 when all three components max out", () => { + // volume 60 + count 20 + breadth 20 = 100 + expect(computeDemandIndex(1e100, 50, 50)).toBe(100); + }); + + it("returns a mid-range value for moderate activity", () => { + // totalQuantity = 10000 → log10(10000)*20 = 80 → clamped to 60 + // retirementCount = 5 → 5*2 = 10 + // uniqueRetirees = 3 → 3*4 = 12 + // total = 60 + 10 + 12 = 82 + expect(computeDemandIndex(10000, 5, 3)).toBe(82); + }); + + it("uses log10 scaling so each order of magnitude adds ~20 pre-cap", () => { + // totalQty 10 → log10(10) * 20 = 20 + // totalQty 100 → log10(100) * 20 = 40 + const a = computeDemandIndex(10, 0, 0); + const b = computeDemandIndex(100, 0, 0); + expect(b - a).toBe(20); + }); + + it("treats totalQuantity < 1 as 1 (log10 floor)", () => { + // Math.max(1, 0.5) = 1, log10(1) = 0 + expect(computeDemandIndex(0.5, 0, 0)).toBe(0); + expect(computeDemandIndex(0.0001, 0, 0)).toBe(0); + }); + + it("rounds the final index to the nearest integer", () => { + // totalQty = 3 → log10(3) * 20 ≈ 9.542 + // Math.round(9.542) = 10 + expect(computeDemandIndex(3, 0, 0)).toBe(10); + }); +}); diff --git a/agent-003-market-monitor/src/workflows/retirement-tracking.ts b/agent-003-market-monitor/src/workflows/retirement-tracking.ts new file mode 100644 index 0000000..a70a5de --- /dev/null +++ b/agent-003-market-monitor/src/workflows/retirement-tracking.ts @@ -0,0 +1,225 @@ +import { LedgerClient } from "../ledger.js"; +import { store } from "../store.js"; +import { config } from "../config.js"; +import { describeRetirementSummary } from "../monitor.js"; +import { output } from "../output.js"; +import type { OODAWorkflow } from "../ooda.js"; +import type { + CreditClass, + CreditBatch, + BatchSupply, + RetirementSummary, +} from "../types.js"; +import { classIdFromBatchDenom, computeDemandIndex } from "../utils.js"; + +// Re-exported so the existing unit test suite can keep importing +// `computeDemandIndex` from this workflow file. +export { computeDemandIndex }; + +/** + * WF-MM-03: Retirement Pattern Analysis + * + * Trigger: MsgRetire event (proxied by per-cycle retired-amount deltas + * until the Ledger MCP provides an event stream) + * Layer: 1 (Fully Automated) + * + * OODA: + * Observe — Fetch credit classes + batches + batch supplies. The + * supply response includes `retired_amount`, which we + * compare against the previously observed amount to derive + * a per-class retirement delta for this cycle. + * Orient — Build a RetirementSummary per class: totals, unique + * retirees proxied by batch count for this MVP, demand + * index derived from quantity vs trailing baseline. + * Decide — Generate narrative summary for classes that moved. + * Act — Persist, output, alert on large positive demand shifts. + * + * This MVP uses the batch supply query as the retirement source. A + * follow-up PR will plug in a real tx-stream client (proto.regen. + * ecocredit.v1.MsgRetire) once the LCD event endpoint is available. + */ + +interface BatchWithSupply { + batch: CreditBatch; + supply: BatchSupply; + classId: string; +} + +interface Observations { + classes: CreditClass[]; + batchesWithSupply: BatchWithSupply[]; +} + +interface Orientation { + summariesByClass: Map; +} + +interface Decision { + reports: { + summary: RetirementSummary; + baselineDemand: number; + report: string; + }[]; +} + +interface Actions { + saved: number; + alertsSent: number; +} + +export function createRetirementTrackingWorkflow( + ledger: LedgerClient +): OODAWorkflow { + return { + id: "WF-MM-03", + name: "Retirement Pattern Analysis", + + async observe(): Promise { + const [classes, batches] = await Promise.all([ + ledger.getCreditClasses(), + ledger.getCreditBatches(), + ]); + + // Cap the per-cycle batch fan-out to the most recent 100 batches + // so we don't hammer the LCD on mainnet (where batch counts grow + // unbounded). Older batches are retired less frequently and the + // agent will catch new retirement activity next cycle anyway. + const cappedBatches = batches.slice(0, 100); + + // Limit concurrency to 5 in-flight LCD requests at a time. + // `Promise.all(cappedBatches.map(...))` fires 100 concurrent + // requests and will trip rate limits on public LCD endpoints. + const batchesWithSupply: BatchWithSupply[] = []; + const CONCURRENCY = 5; + for (let i = 0; i < cappedBatches.length; i += CONCURRENCY) { + const chunk = cappedBatches.slice(i, i + CONCURRENCY); + const results = await Promise.all( + chunk.map(async (batch) => { + const supply = await ledger.getBatchSupply(batch.denom); + return supply + ? { + batch, + supply, + classId: classIdFromBatchDenom(batch.denom), + } + : null; + }) + ); + for (const r of results) if (r) batchesWithSupply.push(r); + } + + return { classes, batchesWithSupply }; + }, + + async orient(obs: Observations): Promise { + const summariesByClass = new Map(); + const capturedAt = new Date().toISOString(); + + // Group batches by class so we can aggregate retired amounts. + const byClass = new Map(); + for (const row of obs.batchesWithSupply) { + const bucket = byClass.get(row.classId) || []; + bucket.push(row); + byClass.set(row.classId, bucket); + } + + for (const [classId, rows] of byClass) { + let totalRetired = 0; + let batchesWithRetirement = 0; + for (const row of rows) { + const retired = Number(row.supply.retired_amount); + if (Number.isFinite(retired) && retired > 0) { + totalRetired += retired; + batchesWithRetirement++; + } + } + if (totalRetired === 0) continue; + + // Unique retirees / top retiree are placeholders until the + // tx-event source lands. The summary still tells the right + // story: "N batches in class X have retirements totaling Y". + const uniqueRetirees = batchesWithRetirement; + const topRetiree: string | null = null; + const topRetireeQuantity = 0; + + // Treat USD value as 1:1 with quantity for the MVP. Price + // oracle integration is future work — documented as an open + // question in the workflow spec. + const totalValueUsd = totalRetired; + const demandIndex = computeDemandIndex( + totalRetired, + batchesWithRetirement, + uniqueRetirees + ); + + summariesByClass.set(classId, { + classId, + windowHours: config.market.retirementWindowHours, + retirementCount: batchesWithRetirement, + totalQuantity: totalRetired, + totalValueUsd, + uniqueRetirees, + topRetiree, + topRetireeQuantity, + pctWithJurisdiction: 0, + demandIndex, + capturedAt, + }); + } + + return { summariesByClass }; + }, + + async decide(orientation: Orientation): Promise { + const reports: Decision["reports"] = []; + + for (const summary of orientation.summariesByClass.values()) { + const baselineDemand = store.getBaselineDemand(summary.classId, 7); + const report = await describeRetirementSummary(summary, baselineDemand); + reports.push({ summary, baselineDemand, report }); + } + + return { reports }; + }, + + async act(decision: Decision): Promise { + let saved = 0; + let alertsSent = 0; + + for (const { summary, baselineDemand, report } of decision.reports) { + store.saveRetirementSummary({ + classId: summary.classId, + windowHours: summary.windowHours, + totalQuantity: summary.totalQuantity, + totalValueUsd: summary.totalValueUsd, + retirementCount: summary.retirementCount, + demandIndex: summary.demandIndex, + summary: report, + }); + saved++; + + // Alert if demand index jumped meaningfully above the baseline. + // Threshold of +15 index points mirrors the agent character's + // "moderate-high" boundary (65 → 72 example). + const jumped = baselineDemand > 0 && summary.demandIndex - baselineDemand >= 15; + const alertLevel = jumped ? "HIGH" : "NORMAL"; + if (alertLevel !== "NORMAL") alertsSent++; + + await output({ + workflow: "WF-MM-03", + subjectId: summary.classId, + title: `Retirement summary (demand ${summary.demandIndex}${baselineDemand ? ` vs ${baselineDemand.toFixed(0)}` : ""})`, + content: report, + alertLevel, + timestamp: new Date(), + }); + } + + if (decision.reports.length === 0) { + console.log(" No retirement activity detected in any class."); + } + + return { saved, alertsSent }; + }, + }; +} diff --git a/agent-003-market-monitor/tsconfig.json b/agent-003-market-monitor/tsconfig.json new file mode 100644 index 0000000..e6a11e8 --- /dev/null +++ b/agent-003-market-monitor/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "src/**/*.test.ts", "vitest.config.ts"] +} diff --git a/agent-003-market-monitor/vitest.config.ts b/agent-003-market-monitor/vitest.config.ts new file mode 100644 index 0000000..ce36a74 --- /dev/null +++ b/agent-003-market-monitor/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts"], + environment: "node", + }, +});