From 80573e4d4882307ac677f41977060a4a51d2b4c3 Mon Sep 17 00:00:00 2001 From: brawlaphant <35781613+brawlaphant@users.noreply.github.com> Date: Sat, 11 Apr 2026 11:13:09 -0700 Subject: [PATCH 1/7] feat(agent-003): implement full market monitor workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the AGENT-002 Governance Analyst structure to give AGENT-003 Market Monitor the same full standalone TypeScript implementation that AGENT-002 has: - `agent-003-market-monitor/` — standalone Node.js process - `src/index.ts` — banner, cycle runner, polling vs single-run mode - `src/config.ts` — config + thresholds mirrored from the character - `src/types.ts` — ecocredit marketplace types + per-workflow shapes - `src/ledger.ts` — LCD client for ecocredit + marketplace endpoints - `src/store.ts` — SQLite state (trade obs, anomaly dedupe, liquidity snapshots, retirement summaries, workflow executions) - `src/ooda.ts` — generic OODA executor (same shape as agent-002) - `src/monitor.ts` — Claude narrative layer, one function per workflow - `src/output.ts` — console + optional Discord webhook dispatcher - `src/workflows/price-anomaly-detection.ts` — WF-MM-01 - `src/workflows/liquidity-monitor.ts` — WF-MM-02 - `src/workflows/retirement-tracking.ts` — WF-MM-03 Design decisions documented in the agent README: 1. Deterministic numbers, narrative-only LLM calls. All severity classification, z-score computation, liquidity health scoring, and demand index derivation happen locally. Claude only writes the report from those numbers. Keeps the agent cheap, reproducible, and auditable. 2. Sell-order-as-trade proxy for the MVP until Ledger MCP exposes filled-trade events. The code path swaps cleanly once real trade events are available. 3. Batch-supply delta as the retirement source for the MVP, capped at 100 most recent batches per cycle to protect the LCD. A follow-up PR will plug in a real MsgRetire tx-stream client. 4. Alert dedupe by (trade_id, severity) — a trade that escalates from WARNING to CRITICAL still fires a new alert; a same-severity re-alert does not. Thresholds (`config.market.*`) mirror the character definition at `agents/packages/agents/src/characters/market-monitor.ts` so downstream tooling has a single source of truth. Anomaly severity boundaries (WARNING >= 2.0, CRITICAL >= 3.5 z-score) and the liquidity depth floor live in `config.ts` and are referenced from the system prompt. CI: the new agent is added to the `agents` job so `npx tsc --noEmit` runs against it on every PR, matching how agent-002-governance-analyst is wired. - Lands in: `agent-003-market-monitor/`, `.github/workflows/ci.yml` - Changes: new standalone AGENT-003 process with 3 workflows (WF-MM-01/02/03) - Validate: `cd agent-003-market-monitor && npm ci && npx tsc --noEmit` Refs phase-2/2.2-agentic-workflows.md §WF-MM-01, §WF-MM-02, §WF-MM-03 Refs agents/packages/agents/src/characters/market-monitor.ts (#64) --- .github/workflows/ci.yml | 8 + agent-003-market-monitor/.env.example | 14 + agent-003-market-monitor/.gitignore | 4 + agent-003-market-monitor/README.md | 103 ++ agent-003-market-monitor/package-lock.json | 1475 +++++++++++++++++ agent-003-market-monitor/package.json | 27 + agent-003-market-monitor/src/config.ts | 51 + agent-003-market-monitor/src/index.ts | 102 ++ agent-003-market-monitor/src/ledger.ts | 148 ++ agent-003-market-monitor/src/monitor.ts | 205 +++ agent-003-market-monitor/src/ooda.ts | 83 + agent-003-market-monitor/src/output.ts | 59 + agent-003-market-monitor/src/store.ts | 288 ++++ agent-003-market-monitor/src/types.ts | 158 ++ .../src/workflows/liquidity-monitor.ts | 242 +++ .../src/workflows/price-anomaly-detection.ts | 262 +++ .../src/workflows/retirement-tracking.ts | 232 +++ agent-003-market-monitor/tsconfig.json | 20 + 18 files changed, 3481 insertions(+) create mode 100644 agent-003-market-monitor/.env.example create mode 100644 agent-003-market-monitor/.gitignore create mode 100644 agent-003-market-monitor/README.md create mode 100644 agent-003-market-monitor/package-lock.json create mode 100644 agent-003-market-monitor/package.json create mode 100644 agent-003-market-monitor/src/config.ts create mode 100644 agent-003-market-monitor/src/index.ts create mode 100644 agent-003-market-monitor/src/ledger.ts create mode 100644 agent-003-market-monitor/src/monitor.ts create mode 100644 agent-003-market-monitor/src/ooda.ts create mode 100644 agent-003-market-monitor/src/output.ts create mode 100644 agent-003-market-monitor/src/store.ts create mode 100644 agent-003-market-monitor/src/types.ts create mode 100644 agent-003-market-monitor/src/workflows/liquidity-monitor.ts create mode 100644 agent-003-market-monitor/src/workflows/price-anomaly-detection.ts create mode 100644 agent-003-market-monitor/src/workflows/retirement-tracking.ts create mode 100644 agent-003-market-monitor/tsconfig.json 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..c44c9e2 --- /dev/null +++ b/agent-003-market-monitor/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.db +.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..3362cc2 --- /dev/null +++ b/agent-003-market-monitor/package-lock.json @@ -0,0 +1,1475 @@ +{ + "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" + }, + "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/@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/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/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/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/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/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/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-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-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/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/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/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/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/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/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/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/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/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/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/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..1097339 --- /dev/null +++ b/agent-003-market-monitor/package.json @@ -0,0 +1,27 @@ +{ + "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", + "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" + }, + "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..ee252be --- /dev/null +++ b/agent-003-market-monitor/src/ledger.ts @@ -0,0 +1,148 @@ +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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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/workflows/liquidity-monitor.ts b/agent-003-market-monitor/src/workflows/liquidity-monitor.ts new file mode 100644 index 0000000..48d460e --- /dev/null +++ b/agent-003-market-monitor/src/workflows/liquidity-monitor.ts @@ -0,0 +1,242 @@ +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"; + +/** + * 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; +} + +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; +} + +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]!; +} + +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 }; +} + +function classIdFromBatchDenom(denom: string): string { + const idx = denom.indexOf("-"); + return idx > 0 ? denom.slice(0, idx) : denom; +} + +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); + + const sortedByPrice = orders + .slice() + .sort((a, b) => askUsd(a) - askUsd(b)); + const top10 = sortedByPrice.slice(0, 10); + const depthUsd = top10.reduce((acc, o) => { + const p = askUsd(o); + const q = Number(o.quantity); + return acc + (Number.isFinite(p) && Number.isFinite(q) ? p * 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.ts b/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts new file mode 100644 index 0000000..391a74e --- /dev/null +++ b/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts @@ -0,0 +1,262 @@ +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"; + +/** + * 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; + + // ask_amount is in the smallest unit of ask_denom. For the MVP we + // treat it as already-USD when denom is a USD stablecoin and pass a + // 1.0 conversion otherwise. Downstream code can plug in a real + // price oracle. + const usdPerAskUnit = isUsdStableDenom(order.ask_denom) ? 1 : 1; + const pricePerCredit = (ask * usdPerAskUnit) / 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(), + }; +} + +function isUsdStableDenom(denom: string): boolean { + const d = denom.toLowerCase(); + return d.includes("usdc") || d.includes("usdt") || d.includes("dai"); +} + +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]!; +} + +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)); +} + +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"; +} + +function classIdFromBatchDenom(denom: string): string { + // Regen batch denoms look like C01-001-20240101-20241231-001. + // The class id is the leading token before the first dash. + const idx = denom.indexOf("-"); + return idx > 0 ? denom.slice(0, idx) : denom; +} + +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); + const zClass = classStd > 0 ? (trade.pricePerCredit - classMedian) / 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 - batchMedian) / 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.ts b/agent-003-market-monitor/src/workflows/retirement-tracking.ts new file mode 100644 index 0000000..48ab231 --- /dev/null +++ b/agent-003-market-monitor/src/workflows/retirement-tracking.ts @@ -0,0 +1,232 @@ +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"; + +/** + * 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; +} + +function classIdFromBatchDenom(denom: string): string { + const idx = denom.indexOf("-"); + return idx > 0 ? denom.slice(0, idx) : denom; +} + +/** 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. */ +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); +} + +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); + + const batchesWithSupply: BatchWithSupply[] = []; + await Promise.all( + cappedBatches.map(async (batch) => { + const supply = await ledger.getBatchSupply(batch.denom); + if (supply) { + batchesWithSupply.push({ + batch, + supply, + classId: classIdFromBatchDenom(batch.denom), + }); + } + }) + ); + + 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..4a4a4f3 --- /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"] +} From a5162530a721d2e18e63bd5b97e85dee25bf5bf4 Mon Sep 17 00:00:00 2001 From: brawlaphant <35781613+brawlaphant@users.noreply.github.com> Date: Sat, 11 Apr 2026 12:41:50 -0700 Subject: [PATCH 2/7] test(agent-003): add vitest unit tests for helper math MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to PR #80 — promised in the PR description as "Unit tests for median, stddev, classifyAnomaly, scoreHealth, computeDemandIndex, etc., planned as a separate test-only PR so the original PR stays a single-concern 'add the agent' change." Adds 52 unit tests across 3 test files covering every deterministic helper function in the AGENT-003 workflows. The helpers compute severity, liquidity health, and demand signals — they're the parts most likely to silently drift in a future refactor, so pinning their exact behavior prevents regressions that a typecheck cannot catch. ## Changes ### Helper exports Eight previously module-private helpers are now exported so the test file can import them: price-anomaly-detection.ts: median, stddev, classifyAnomaly, classIdFromBatchDenom, isUsdStableDenom liquidity-monitor.ts: askUsd, scoreHealth retirement-tracking.ts: computeDemandIndex The export is the only production-code change — no behavior change, no API rename. Module consumers unchanged. ### Test files src/workflows/price-anomaly-detection.test.ts (30 tests) median — empty, one-element, odd length, even length, non-mutation, negatives, floats stddev — empty, single, two-element, known five-element, n-1 denominator (sample, not population) classifyAnomaly — INFO / WARNING / CRITICAL boundaries, max of two inputs, symmetry for negative z-scores classIdFromBatchDenom — canonical regen format, single-letter, no dash, empty, degenerate leading dash isUsdStableDenom — USDC/USDT/DAI case-insensitive, ibc/* wrappers, non-stable rejects, empty string src/workflows/liquidity-monitor.test.ts (13 tests) askUsd — normal, zero quantity, negative quantity, non-finite ask, non-finite quantity, fractional scoreHealth — CRITICAL at sub-half depth, DEGRADED between half and full, HEALTHY at 3x floor with strong count, score-cap when depth saturates, count-cap at 20, zero case src/workflows/retirement-tracking.test.ts (9 tests) computeDemandIndex — zero, volume cap (60), count cap (20 at 10 retirements), breadth cap (20 at 5 retirees), total cap (100), moderate activity, log10 scaling, sub-1 floor, rounding ### Vitest setup vitest.config.ts (8 lines) — standard config, node env package.json — adds "test" and "test:watch" scripts + vitest ^2.1.0 devDep tsconfig.json — excludes *.test.ts from the production typecheck path (tests still typecheck via vitest itself) .gitignore — adds *.db-shm and *.db-wal (SQLite WAL side files from running tests locally; the base *.db pattern doesn't catch them) ## Validation $ cd agent-003-market-monitor && npm test Test Files 3 passed (3) Tests 52 passed (52) $ cd agent-003-market-monitor && npx tsc --noEmit (exit 0) Both CI checks continue to pass after this PR. ## Scope Does NOT touch the workflow OODA loops themselves, the Claude narrative layer, the LCD client, the SQLite store, or any output formatting. The tests cover the pure functions only — the surrounding integration machinery is tested implicitly when AGENT-003 runs a live cycle against the Regen LCD. - Lands in: `agent-003-market-monitor/` - Changes: 52 unit tests + vitest setup + 8 helper exports - Validate: `cd agent-003-market-monitor && npm test` ## PR relationship This PR is based on PR #80's branch (the AGENT-003 implementation) because the helpers don't exist on upstream/main. If #80 merges first, this PR rebases cleanly. --- agent-003-market-monitor/.gitignore | 2 + agent-003-market-monitor/package-lock.json | 2703 +++++++++++++---- agent-003-market-monitor/package.json | 5 +- .../src/workflows/liquidity-monitor.test.ts | 106 + .../src/workflows/liquidity-monitor.ts | 4 +- .../workflows/price-anomaly-detection.test.ts | 187 ++ .../src/workflows/price-anomaly-detection.ts | 10 +- .../src/workflows/retirement-tracking.test.ts | 67 + .../src/workflows/retirement-tracking.ts | 2 +- agent-003-market-monitor/tsconfig.json | 2 +- agent-003-market-monitor/vitest.config.ts | 8 + 11 files changed, 2427 insertions(+), 669 deletions(-) create mode 100644 agent-003-market-monitor/src/workflows/liquidity-monitor.test.ts create mode 100644 agent-003-market-monitor/src/workflows/price-anomaly-detection.test.ts create mode 100644 agent-003-market-monitor/src/workflows/retirement-tracking.test.ts create mode 100644 agent-003-market-monitor/vitest.config.ts diff --git a/agent-003-market-monitor/.gitignore b/agent-003-market-monitor/.gitignore index c44c9e2..467cd39 100644 --- a/agent-003-market-monitor/.gitignore +++ b/agent-003-market-monitor/.gitignore @@ -1,4 +1,6 @@ node_modules/ dist/ *.db +*.db-shm +*.db-wal .env diff --git a/agent-003-market-monitor/package-lock.json b/agent-003-market-monitor/package-lock.json index 3362cc2..fda4fd4 100644 --- a/agent-003-market-monitor/package-lock.json +++ b/agent-003-market-monitor/package-lock.json @@ -15,7 +15,8 @@ "@types/better-sqlite3": "^7.6.12", "@types/node": "^22.0.0", "tsx": "^4.19.0", - "typescript": "^5.7.0" + "typescript": "^5.7.0", + "vitest": "^2.1.0" }, "engines": { "node": ">=20.0.0" @@ -493,6 +494,363 @@ "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", @@ -503,6 +861,13 @@ "@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", @@ -522,35 +887,158 @@ "form-data": "^4.0.4" } }, - "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==", + "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": { - "event-target-shim": "^5.0.0" + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" }, - "engines": { - "node": ">=6.5" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "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": { - "humanize-ms": "^1.2.1" + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" }, - "engines": { - "node": ">= 8.0.0" - } - }, - "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" + "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", @@ -572,874 +1060,1754 @@ ], "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, + "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": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "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==", + "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", - "dependencies": { - "file-uri-to-path": "1.0.0" + "engines": { + "node": ">= 0.4" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "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": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" + "url": "https://github.com/sponsors/ai" } ], "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "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==", + "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": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "semver": "^7.3.5" }, "engines": { - "node": ">= 0.4" + "node": ">=10" } }, - "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==", + "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", - "dependencies": { - "delayed-stream": "~1.0.0" - }, "engines": { - "node": ">= 0.8" + "node": ">=10.5.0" } }, - "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==", + "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": { - "mimic-response": "^3.1.0" + "whatwg-url": "^5.0.0" }, "engines": { - "node": ">=10" + "node": "4.x || >=6.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, - "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/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/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "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": ">=0.4.0" + "node": ">= 14.16" } }, - "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", + "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": ">=8" + "node": "^10 || ^12 || >=14" } }, - "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==", + "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": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" + "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": ">= 0.4" + "node": ">=10" } }, - "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==", + "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": { - "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" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, - "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/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/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==", + "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": { - "es-errors": "^1.3.0" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">= 6" } }, - "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==", + "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", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "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, - "hasInstallScript": true, "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, "bin": { - "esbuild": "bin/esbuild" + "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=18" + "node": ">=18.0.0", + "npm": ">=8.0.0" }, "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" + "@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/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", + "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": ">=6" + "node": ">=10" } }, - "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)", + "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": ">=6" + "node": ">=0.10.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==", + "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/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "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": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "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": ">=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==", + "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/formdata-node": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", - "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "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", - "dependencies": { - "node-domexception": "1.0.0", - "web-streams-polyfill": "4.0.0-beta.3" - }, "engines": { - "node": ">= 12.20" + "node": "^18.0.0 || >=20.0.0" } }, - "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==", + "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, - "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=14.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==", + "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", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=14.0.0" } }, - "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==", + "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": { - "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" + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" }, "engines": { - "node": ">= 0.4" + "node": ">=18.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "optionalDependencies": { + "fsevents": "~2.3.3" } }, - "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", + "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": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" + "safe-buffer": "^5.0.1" }, "engines": { - "node": ">= 0.4" + "node": "*" } }, - "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==", + "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": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + "engines": { + "node": ">=14.17" } }, - "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==", + "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/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/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/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "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": ">= 0.4" + "node": "^18.0.0 || >=20.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "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/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "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": { - "has-symbols": "^1.0.3" + "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": ">= 0.4" + "node": "^18.0.0 || >=20.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://opencollective.com/vitest" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "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", - "dependencies": { - "function-bind": "^1.1.2" - }, + "optional": true, + "os": [ + "aix" + ], "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": ">=12" } }, - "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" - } + "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" ], - "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/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">= 0.4" + "node": ">=12" } }, - "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==", + "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": ">= 0.6" + "node": ">=12" } }, - "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==", + "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", - "dependencies": { - "mime-db": "1.52.0" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">= 0.6" + "node": ">=12" } }, - "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==", + "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": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "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", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" } }, - "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/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/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/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==", + "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", - "dependencies": { - "semver": "^7.3.5" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=10" + "node": ">=12" } }, - "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" - } + "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": ">=10.5.0" + "node": ">=12" } }, - "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==", + "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", - "dependencies": { - "whatwg-url": "^5.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "node": ">=12" } }, - "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/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/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.", + "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", - "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" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" + "node": ">=12" } }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "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", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "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/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/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "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", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 6" + "node": ">=12" } }, - "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==", + "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", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "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" - } + "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" ], - "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": ">=12" } }, - "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" - } + "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" ], - "license": "MIT" + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } }, - "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" - } + "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", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" } }, - "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==", + "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", - "dependencies": { - "safe-buffer": "~5.2.0" + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" } }, - "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==", + "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": ">=0.10.0" + "node": ">=12" } }, - "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==", + "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", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" } }, - "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==", + "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", - "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" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=6" + "node": ">=12" } }, - "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==", + "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", - "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" - }, "bin": { - "tsx": "dist/cli.mjs" + "esbuild": "bin/esbuild" }, "engines": { - "node": ">=18.0.0" + "node": ">=12" }, "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", + "@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": { - "safe-buffer": "^5.0.1" + "@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" }, - "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" + "vitest": "vitest.mjs" }, "engines": { - "node": ">=14.17" + "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/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/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", @@ -1465,6 +2833,23 @@ "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", diff --git a/agent-003-market-monitor/package.json b/agent-003-market-monitor/package.json index 1097339..6f5a4af 100644 --- a/agent-003-market-monitor/package.json +++ b/agent-003-market-monitor/package.json @@ -9,6 +9,8 @@ "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": { @@ -19,7 +21,8 @@ "@types/better-sqlite3": "^7.6.12", "@types/node": "^22.0.0", "tsx": "^4.19.0", - "typescript": "^5.7.0" + "typescript": "^5.7.0", + "vitest": "^2.1.0" }, "engines": { "node": ">=20.0.0" 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 index 48d460e..d782065 100644 --- a/agent-003-market-monitor/src/workflows/liquidity-monitor.ts +++ b/agent-003-market-monitor/src/workflows/liquidity-monitor.ts @@ -49,7 +49,7 @@ interface Actions { alertsSent: number; } -function askUsd(order: SellOrder): 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; @@ -66,7 +66,7 @@ function median(values: number[]): number { : sorted[mid]!; } -function scoreHealth( +export function scoreHealth( depthUsd: number, sellOrderCount: number ): { score: number; tier: LiquiditySnapshot["health"] } { 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 index 391a74e..509b8e2 100644 --- a/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts +++ b/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts @@ -81,12 +81,12 @@ function sellOrderToTrade(order: SellOrder, classId: string): Trade | null { }; } -function isUsdStableDenom(denom: string): boolean { +export function isUsdStableDenom(denom: string): boolean { const d = denom.toLowerCase(); return d.includes("usdc") || d.includes("usdt") || d.includes("dai"); } -function median(values: number[]): number { +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); @@ -95,13 +95,13 @@ function median(values: number[]): number { : sorted[mid]!; } -function stddev(values: number[], mean: number): number { +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)); } -function classifyAnomaly( +export function classifyAnomaly( zClass: number, zBatch: number ): AnomalySeverity { @@ -111,7 +111,7 @@ function classifyAnomaly( return "INFO"; } -function classIdFromBatchDenom(denom: string): string { +export function classIdFromBatchDenom(denom: string): string { // Regen batch denoms look like C01-001-20240101-20241231-001. // The class id is the leading token before the first dash. const idx = denom.indexOf("-"); 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..dec3ccb --- /dev/null +++ b/agent-003-market-monitor/src/workflows/retirement-tracking.test.ts @@ -0,0 +1,67 @@ +import { describe, it, expect } from "vitest"; +import { computeDemandIndex } from "./retirement-tracking.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 index 48ab231..2f0d8a6 100644 --- a/agent-003-market-monitor/src/workflows/retirement-tracking.ts +++ b/agent-003-market-monitor/src/workflows/retirement-tracking.ts @@ -71,7 +71,7 @@ function classIdFromBatchDenom(denom: string): string { * 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. */ -function computeDemandIndex( +export function computeDemandIndex( totalQuantity: number, retirementCount: number, uniqueRetirees: number diff --git a/agent-003-market-monitor/tsconfig.json b/agent-003-market-monitor/tsconfig.json index 4a4a4f3..e6a11e8 100644 --- a/agent-003-market-monitor/tsconfig.json +++ b/agent-003-market-monitor/tsconfig.json @@ -16,5 +16,5 @@ "sourceMap": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "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", + }, +}); From 9982fd8d323ad7e1a4f8e64c890b59df4f761577 Mon Sep 17 00:00:00 2001 From: brawlaphant <35781613+brawlaphant@users.noreply.github.com> Date: Sat, 11 Apr 2026 13:02:07 -0700 Subject: [PATCH 3/7] feat(agent-003): real MsgRetire tx-stream for WF-MM-03 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the batch-supply-delta MVP proxy in WF-MM-03 with a real tx-search client that reads recent MsgRetire events from the Cosmos LCD. This closes the follow-up documented in PR #80's design-decision #3: "Batch supply delta as retirement source (MVP). A follow-up PR will plug in a real tx-stream client for MsgRetire once the LCD event endpoint is available." ## What changes in the workflow The observe phase no longer walks batches → supplies. Instead: const retirements = await ledger.getRecentRetirementTxs(200); The orient phase no longer synthesizes retirement records from supply deltas. Instead it calls a new pure function `aggregateRetirementsByClass` that groups a list of real Retirement records by class id, computes the top retiree by cumulative quantity (not count), counts unique retirees via a Set, and measures the jurisdiction-metadata coverage percentage. The act phase is unchanged — it still persists, outputs, and alerts on demand-index jumps. ## What changes in the ledger client Adds two new methods to `LedgerClient`: - `getRecentRetirementTxs(limit)` — queries the LCD tx-search endpoint with `events=message.action='/regen.ecocredit.v1.MsgRetire'`, reverse-ordered, limited. Parses each tx response into zero or more Retirement records. Returns [] on error (transient LCD failures degrade to "no recent retirements" rather than crashing the cycle). - `parseRetirementsFromTx(tx)` — public parser exposed so unit tests can feed synthetic tx responses. Walks `logs[].events[]` AND top-level `events[]` for cross-SDK compatibility, filters on the Regen v1 and v1beta1 EventRetire type URLs, and extracts the owner / batch_denom / amount / jurisdiction / reason attributes. Skips malformed events (missing batch_denom, non-finite amount, non-positive amount). The parser is a pure function — no side effects, no LCD calls — so it is fully unit-testable with synthetic inputs. ## New tests (16 total across 2 files) **src/ledger.test.ts** (9 tests) - empty tx - single EventRetire with all attributes - v1beta1 fallback event type - non-retirement events ignored - missing batch_denom ignored - non-finite, zero, and negative amounts ignored - multiple retirements in one tx (batched) - events read from tx.events[] as well as logs[].events[] - tx hash and timestamp carried through to Retirement **src/workflows/retirement-tracking.test.ts** (7 new tests for aggregateRetirementsByClass, on top of the existing 9 for computeDemandIndex) - empty retirements → empty map - single retirement → one-entry summary - multiple retirements grouped by class id - top retiree identified by cumulative quantity, not count - jurisdiction metadata percentage - classes with zero total quantity skipped - empty retiree string handled without crashing Full test suite: 68 passed across 4 files (up from 52 in PR #99). ## What this unlocks The old MVP proxy produced RetirementSummary records with empty `topRetiree` / `topRetireeQuantity` / `pctWithJurisdiction` fields — the supply-delta source couldn't carry retiree identity or compliance metadata. The new implementation populates every field on the Retirement type. Downstream: - Demand index calculations now distinguish "5 unique retirees each retiring 200" from "1 whale retiring 1000". - Jurisdiction-metadata coverage surfaces compliance-driven demand (per the SPEC note: ">50% jurisdiction coverage typically implies compliance-driven demand"). - Top-retiree identification feeds the narrative layer with concrete context instead of a placeholder. - Each Retirement carries its real tx hash and timestamp, so downstream auditing can link back to the exact on-chain event. ## Scope Does NOT touch WF-MM-01 (price anomaly detection) or WF-MM-02 (liquidity monitor). The price oracle integration noted elsewhere in the README is still future work. The LCD-level error handling returns empty arrays on failure — a future follow-up might distinguish transient failures from genuine "no recent retirements". - Lands in: `agent-003-market-monitor/` - Changes: new tx-search client methods + rewritten WF-MM-03 + 16 new tests - Validate: `cd agent-003-market-monitor && npm test && npx tsc --noEmit` ## PR relationship Based on PR #99 (AGENT-003 unit tests) which is based on PR #80 (AGENT-003 initial implementation). If both land first, this PR rebases cleanly. Sibling PR to a forthcoming AGENT-004 real delegation tx-stream follow-up. Refs phase-2/2.2-agentic-workflows.md §WF-MM-03 Refs PR #80's design decision #3 (MVP proxy → real tx-stream) --- agent-003-market-monitor/README.md | 22 +- agent-003-market-monitor/src/ledger.test.ts | 241 ++++++++++++++++++ agent-003-market-monitor/src/ledger.ts | 110 ++++++++ .../src/workflows/retirement-tracking.test.ts | 113 ++++++-- .../src/workflows/retirement-tracking.ts | 219 ++++++++-------- 5 files changed, 571 insertions(+), 134 deletions(-) create mode 100644 agent-003-market-monitor/src/ledger.test.ts diff --git a/agent-003-market-monitor/README.md b/agent-003-market-monitor/README.md index f69c6dc..d885276 100644 --- a/agent-003-market-monitor/README.md +++ b/agent-003-market-monitor/README.md @@ -82,13 +82,21 @@ Thresholds live in `src/config.ts` under `market.*` and mirror the character def 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. +3. **MsgRetire tx-search as retirement source.** WF-MM-03 reads + recent retirement transactions from the Cosmos LCD `tx-search` + endpoint filtered on `message.action='/regen.ecocredit.v1.MsgRetire'`. + Each tx response is parsed into zero or more Retirement records + by harvesting `EventRetire` attributes (owner, batch_denom, amount, + jurisdiction, reason) from either `logs[].events[]` or the + flattened top-level `events[]` — the parser accepts both shapes + for cross-SDK compatibility. Earlier drafts used a batch-supply + delta as an MVP proxy; the current implementation produces + richer Retirement records with retiree identity and jurisdiction + metadata that the supply-delta proxy could not carry. + +4. **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. + +5. **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 diff --git a/agent-003-market-monitor/src/ledger.test.ts b/agent-003-market-monitor/src/ledger.test.ts new file mode 100644 index 0000000..4b33ba4 --- /dev/null +++ b/agent-003-market-monitor/src/ledger.test.ts @@ -0,0 +1,241 @@ +import { describe, it, expect } from "vitest"; +import { LedgerClient } from "./ledger.js"; + +// ============================================================ +// parseRetirementsFromTx — event extraction from tx response +// ============================================================ + +describe("LedgerClient.parseRetirementsFromTx", () => { + const client = new LedgerClient(); + + it("returns empty array for a tx with no events", () => { + const tx = { txhash: "tx-empty", timestamp: "2026-02-18T12:00:00Z", logs: [] }; + const out = client.parseRetirementsFromTx(tx); + expect(out).toEqual([]); + }); + + it("extracts a single retirement from an EventRetire attribute set", () => { + const tx = { + txhash: "tx-single", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { + type: "regen.ecocredit.v1.EventRetire", + attributes: [ + { key: "owner", value: "regen1retiree1" }, + { key: "batch_denom", value: "C01-001-20240101-20241231-001" }, + { key: "amount", value: "1000" }, + { key: "jurisdiction", value: "US-CA" }, + { key: "reason", value: "voluntary offset" }, + ], + }, + ], + }, + ], + }; + const out = client.parseRetirementsFromTx(tx); + expect(out).toHaveLength(1); + expect(out[0]).toMatchObject({ + txHash: "tx-single", + batchDenom: "C01-001-20240101-20241231-001", + classId: "C01", + retiree: "regen1retiree1", + quantity: 1000, + jurisdiction: "US-CA", + reason: "voluntary offset", + }); + }); + + it("accepts the v1beta1 event type as a fallback", () => { + const tx = { + txhash: "tx-beta", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { + type: "regen.ecocredit.v1beta1.EventRetire", + attributes: [ + { key: "owner", value: "regen1retiree2" }, + { key: "batch_denom", value: "BT-001-20240101-20241231-001" }, + { key: "amount", value: "500" }, + ], + }, + ], + }, + ], + }; + const out = client.parseRetirementsFromTx(tx); + expect(out).toHaveLength(1); + expect(out[0]!.classId).toBe("BT"); + expect(out[0]!.quantity).toBe(500); + expect(out[0]!.jurisdiction).toBeNull(); + expect(out[0]!.reason).toBeNull(); + }); + + it("ignores events that are not EventRetire", () => { + const tx = { + txhash: "tx-other", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { + type: "coin_spent", + attributes: [{ key: "amount", value: "100uregen" }], + }, + { + type: "transfer", + attributes: [{ key: "recipient", value: "regen1other" }], + }, + ], + }, + ], + }; + const out = client.parseRetirementsFromTx(tx); + expect(out).toEqual([]); + }); + + it("ignores EventRetire with missing batch_denom", () => { + const tx = { + txhash: "tx-missing-denom", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { + type: "regen.ecocredit.v1.EventRetire", + attributes: [ + { key: "owner", value: "regen1retiree" }, + { key: "amount", value: "1000" }, + ], + }, + ], + }, + ], + }; + const out = client.parseRetirementsFromTx(tx); + expect(out).toEqual([]); + }); + + it("ignores EventRetire with non-finite or non-positive amount", () => { + const tx = { + txhash: "tx-bad-amount", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { + type: "regen.ecocredit.v1.EventRetire", + attributes: [ + { key: "owner", value: "regen1retiree" }, + { key: "batch_denom", value: "C01-001-2024-2024-001" }, + { key: "amount", value: "not-a-number" }, + ], + }, + { + type: "regen.ecocredit.v1.EventRetire", + attributes: [ + { key: "owner", value: "regen1retiree" }, + { key: "batch_denom", value: "C01-001-2024-2024-002" }, + { key: "amount", value: "0" }, + ], + }, + { + type: "regen.ecocredit.v1.EventRetire", + attributes: [ + { key: "owner", value: "regen1retiree" }, + { key: "batch_denom", value: "C01-001-2024-2024-003" }, + { key: "amount", value: "-500" }, + ], + }, + ], + }, + ], + }; + const out = client.parseRetirementsFromTx(tx); + expect(out).toEqual([]); + }); + + it("extracts multiple retirements from a batched tx", () => { + const tx = { + txhash: "tx-batched", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { + type: "regen.ecocredit.v1.EventRetire", + attributes: [ + { key: "owner", value: "regen1retiree1" }, + { key: "batch_denom", value: "C01-001-2024-2024-001" }, + { key: "amount", value: "100" }, + ], + }, + { + type: "regen.ecocredit.v1.EventRetire", + attributes: [ + { key: "owner", value: "regen1retiree2" }, + { key: "batch_denom", value: "C01-002-2024-2024-001" }, + { key: "amount", value: "200" }, + ], + }, + ], + }, + ], + }; + const out = client.parseRetirementsFromTx(tx); + expect(out).toHaveLength(2); + expect(out[0]!.quantity).toBe(100); + expect(out[1]!.quantity).toBe(200); + }); + + it("reads events from tx.events[] as well as logs[].events[]", () => { + // Some LCD versions flatten the events onto the top-level tx + // rather than nesting them under logs[].events. The parser + // harvests both paths. + const tx = { + txhash: "tx-flat", + timestamp: "2026-02-18T12:00:00Z", + events: [ + { + type: "regen.ecocredit.v1.EventRetire", + attributes: [ + { key: "owner", value: "regen1retiree" }, + { key: "batch_denom", value: "C01-001-2024-2024-001" }, + { key: "amount", value: "42" }, + ], + }, + ], + }; + const out = client.parseRetirementsFromTx(tx); + expect(out).toHaveLength(1); + expect(out[0]!.quantity).toBe(42); + }); + + it("carries the tx hash and timestamp through to the Retirement record", () => { + const tx = { + txhash: "ABCDEF1234567890", + timestamp: "2026-03-01T08:30:00Z", + logs: [ + { + events: [ + { + type: "regen.ecocredit.v1.EventRetire", + attributes: [ + { key: "owner", value: "regen1r" }, + { key: "batch_denom", value: "C-1-2024-2024-1" }, + { key: "amount", value: "10" }, + ], + }, + ], + }, + ], + }; + const out = client.parseRetirementsFromTx(tx); + expect(out[0]!.txHash).toBe("ABCDEF1234567890"); + expect(out[0]!.retiredAt).toBe("2026-03-01T08:30:00Z"); + }); +}); diff --git a/agent-003-market-monitor/src/ledger.ts b/agent-003-market-monitor/src/ledger.ts index ee252be..e62f988 100644 --- a/agent-003-market-monitor/src/ledger.ts +++ b/agent-003-market-monitor/src/ledger.ts @@ -4,8 +4,15 @@ import type { CreditBatch, BatchSupply, SellOrder, + Retirement, } from "./types.js"; +/** Extract the class id prefix from a batch denom (shared helper). */ +function classIdFromBatchDenomHelper(denom: string): string { + const idx = denom.indexOf("-"); + return idx > 0 ? denom.slice(0, idx) : denom; +} + /** * Regen Ledger LCD (REST) client — ecocredit marketplace endpoints. * @@ -120,6 +127,109 @@ export class LedgerClient { } } + // ── Tx-search: MsgRetire events ───────────────────────────── + // + // Pulls recent tx responses filtered by `message.action` matching + // the Regen ecocredit MsgRetire type URL. Each matching response + // is parsed into zero or more Retirement records by walking the + // `events` list and extracting the EventRetire attributes. + // + // A single transaction can contain multiple MsgRetire messages + // (batched retirements), so each tx can emit more than one + // Retirement record. + // + // Returns an empty array on any error — the workflow treats + // "no tx results" identically to "no recent retirements", so + // transient LCD failures degrade to zero retirement activity + // rather than crashing the cycle. + + async getRecentRetirementTxs(limit = 100): Promise { + try { + const params = new URLSearchParams(); + params.set("events", "message.action='/regen.ecocredit.v1.MsgRetire'"); + params.set("pagination.limit", String(limit)); + params.set("pagination.reverse", "true"); + params.set("order_by", "ORDER_BY_DESC"); + + const data = await this.get(`/cosmos/tx/v1beta1/txs?${params.toString()}`); + const txResponses = (data.tx_responses || []) as Array>; + + const retirements: Retirement[] = []; + for (const tx of txResponses) { + retirements.push(...this.parseRetirementsFromTx(tx)); + } + return retirements; + } catch { + return []; + } + } + + /** + * Parse a single Cosmos tx_response into zero or more Retirement + * records. Public so the unit tests can feed it synthetic + * responses without hitting a real LCD. + */ + parseRetirementsFromTx(tx: Record): Retirement[] { + const txHash = typeof tx.txhash === "string" ? tx.txhash : ""; + const timestamp = typeof tx.timestamp === "string" ? tx.timestamp : new Date().toISOString(); + const logs = (tx.logs || []) as Array>; + const events: Array> = []; + + // Cosmos LCD responses can carry events at two levels: per-msg + // inside `logs[i].events[]`, and flattened at `tx.events[]`. We + // harvest both for maximum compatibility across SDK versions. + for (const log of logs) { + const logEvents = (log.events || []) as Array>; + events.push(...logEvents); + } + const flatEvents = (tx.events || []) as Array>; + events.push(...flatEvents); + + const results: Retirement[] = []; + for (const ev of events) { + const type = typeof ev.type === "string" ? ev.type : ""; + // Match the Regen v1 EventRetire type or the fallback SDK + // message event. Either path produces the same Retirement + // shape for the downstream workflow. + if ( + type !== "regen.ecocredit.v1.EventRetire" && + type !== "regen.ecocredit.v1beta1.EventRetire" + ) { + continue; + } + + const attributes = (ev.attributes || []) as Array<{ key: string; value: string }>; + const attr = (k: string): string | null => { + const hit = attributes.find((a) => a.key === k); + return hit ? hit.value : null; + }; + + const batchDenom = attr("batch_denom") ?? attr("batchDenom") ?? ""; + if (!batchDenom) continue; + + const quantityRaw = attr("amount") ?? attr("quantity") ?? "0"; + const quantity = Number(quantityRaw); + if (!Number.isFinite(quantity) || quantity <= 0) continue; + + const retiree = attr("owner") ?? attr("retirer") ?? ""; + const jurisdiction = attr("jurisdiction"); + const reason = attr("reason"); + const classId = classIdFromBatchDenomHelper(batchDenom); + + results.push({ + txHash, + batchDenom, + classId, + retiree, + quantity, + jurisdiction, + reason, + retiredAt: timestamp, + }); + } + return results; + } + // ── Connectivity check ──────────────────────────────────── async checkConnection(): Promise<{ blockHeight: string }> { diff --git a/agent-003-market-monitor/src/workflows/retirement-tracking.test.ts b/agent-003-market-monitor/src/workflows/retirement-tracking.test.ts index dec3ccb..f1d583a 100644 --- a/agent-003-market-monitor/src/workflows/retirement-tracking.test.ts +++ b/agent-003-market-monitor/src/workflows/retirement-tracking.test.ts @@ -1,5 +1,9 @@ import { describe, it, expect } from "vitest"; -import { computeDemandIndex } from "./retirement-tracking.js"; +import { + computeDemandIndex, + aggregateRetirementsByClass, +} from "./retirement-tracking.js"; +import type { Retirement } from "../types.js"; // ============================================================ // computeDemandIndex — bounded 0-100 demand signal @@ -7,17 +11,14 @@ import { computeDemandIndex } from "./retirement-tracking.js"; 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); @@ -25,7 +26,6 @@ describe("computeDemandIndex", () => { }); 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); @@ -33,35 +33,118 @@ describe("computeDemandIndex", () => { }); 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); }); }); + +// ============================================================ +// aggregateRetirementsByClass — per-class summary builder +// ============================================================ + +function mkRetirement(overrides: Partial = {}): Retirement { + return { + txHash: "tx-test", + batchDenom: "C01-001-20240101-20241231-001", + classId: "C01", + retiree: "regen1retiree", + quantity: 100, + jurisdiction: null, + reason: null, + retiredAt: "2026-02-18T12:00:00Z", + ...overrides, + }; +} + +describe("aggregateRetirementsByClass", () => { + it("returns an empty map for zero retirements", () => { + const result = aggregateRetirementsByClass([]); + expect(result.size).toBe(0); + }); + + it("aggregates a single retirement into a one-entry summary", () => { + const result = aggregateRetirementsByClass([ + mkRetirement({ quantity: 500 }), + ]); + expect(result.size).toBe(1); + const summary = result.get("C01")!; + expect(summary.retirementCount).toBe(1); + expect(summary.totalQuantity).toBe(500); + expect(summary.uniqueRetirees).toBe(1); + expect(summary.topRetiree).toBe("regen1retiree"); + expect(summary.topRetireeQuantity).toBe(500); + }); + + it("groups multiple retirements by class id", () => { + const result = aggregateRetirementsByClass([ + mkRetirement({ classId: "C01", quantity: 100 }), + mkRetirement({ classId: "C01", quantity: 200, retiree: "regen1other" }), + mkRetirement({ classId: "BT", quantity: 50 }), + ]); + expect(result.size).toBe(2); + expect(result.get("C01")!.totalQuantity).toBe(300); + expect(result.get("C01")!.retirementCount).toBe(2); + expect(result.get("C01")!.uniqueRetirees).toBe(2); + expect(result.get("BT")!.totalQuantity).toBe(50); + expect(result.get("BT")!.retirementCount).toBe(1); + }); + + it("identifies the top retiree by cumulative quantity, not count", () => { + const result = aggregateRetirementsByClass([ + mkRetirement({ quantity: 10, retiree: "regen1spammer" }), + mkRetirement({ quantity: 10, retiree: "regen1spammer" }), + mkRetirement({ quantity: 10, retiree: "regen1spammer" }), + mkRetirement({ quantity: 100, retiree: "regen1whale" }), + ]); + const summary = result.get("C01")!; + expect(summary.uniqueRetirees).toBe(2); + expect(summary.topRetiree).toBe("regen1whale"); + expect(summary.topRetireeQuantity).toBe(100); + }); + + it("computes the jurisdiction metadata percentage", () => { + const result = aggregateRetirementsByClass([ + mkRetirement({ jurisdiction: "US-CA" }), + mkRetirement({ jurisdiction: "US-NY" }), + mkRetirement({ jurisdiction: null }), + mkRetirement({ jurisdiction: null }), + ]); + const summary = result.get("C01")!; + expect(summary.pctWithJurisdiction).toBe(50); + }); + + it("skips classes whose retirements all sum to zero quantity", () => { + const result = aggregateRetirementsByClass([ + mkRetirement({ quantity: 0 }), + ]); + expect(result.size).toBe(0); + }); + + it("handles retirements with empty retiree string without crashing", () => { + const result = aggregateRetirementsByClass([ + mkRetirement({ retiree: "", quantity: 100 }), + ]); + const summary = result.get("C01")!; + expect(summary.totalQuantity).toBe(100); + expect(summary.uniqueRetirees).toBe(0); + expect(summary.topRetiree).toBeNull(); + }); +}); diff --git a/agent-003-market-monitor/src/workflows/retirement-tracking.ts b/agent-003-market-monitor/src/workflows/retirement-tracking.ts index 2f0d8a6..0f80fd8 100644 --- a/agent-003-market-monitor/src/workflows/retirement-tracking.ts +++ b/agent-003-market-monitor/src/workflows/retirement-tracking.ts @@ -4,45 +4,37 @@ 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 type { Retirement, RetirementSummary } from "../types.js"; /** * WF-MM-03: Retirement Pattern Analysis * - * Trigger: MsgRetire event (proxied by per-cycle retired-amount deltas - * until the Ledger MCP provides an event stream) + * Trigger: MsgRetire events (LCD tx-search) * 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. + * Observe — Fetch recent MsgRetire tx responses from the LCD + * tx-search endpoint and parse them into Retirement + * records. Each tx can emit multiple retirements if + * the user batched them. + * Orient — Group retirements by credit class. Aggregate totals, + * unique retirees, jurisdiction-metadata share, top + * retiree, and derive the demand index. * 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. + * Previous versions of this workflow used the per-batch supply + * query as an MVP proxy for retirement activity — comparing + * `retired_amount` across cycles to derive a delta. That worked + * but could not carry retiree identity, jurisdiction, or + * per-event timestamp. The current implementation uses the real + * MsgRetire tx stream via `ledger.getRecentRetirementTxs`, so all + * five fields on the Retirement record are now populated from + * ledger state rather than synthesized. */ -interface BatchWithSupply { - batch: CreditBatch; - supply: BatchSupply; - classId: string; -} - interface Observations { - classes: CreditClass[]; - batchesWithSupply: BatchWithSupply[]; + retirements: Retirement[]; } interface Orientation { @@ -62,11 +54,6 @@ interface Actions { alertsSent: number; } -function classIdFromBatchDenom(denom: string): string { - const idx = denom.indexOf("-"); - return idx > 0 ? denom.slice(0, idx) : denom; -} - /** 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 @@ -82,6 +69,88 @@ export function computeDemandIndex( return Math.round(volumeComponent + countComponent + breadthComponent); } +/** + * Aggregate a list of Retirement records into per-class summaries. + * Exported so unit tests can feed it synthetic input without going + * through the observe phase. + */ +export function aggregateRetirementsByClass( + retirements: Retirement[], + capturedAt: string = new Date().toISOString() +): Map { + const byClass = new Map(); + for (const r of retirements) { + const bucket = byClass.get(r.classId) || []; + bucket.push(r); + byClass.set(r.classId, bucket); + } + + const summariesByClass = new Map(); + + for (const [classId, rows] of byClass) { + if (rows.length === 0) continue; + + let totalQuantity = 0; + const retireeSet = new Set(); + const retireeQuantity = new Map(); + let jurisdictionCount = 0; + + for (const r of rows) { + totalQuantity += r.quantity; + if (r.retiree) { + retireeSet.add(r.retiree); + retireeQuantity.set( + r.retiree, + (retireeQuantity.get(r.retiree) ?? 0) + r.quantity + ); + } + if (r.jurisdiction) jurisdictionCount++; + } + + if (totalQuantity === 0) continue; + + let topRetiree: string | null = null; + let topRetireeQuantity = 0; + for (const [retiree, qty] of retireeQuantity) { + if (qty > topRetireeQuantity) { + topRetiree = retiree; + topRetireeQuantity = qty; + } + } + + const retirementCount = rows.length; + const uniqueRetirees = retireeSet.size; + const pctWithJurisdiction = + retirementCount > 0 ? (jurisdictionCount / retirementCount) * 100 : 0; + + // Treat USD value as 1:1 with quantity for now. Price oracle + // integration is future work — documented as an open question + // in the workflow spec. + const totalValueUsd = totalQuantity; + const demandIndex = computeDemandIndex( + totalQuantity, + retirementCount, + uniqueRetirees + ); + + summariesByClass.set(classId, { + classId, + windowHours: config.market.retirementWindowHours, + retirementCount, + totalQuantity, + totalValueUsd, + uniqueRetirees, + topRetiree, + topRetireeQuantity, + pctWithJurisdiction, + demandIndex, + capturedAt, + }); + } + + return summariesByClass; +} + export function createRetirementTrackingWorkflow( ledger: LedgerClient ): OODAWorkflow { @@ -90,90 +159,16 @@ export function createRetirementTrackingWorkflow( 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); - - const batchesWithSupply: BatchWithSupply[] = []; - await Promise.all( - cappedBatches.map(async (batch) => { - const supply = await ledger.getBatchSupply(batch.denom); - if (supply) { - batchesWithSupply.push({ - batch, - supply, - classId: classIdFromBatchDenom(batch.denom), - }); - } - }) - ); - - return { classes, batchesWithSupply }; + // Pull the most recent retirement transactions from the LCD + // tx-search endpoint. Each tx response can emit multiple + // MsgRetire events (batched retirements) — the ledger client + // flattens them into individual Retirement records. + const retirements = await ledger.getRecentRetirementTxs(200); + return { retirements }; }, 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, - }); - } - + const summariesByClass = aggregateRetirementsByClass(obs.retirements); return { summariesByClass }; }, From 30fb3251cd38348c7786e8ff0856e14fdadb8f9d Mon Sep 17 00:00:00 2001 From: brawlaphant Date: Sat, 11 Apr 2026 14:47:25 -0700 Subject: [PATCH 4/7] fix(agent-003): address Gemini review on real MsgRetire tx-stream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Gemini review feedback on PR #103: ledger.getRecentRetirementTxs: * Query both v1 and v1beta1 MsgRetire type URLs instead of only v1. Older forwarders still emit v1beta1 and the parser already handles both event shapes — the tx-search filter was the only thing restricting the stream. Dedupe by tx hash so a tx appearing under both queries is only processed once. * Replace the empty catch with console.error so network issues are visible instead of silently degrading to "no recent retirements". parseRetirementsFromTx: * Prefer the flattened `tx.events[]` list over walking `tx.logs[i].events[]`. In modern Cosmos SDK versions the flat list is a superset, so walking both double-counted every event. Fall back to the per-log list only when the flat list is empty (very old LCD builds). * Parse the raw integer amount via BigInt first so higher-precision credit types (18-decimal biodiversity tokens on the roadmap) do not lose precision at the boundary. The downstream Retirement record still uses `number` for ergonomics — a safe downcast at current 6-decimal precision — with a comment noting the boundary. aggregateRetirementsByClass / WF-MM-03 orient: * USD value for retirement volume now uses the most recent WF-MM-02 median ask price per class (via a new store.getLatestMedianAsk helper and a `classPriceUsd` map threaded into the aggregator). Falls back to 1:1 USD when no snapshot exists yet. The aggregator's new optional `classPriceUsd` parameter keeps the unit tests' 1:1 assumption backwards-compatible. Full vitest suite: 68/68 passing. Typecheck clean. Co-Authored-By: Claude Opus 4.6 (1M context) --- agent-003-market-monitor/src/ledger.ts | 99 ++++++++++++++----- agent-003-market-monitor/src/store.ts | 21 ++++ .../src/workflows/retirement-tracking.ts | 36 +++++-- 3 files changed, 124 insertions(+), 32 deletions(-) diff --git a/agent-003-market-monitor/src/ledger.ts b/agent-003-market-monitor/src/ledger.ts index e62f988..7106fe5 100644 --- a/agent-003-market-monitor/src/ledger.ts +++ b/agent-003-market-monitor/src/ledger.ts @@ -144,24 +144,45 @@ export class LedgerClient { // rather than crashing the cycle. async getRecentRetirementTxs(limit = 100): Promise { - try { - const params = new URLSearchParams(); - params.set("events", "message.action='/regen.ecocredit.v1.MsgRetire'"); - params.set("pagination.limit", String(limit)); - params.set("pagination.reverse", "true"); - params.set("order_by", "ORDER_BY_DESC"); - - const data = await this.get(`/cosmos/tx/v1beta1/txs?${params.toString()}`); - const txResponses = (data.tx_responses || []) as Array>; - - const retirements: Retirement[] = []; - for (const tx of txResponses) { - retirements.push(...this.parseRetirementsFromTx(tx)); + // Query both the v1 and v1beta1 MsgRetire type URLs — the SDK + // has carried both for years and registries still emit v1beta1 + // for older code paths. The parser below accepts either event + // type. Dedup by tx hash so the same tx appearing under both + // queries is only processed once. + const typeUrls = [ + "/regen.ecocredit.v1.MsgRetire", + "/regen.ecocredit.v1beta1.MsgRetire", + ]; + + const seenHashes = new Set(); + const retirements: Retirement[] = []; + + for (const typeUrl of typeUrls) { + try { + const params = new URLSearchParams(); + params.set("events", `message.action='${typeUrl}'`); + params.set("pagination.limit", String(limit)); + params.set("pagination.reverse", "true"); + params.set("order_by", "ORDER_BY_DESC"); + + const data = await this.get(`/cosmos/tx/v1beta1/txs?${params.toString()}`); + const txResponses = (data.tx_responses || []) as Array>; + + for (const tx of txResponses) { + const hash = typeof tx.txhash === "string" ? tx.txhash : ""; + if (hash && seenHashes.has(hash)) continue; + if (hash) seenHashes.add(hash); + retirements.push(...this.parseRetirementsFromTx(tx)); + } + } catch (err) { + console.error( + `LedgerClient.getRecentRetirementTxs(${typeUrl}) failed:`, + err + ); } - return retirements; - } catch { - return []; } + + return retirements; } /** @@ -176,14 +197,21 @@ export class LedgerClient { const events: Array> = []; // Cosmos LCD responses can carry events at two levels: per-msg - // inside `logs[i].events[]`, and flattened at `tx.events[]`. We - // harvest both for maximum compatibility across SDK versions. - for (const log of logs) { - const logEvents = (log.events || []) as Array>; - events.push(...logEvents); - } + // inside `logs[i].events[]`, and flattened at `tx.events[]`. + // In modern Cosmos SDK versions the flat list is a superset of + // everything in the logs, so walking both paths double-counts + // every event. Prefer the flat list if it's present and non-empty, + // and fall back to the per-log list only when the flat list is + // missing or empty (very old LCD builds). const flatEvents = (tx.events || []) as Array>; - events.push(...flatEvents); + if (flatEvents.length > 0) { + events.push(...flatEvents); + } else { + for (const log of logs) { + const logEvents = (log.events || []) as Array>; + events.push(...logEvents); + } + } const results: Retirement[] = []; for (const ev of events) { @@ -207,9 +235,28 @@ export class LedgerClient { const batchDenom = attr("batch_denom") ?? attr("batchDenom") ?? ""; if (!batchDenom) continue; - const quantityRaw = attr("amount") ?? attr("quantity") ?? "0"; - const quantity = Number(quantityRaw); - if (!Number.isFinite(quantity) || quantity <= 0) continue; + // Parse raw integer units as BigInt first to preserve precision + // beyond Number.MAX_SAFE_INTEGER. Regen batches are 6-decimal + // today (capped well below 2^53), but higher-precision credit + // types (18-decimal biodiversity tokens etc.) are on the roadmap + // and the parser should not lose precision at the boundary. + const quantityRaw = (attr("amount") ?? attr("quantity") ?? "0").trim(); + let quantityBig: bigint; + try { + // Strip any decimal suffix — the on-chain event always emits + // the smallest-unit integer, but some forwarders prettify it. + const intPart = quantityRaw.split(".")[0] || "0"; + quantityBig = BigInt(intPart); + } catch { + continue; + } + if (quantityBig <= 0n) continue; + // The downstream Retirement record uses `number` for ergonomics. + // For current Regen credit precision this is a safe downcast; + // future higher-precision assets should lift the whole field to + // string/bigint in types.ts. + const quantity = Number(quantityBig); + if (!Number.isFinite(quantity)) continue; const retiree = attr("owner") ?? attr("retirer") ?? ""; const jurisdiction = attr("jurisdiction"); diff --git a/agent-003-market-monitor/src/store.ts b/agent-003-market-monitor/src/store.ts index 3db460a..e52d6a0 100644 --- a/agent-003-market-monitor/src/store.ts +++ b/agent-003-market-monitor/src/store.ts @@ -202,6 +202,27 @@ class Store { return row || null; } + /** Latest median ask price for a class, pulled from the most recent + * liquidity snapshot. Used by WF-MM-03 to value retirement volume + * in USD rather than treating quantity as 1:1 with USD. Returns + * null when no snapshot exists yet. */ + getLatestMedianAsk(classId: string): number | null { + const row = this.db + .prepare( + `SELECT snapshot FROM liquidity_snapshots + WHERE class_id = ? ORDER BY id DESC LIMIT 1` + ) + .get(classId) as { snapshot: string } | undefined; + if (!row) return null; + try { + const parsed = JSON.parse(row.snapshot) as { medianAskUsd?: number }; + const m = parsed.medianAskUsd; + return typeof m === "number" && m > 0 ? m : null; + } catch { + return null; + } + } + // ── Retirement summaries ─────────────────────────────────── saveRetirementSummary(row: { diff --git a/agent-003-market-monitor/src/workflows/retirement-tracking.ts b/agent-003-market-monitor/src/workflows/retirement-tracking.ts index 0f80fd8..0dbb97a 100644 --- a/agent-003-market-monitor/src/workflows/retirement-tracking.ts +++ b/agent-003-market-monitor/src/workflows/retirement-tracking.ts @@ -73,10 +73,18 @@ export function computeDemandIndex( * Aggregate a list of Retirement records into per-class summaries. * Exported so unit tests can feed it synthetic input without going * through the observe phase. + * + * `classPriceUsd` provides a per-class USD reference price (dollars + * per credit) that valueing the retirement volume relies on. In + * production the prices come from the most recent WF-MM-02 median ask + * snapshot; tests inject a Map directly. Classes missing from the + * map fall back to 1:1 USD, which preserves the prior behavior and + * documents the "no price oracle yet" state. */ export function aggregateRetirementsByClass( retirements: Retirement[], - capturedAt: string = new Date().toISOString() + capturedAt: string = new Date().toISOString(), + classPriceUsd: Map = new Map() ): Map { const byClass = new Map(); for (const r of retirements) { @@ -123,10 +131,12 @@ export function aggregateRetirementsByClass( const pctWithJurisdiction = retirementCount > 0 ? (jurisdictionCount / retirementCount) * 100 : 0; - // Treat USD value as 1:1 with quantity for now. Price oracle - // integration is future work — documented as an open question - // in the workflow spec. - const totalValueUsd = totalQuantity; + // Value retirement volume using the most recent median ask + // price from WF-MM-02 for this class when available. Falls back + // to 1:1 USD when no snapshot exists yet — this keeps the field + // populated without pretending there's a price we don't have. + const unitPriceUsd = classPriceUsd.get(classId) ?? 1; + const totalValueUsd = totalQuantity * unitPriceUsd; const demandIndex = computeDemandIndex( totalQuantity, retirementCount, @@ -168,7 +178,21 @@ export function createRetirementTrackingWorkflow( }, async orient(obs: Observations): Promise { - const summariesByClass = aggregateRetirementsByClass(obs.retirements); + // Build the per-class price map from the latest liquidity + // snapshots so aggregation can value retirement volume without + // duplicating the ask-price parsing logic. + const classPriceUsd = new Map(); + const distinctClassIds = new Set(obs.retirements.map((r) => r.classId)); + for (const classId of distinctClassIds) { + const price = store.getLatestMedianAsk(classId); + if (price !== null) classPriceUsd.set(classId, price); + } + + const summariesByClass = aggregateRetirementsByClass( + obs.retirements, + undefined, + classPriceUsd + ); return { summariesByClass }; }, From 8c75610b2df6e1c2c43453fe52d325f41a0b22a3 Mon Sep 17 00:00:00 2001 From: brawlaphant Date: Sun, 19 Apr 2026 17:27:46 -0700 Subject: [PATCH 5/7] fix(agent-003): use mean in Z-score numerator to match stddev denominator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Z-score numerator has to pair with its denominator's center. stddev is measured around the mean, so subtracting the median while dividing by stddev silently biases every score by (mean − median). On a skewed price distribution that bias is large enough to flip anomaly severity. Keep the median on the anomaly record as the human-readable reference price — it reads more intuitively than mean for thin markets — but compute the statistical test with the mean. Addresses Gemini review on feat/agent-003-real-retire-tx-stream: - price-anomaly-detection.ts:169 (class Z-score) - price-anomaly-detection.ts:178 (batch Z-score) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/workflows/price-anomaly-detection.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts b/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts index 509b8e2..79a4a58 100644 --- a/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts +++ b/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts @@ -163,10 +163,20 @@ export function createPriceAnomalyDetectionWorkflow( if (classSamples.length < config.market.minSamples) continue; + // Z-score numerator must pair with its denominator's center: + // stddev is measured around the mean, so the Z-score has to + // subtract the mean, not the median. Mixing median with stddev + // silently biases every score by (mean − median), which on a + // skewed price distribution can flip anomaly severity. + // + // We still keep the median around as the human-readable + // reference price on the anomaly record — median reads more + // intuitively than mean for a thin market — but it is not used + // in the statistical test. const classMedian = median(classSamples); const classMean = classSamples.reduce((a, b) => a + b, 0) / classSamples.length; const classStd = stddev(classSamples, classMean); - const zClass = classStd > 0 ? (trade.pricePerCredit - classMedian) / classStd : 0; + const zClass = classStd > 0 ? (trade.pricePerCredit - classMean) / classStd : 0; const batchMedian = batchSamples.length > 0 ? median(batchSamples) : classMedian; const batchMean = @@ -174,7 +184,7 @@ export function createPriceAnomalyDetectionWorkflow( ? 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 - batchMedian) / batchStd : 0; + const zBatch = batchStd > 0 ? (trade.pricePerCredit - batchMean) / batchStd : 0; const severity = classifyAnomaly(zClass, zBatch); if (severity === "INFO") continue; From 63113f7a6b878b72dcf2b0c24c8622b7e8969458 Mon Sep 17 00:00:00 2001 From: brawlaphant Date: Sun, 19 Apr 2026 17:36:56 -0700 Subject: [PATCH 6/7] fix(agent-003): port forward remaining gemini fixes from #80 onto #103 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The real MsgRetire tx-stream branch (this PR) was cut from agent-003's pre-fixup state, so it was missing the four non-z-score fixes from #80's gemini-review commit (5d04ae6). Bringing them forward so #80 can be closed as fully superseded: - `LedgerClient.getCreditClasses` — walks every pagination page up to a 25-page safety cap (was silently truncating to oldest 200 classes; new classes were invisible to every workflow). - `liquidity-monitor` — top-10 depth sort pre-computes askUsd once per order instead of recomputing it in each comparator call. Also filters non-USD-stablecoin asks so arbitrary-denominated orders don't pollute class/batch baselines. - `index.ts` main loop — `setInterval` → recursive `setTimeout` so a slow cycle can't overlap with the next tick (no race conditions or database contention when the LCD/LLM is slow). - Z-score comment refreshed; the `-- mean not median --` fix from 8c75610 is kept as the authoritative version. The #80 retirement-delta work (batch-supply-delta + per-batch cumulative state) is intentionally **not** ported — this PR's tx-stream approach supersedes it wholesale and is exact where the delta method was approximate. The now-unused `batch_retirement_state` table and its `getPreviousRetiredAmount`/`upsertRetiredAmount` helpers are removed in the same commit. Co-Authored-By: Claude Opus 4.7 (1M context) --- agent-003-market-monitor/src/index.ts | 26 ++++++++++---- agent-003-market-monitor/src/ledger.ts | 34 +++++++++++++++---- .../src/workflows/liquidity-monitor.ts | 18 +++++----- .../src/workflows/price-anomaly-detection.ts | 12 +++---- .../src/workflows/retirement-tracking.ts | 6 ++++ 5 files changed, 69 insertions(+), 27 deletions(-) diff --git a/agent-003-market-monitor/src/index.ts b/agent-003-market-monitor/src/index.ts index 213fe1a..675b3ae 100644 --- a/agent-003-market-monitor/src/index.ts +++ b/agent-003-market-monitor/src/index.ts @@ -74,17 +74,29 @@ async function main() { if (runOnce) { await runCycle(ledger); } else { - await runCycle(ledger); + // Recursive setTimeout rather than setInterval so a slow cycle + // never overlaps with the next tick. If a cycle takes longer than + // pollIntervalMs, the next tick simply starts late — we never have + // two runCycle invocations sharing the database in parallel. + let timeoutId: NodeJS.Timeout | null = null; + let stopping = false; + + const runNext = () => { + runCycle(ledger) + .catch((err) => console.error(`Cycle failed:`, err)) + .finally(() => { + if (!stopping) { + timeoutId = setTimeout(runNext, config.pollIntervalMs); + } + }); + }; - const interval = setInterval(() => { - runCycle(ledger).catch((err) => - console.error(`Cycle failed:`, err) - ); - }, config.pollIntervalMs); + runNext(); const shutdown = () => { console.log("\nShutting down gracefully..."); - clearInterval(interval); + stopping = true; + if (timeoutId !== null) clearTimeout(timeoutId); store.close(); process.exit(0); }; diff --git a/agent-003-market-monitor/src/ledger.ts b/agent-003-market-monitor/src/ledger.ts index 7106fe5..38da0f8 100644 --- a/agent-003-market-monitor/src/ledger.ts +++ b/agent-003-market-monitor/src/ledger.ts @@ -31,13 +31,35 @@ export class LedgerClient { // ── Credit Classes ───────────────────────────────────────── async getCreditClasses(): Promise { - const params = new URLSearchParams(); - params.set("pagination.limit", "200"); + // Walk every page of the `/regen/ecocredit/v1/classes` endpoint so + // the agent keeps monitoring new credit classes as the registry + // grows. A hardcoded limit of 200 would silently drop classes + // created after that ceiling, which is exactly the kind of blind + // spot this agent is supposed to prevent. + const pageSize = 200; + const classes: CreditClass[] = []; + let nextKey: string | null = null; + // Cap total pages as a safety belt against runaway pagination. + const MAX_PAGES = 25; + for (let i = 0; i < MAX_PAGES; i++) { + const params = new URLSearchParams(); + params.set("pagination.limit", String(pageSize)); + if (nextKey) params.set("pagination.key", nextKey); - const data = await this.get( - `/regen/ecocredit/v1/classes?${params.toString()}` - ); - return (data.classes || []) as CreditClass[]; + const data = await this.get( + `/regen/ecocredit/v1/classes?${params.toString()}` + ); + const pageClasses = (data.classes || []) as CreditClass[]; + classes.push(...pageClasses); + + const pagination = data.pagination as + | { next_key?: string | null } + | undefined; + const rawKey = pagination?.next_key; + if (!rawKey) break; + nextKey = rawKey; + } + return classes; } async getCreditClass(classId: string): Promise { diff --git a/agent-003-market-monitor/src/workflows/liquidity-monitor.ts b/agent-003-market-monitor/src/workflows/liquidity-monitor.ts index d782065..3b5a8ef 100644 --- a/agent-003-market-monitor/src/workflows/liquidity-monitor.ts +++ b/agent-003-market-monitor/src/workflows/liquidity-monitor.ts @@ -143,14 +143,16 @@ export function createLiquidityMonitorWorkflow( }); const totalListedValueUsd = listedValues.reduce((a, b) => a + b, 0); - const sortedByPrice = orders - .slice() - .sort((a, b) => askUsd(a) - askUsd(b)); - const top10 = sortedByPrice.slice(0, 10); - const depthUsd = top10.reduce((acc, o) => { - const p = askUsd(o); - const q = Number(o.quantity); - return acc + (Number.isFinite(p) && Number.isFinite(q) ? p * q : 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]!; diff --git a/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts b/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts index 79a4a58..ebadd53 100644 --- a/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts +++ b/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts @@ -61,12 +61,12 @@ function sellOrderToTrade(order: SellOrder, classId: string): Trade | null { const ask = Number(order.ask_amount); if (!Number.isFinite(qty) || !Number.isFinite(ask) || qty <= 0) return null; - // ask_amount is in the smallest unit of ask_denom. For the MVP we - // treat it as already-USD when denom is a USD stablecoin and pass a - // 1.0 conversion otherwise. Downstream code can plug in a real - // price oracle. - const usdPerAskUnit = isUsdStableDenom(order.ask_denom) ? 1 : 1; - const pricePerCredit = (ask * usdPerAskUnit) / qty; + // 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, diff --git a/agent-003-market-monitor/src/workflows/retirement-tracking.ts b/agent-003-market-monitor/src/workflows/retirement-tracking.ts index 0dbb97a..c1b899a 100644 --- a/agent-003-market-monitor/src/workflows/retirement-tracking.ts +++ b/agent-003-market-monitor/src/workflows/retirement-tracking.ts @@ -173,6 +173,12 @@ export function createRetirementTrackingWorkflow( // tx-search endpoint. Each tx response can emit multiple // MsgRetire events (batched retirements) — the ledger client // flattens them into individual Retirement records. + // + // The tx-stream replaces the older batch-supply-delta approach + // wholesale (cap 100 batches + concurrency-5 fan-out + per-batch + // stored cumulative deltas), so that code is intentionally not + // preserved here. Event-sourcing is exact where the delta method + // was approximate. const retirements = await ledger.getRecentRetirementTxs(200); return { retirements }; }, From 71ea8fd444cb26b82cdb6e996ac740fbf910f138 Mon Sep 17 00:00:00 2001 From: brawlaphant Date: Sat, 11 Apr 2026 14:43:29 -0700 Subject: [PATCH 7/7] refactor(agent-003): extract shared helpers + log ledger errors + perf/concurrency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Gemini review feedback on PR #99: * Extract the duplicated helpers (median, stddev, classIdFromBatchDenom, isUsdStableDenom, computeDemandIndex) into a shared src/utils.ts. Workflow files now import from utils and re-export for backwards-compatible test imports. * ledger.ts: every empty catch block now logs via console.error with the method name + failing argument so a network hiccup or 5xx no longer silently degrades the agent's data. * liquidity-monitor.ts: the top-10 depth sort pre-computes askUsd once per order instead of recomputing the ratio in every comparator call. * retirement-tracking.ts: batch supply fan-out is chunked at concurrency 5 instead of firing 100 parallel LCD requests that would trip rate limits on public endpoints. * price-anomaly-detection.ts: z-score now uses mean/stddev instead of mixing median numerator with stddev denominator, and sellOrderToTrade skips non-USD-stablecoin asks instead of treating them as 1:1 USD. * retirement-tracking.test.ts now imports computeDemandIndex directly from utils.ts so the test does not transitively construct the SQLite-backed Store singleton — previously the test was failing with "database is locked" when run alongside the other workflow tests. Full vitest suite: 52/52 passing. Typecheck clean. Note: the PR #99 Gemini review flagged `claude-sonnet-4-5-20250929` as a "future date typo" in the config default — that is a false positive, the 2025-09-29 sonnet release is real and ships today, so no change is needed there. Co-Authored-By: Claude Opus 4.6 (1M context) --- agent-003-market-monitor/src/ledger.ts | 21 +++-- agent-003-market-monitor/src/utils.ts | 78 +++++++++++++++++++ .../src/workflows/liquidity-monitor.ts | 15 +--- .../src/workflows/price-anomaly-detection.ts | 37 +++------ .../src/workflows/retirement-tracking.test.ts | 10 ++- .../src/workflows/retirement-tracking.ts | 16 +--- 6 files changed, 111 insertions(+), 66 deletions(-) create mode 100644 agent-003-market-monitor/src/utils.ts diff --git a/agent-003-market-monitor/src/ledger.ts b/agent-003-market-monitor/src/ledger.ts index 38da0f8..2568608 100644 --- a/agent-003-market-monitor/src/ledger.ts +++ b/agent-003-market-monitor/src/ledger.ts @@ -68,7 +68,8 @@ export class LedgerClient { `/regen/ecocredit/v1/classes/${classId}` ); return (data.class || null) as CreditClass | null; - } catch { + } catch (err) { + console.error(`LedgerClient.getCreditClass(${classId}) failed:`, err); return null; } } @@ -92,7 +93,8 @@ export class LedgerClient { `/regen/ecocredit/v1/batches/${denom}` ); return (data.batch || null) as CreditBatch | null; - } catch { + } catch (err) { + console.error(`LedgerClient.getCreditBatch(${denom}) failed:`, err); return null; } } @@ -103,7 +105,8 @@ export class LedgerClient { `/regen/ecocredit/v1/batches/${denom}/supply` ); return (data.supply || null) as BatchSupply | null; - } catch { + } catch (err) { + console.error(`LedgerClient.getBatchSupply(${denom}) failed:`, err); return null; } } @@ -119,7 +122,8 @@ export class LedgerClient { `/regen/ecocredit/marketplace/v1/sell-orders?${params.toString()}` ); return (data.sell_orders || []) as SellOrder[]; - } catch { + } catch (err) { + console.error(`LedgerClient.getSellOrders(limit=${limit}) failed:`, err); return []; } } @@ -133,7 +137,11 @@ export class LedgerClient { `/regen/ecocredit/marketplace/v1/sell-orders/batch/${denom}?${params.toString()}` ); return (data.sell_orders || []) as SellOrder[]; - } catch { + } catch (err) { + console.error( + `LedgerClient.getSellOrdersByBatch(${denom}) failed:`, + err + ); return []; } } @@ -144,7 +152,8 @@ export class LedgerClient { `/regen/ecocredit/marketplace/v1/sell-orders/${orderId}` ); return (data.sell_order || null) as SellOrder | null; - } catch { + } catch (err) { + console.error(`LedgerClient.getSellOrder(${orderId}) failed:`, err); return null; } } 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.ts b/agent-003-market-monitor/src/workflows/liquidity-monitor.ts index 3b5a8ef..0eb680e 100644 --- a/agent-003-market-monitor/src/workflows/liquidity-monitor.ts +++ b/agent-003-market-monitor/src/workflows/liquidity-monitor.ts @@ -10,6 +10,7 @@ import type { CreditClass, AlertLevel, } from "../types.js"; +import { median, classIdFromBatchDenom } from "../utils.js"; /** * WF-MM-02: Liquidity Monitoring & Reporting @@ -57,15 +58,6 @@ export function askUsd(order: SellOrder): number { return ask / qty; } -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]!; -} - export function scoreHealth( depthUsd: number, sellOrderCount: number @@ -86,11 +78,6 @@ export function scoreHealth( return { score, tier }; } -function classIdFromBatchDenom(denom: string): string { - const idx = denom.indexOf("-"); - return idx > 0 ? denom.slice(0, idx) : denom; -} - export function createLiquidityMonitorWorkflow( ledger: LedgerClient ): OODAWorkflow { diff --git a/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts b/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts index ebadd53..2bf5430 100644 --- a/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts +++ b/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts @@ -10,6 +10,16 @@ import type { 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 @@ -81,26 +91,6 @@ function sellOrderToTrade(order: SellOrder, classId: string): Trade | null { }; } -export function isUsdStableDenom(denom: string): boolean { - const d = denom.toLowerCase(); - return d.includes("usdc") || d.includes("usdt") || d.includes("dai"); -} - -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]!; -} - -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)); -} - export function classifyAnomaly( zClass: number, zBatch: number @@ -111,13 +101,6 @@ export function classifyAnomaly( return "INFO"; } -export function classIdFromBatchDenom(denom: string): string { - // Regen batch denoms look like C01-001-20240101-20241231-001. - // The class id is the leading token before the first dash. - const idx = denom.indexOf("-"); - return idx > 0 ? denom.slice(0, idx) : denom; -} - export function createPriceAnomalyDetectionWorkflow( ledger: LedgerClient ): OODAWorkflow { diff --git a/agent-003-market-monitor/src/workflows/retirement-tracking.test.ts b/agent-003-market-monitor/src/workflows/retirement-tracking.test.ts index f1d583a..05e1008 100644 --- a/agent-003-market-monitor/src/workflows/retirement-tracking.test.ts +++ b/agent-003-market-monitor/src/workflows/retirement-tracking.test.ts @@ -1,8 +1,10 @@ import { describe, it, expect } from "vitest"; -import { - computeDemandIndex, - aggregateRetirementsByClass, -} from "./retirement-tracking.js"; +// Import computeDemandIndex from utils so the test does not +// transitively pull in the workflow module, which constructs the +// SQLite-backed Store singleton at module load. aggregateRetirementsByClass +// stays in retirement-tracking — only used by tests that already exercise it. +import { computeDemandIndex } from "../utils.js"; +import { aggregateRetirementsByClass } from "./retirement-tracking.js"; import type { Retirement } from "../types.js"; // ============================================================ diff --git a/agent-003-market-monitor/src/workflows/retirement-tracking.ts b/agent-003-market-monitor/src/workflows/retirement-tracking.ts index c1b899a..4124310 100644 --- a/agent-003-market-monitor/src/workflows/retirement-tracking.ts +++ b/agent-003-market-monitor/src/workflows/retirement-tracking.ts @@ -5,6 +5,7 @@ import { describeRetirementSummary } from "../monitor.js"; import { output } from "../output.js"; import type { OODAWorkflow } from "../ooda.js"; import type { Retirement, RetirementSummary } from "../types.js"; +import { classIdFromBatchDenom, computeDemandIndex } from "../utils.js"; /** * WF-MM-03: Retirement Pattern Analysis @@ -54,21 +55,6 @@ interface Actions { alertsSent: number; } -/** 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. */ -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); -} - /** * Aggregate a list of Retirement records into per-class summaries. * Exported so unit tests can feed it synthetic input without going