diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b09c30e..7a9c03f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,6 +79,14 @@ jobs: working-directory: agent-002-governance-analyst run: npx tsc --noEmit + - name: Install agent-003-market-monitor/ dependencies + working-directory: agent-003-market-monitor + run: npm ci + + - name: Typecheck agent-003-market-monitor/ + working-directory: agent-003-market-monitor + run: npx tsc --noEmit + # ── Spec verification: schemas, reference impls, datasets ─────────── verify-specs: name: Verify Specs diff --git a/agent-003-market-monitor/.env.example b/agent-003-market-monitor/.env.example new file mode 100644 index 0000000..f7da725 --- /dev/null +++ b/agent-003-market-monitor/.env.example @@ -0,0 +1,14 @@ +# Required: Anthropic API key for Claude-powered narrative reports +ANTHROPIC_API_KEY=sk-ant-... + +# Regen LCD endpoint (defaults to public endpoint if not set) +REGEN_LCD_URL=https://regen.api.chandrastation.com + +# Optional: Discord webhook for posting market reports and alerts +DISCORD_WEBHOOK_URL= + +# Optional: polling interval in seconds (default: 300 = 5 minutes) +POLL_INTERVAL_SECONDS=300 + +# Optional: Claude model to use (default: claude-sonnet-4-5-20250929) +ANTHROPIC_MODEL=claude-sonnet-4-5-20250929 diff --git a/agent-003-market-monitor/.gitignore b/agent-003-market-monitor/.gitignore new file mode 100644 index 0000000..467cd39 --- /dev/null +++ b/agent-003-market-monitor/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +*.db +*.db-shm +*.db-wal +.env diff --git a/agent-003-market-monitor/README.md b/agent-003-market-monitor/README.md new file mode 100644 index 0000000..d885276 --- /dev/null +++ b/agent-003-market-monitor/README.md @@ -0,0 +1,111 @@ +# 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. **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 + +This agent operates at **Layer 1 only**: + +- Read-only access to on-chain state +- Cannot submit proposals +- Cannot create, modify, or cancel sell orders +- Cannot execute transactions +- Informational output only + +This matches the framework's principle of starting with the lowest-risk, highest-value capability. Raising the automation layer is a separate governance decision. diff --git a/agent-003-market-monitor/package-lock.json b/agent-003-market-monitor/package-lock.json new file mode 100644 index 0000000..fda4fd4 --- /dev/null +++ b/agent-003-market-monitor/package-lock.json @@ -0,0 +1,2860 @@ +{ + "name": "@regen/agent-003-market-monitor", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@regen/agent-003-market-monitor", + "version": "0.1.0", + "dependencies": { + "@anthropic-ai/sdk": "^0.39.0", + "better-sqlite3": "^11.7.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.12", + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0", + "vitest": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.39.0.tgz", + "integrity": "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg==", + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/agent-003-market-monitor/package.json b/agent-003-market-monitor/package.json new file mode 100644 index 0000000..6f5a4af --- /dev/null +++ b/agent-003-market-monitor/package.json @@ -0,0 +1,30 @@ +{ + "name": "@regen/agent-003-market-monitor", + "version": "0.1.0", + "description": "Regen Network Market Monitor Agent — Layer 1 read-only price anomaly detection, liquidity monitoring, and retirement tracking", + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node --import tsx src/index.ts", + "dev": "node --import tsx --watch src/index.ts", + "analyze": "node --import tsx src/index.ts --once", + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@anthropic-ai/sdk": "^0.39.0", + "better-sqlite3": "^11.7.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.12", + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0", + "vitest": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/agent-003-market-monitor/src/config.ts b/agent-003-market-monitor/src/config.ts new file mode 100644 index 0000000..b65c526 --- /dev/null +++ b/agent-003-market-monitor/src/config.ts @@ -0,0 +1,51 @@ +export const config = { + // Regen LCD endpoint + lcdUrl: process.env.REGEN_LCD_URL || "https://regen.api.chandrastation.com", + + // Anthropic + anthropicApiKey: process.env.ANTHROPIC_API_KEY || "", + model: process.env.ANTHROPIC_MODEL || "claude-sonnet-4-5-20250929", + + // Discord webhook (optional) + discordWebhookUrl: process.env.DISCORD_WEBHOOK_URL || "", + + // Polling — market monitor runs faster than governance analyst + // because trade/retirement events move quickly. + pollIntervalMs: (parseInt(process.env.POLL_INTERVAL_SECONDS || "300", 10)) * 1000, + + // Market monitor thresholds. These mirror the character thresholds in + // agents/packages/agents/src/characters/market-monitor.ts so downstream + // tooling has a single source of truth. Keep them in sync if either + // file changes. + market: { + /** z-score at or above which anomaly severity becomes CRITICAL */ + criticalZScore: 3.5, + /** z-score at which anomaly is flagged for watchlist (WARNING) */ + warningZScore: 2.0, + /** minimum sample size before computing a z-score */ + minSamples: 5, + /** trailing window for class median (days) */ + classMedianWindowDays: 30, + /** trailing window for batch median (days) */ + batchMedianWindowDays: 30, + /** liquidity depth threshold (USD) below which health is DEGRADED */ + liquidityDepthFloor: 5_000, + /** large trade volume (USD) that triggers an off-cycle liquidity report */ + largeTradeVolumeUsd: 10_000, + /** retirement tx window (hours) for demand signal extraction */ + retirementWindowHours: 168, // 7 days + }, + + // Agent identity + agentId: "AGENT-003", + agentName: "RegenMarketMonitor", + governanceLayer: 1 as const, +} as const; + +export function validateConfig(): void { + if (!config.anthropicApiKey) { + throw new Error( + "ANTHROPIC_API_KEY is required. Copy .env.example to .env and set it." + ); + } +} diff --git a/agent-003-market-monitor/src/index.ts b/agent-003-market-monitor/src/index.ts new file mode 100644 index 0000000..675b3ae --- /dev/null +++ b/agent-003-market-monitor/src/index.ts @@ -0,0 +1,114 @@ +#!/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 { + // 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); + } + }); + }; + + runNext(); + + const shutdown = () => { + console.log("\nShutting down gracefully..."); + stopping = true; + if (timeoutId !== null) clearTimeout(timeoutId); + 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.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 new file mode 100644 index 0000000..2568608 --- /dev/null +++ b/agent-003-market-monitor/src/ledger.ts @@ -0,0 +1,336 @@ +import { config } from "./config.js"; +import type { + CreditClass, + 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. + * + * 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 { + // 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()}` + ); + 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 { + try { + const data = await this.get( + `/regen/ecocredit/v1/classes/${classId}` + ); + return (data.class || null) as CreditClass | null; + } catch (err) { + console.error(`LedgerClient.getCreditClass(${classId}) failed:`, err); + return null; + } + } + + // ── Credit Batches ───────────────────────────────────────── + + async getCreditBatches(): Promise { + const params = new URLSearchParams(); + params.set("pagination.limit", "200"); + params.set("pagination.reverse", "true"); + + const data = await this.get( + `/regen/ecocredit/v1/batches?${params.toString()}` + ); + return (data.batches || []) as CreditBatch[]; + } + + async getCreditBatch(denom: string): Promise { + try { + const data = await this.get( + `/regen/ecocredit/v1/batches/${denom}` + ); + return (data.batch || null) as CreditBatch | null; + } catch (err) { + console.error(`LedgerClient.getCreditBatch(${denom}) failed:`, err); + return null; + } + } + + async getBatchSupply(denom: string): Promise { + try { + const data = await this.get( + `/regen/ecocredit/v1/batches/${denom}/supply` + ); + return (data.supply || null) as BatchSupply | null; + } catch (err) { + console.error(`LedgerClient.getBatchSupply(${denom}) failed:`, err); + return null; + } + } + + // ── Marketplace: Sell Orders ─────────────────────────────── + + async getSellOrders(limit = 200): Promise { + try { + const params = new URLSearchParams(); + params.set("pagination.limit", String(limit)); + + const data = await this.get( + `/regen/ecocredit/marketplace/v1/sell-orders?${params.toString()}` + ); + return (data.sell_orders || []) as SellOrder[]; + } catch (err) { + console.error(`LedgerClient.getSellOrders(limit=${limit}) failed:`, err); + return []; + } + } + + async getSellOrdersByBatch(denom: string): Promise { + try { + const params = new URLSearchParams(); + params.set("pagination.limit", "200"); + + const data = await this.get( + `/regen/ecocredit/marketplace/v1/sell-orders/batch/${denom}?${params.toString()}` + ); + return (data.sell_orders || []) as SellOrder[]; + } catch (err) { + console.error( + `LedgerClient.getSellOrdersByBatch(${denom}) failed:`, + err + ); + return []; + } + } + + async getSellOrder(orderId: string): Promise { + try { + const data = await this.get( + `/regen/ecocredit/marketplace/v1/sell-orders/${orderId}` + ); + return (data.sell_order || null) as SellOrder | null; + } catch (err) { + console.error(`LedgerClient.getSellOrder(${orderId}) failed:`, err); + return null; + } + } + + // ── 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 { + // 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; + } + + /** + * 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[]`. + // 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>; + 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) { + 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; + + // 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"); + 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 }> { + 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..e52d6a0 --- /dev/null +++ b/agent-003-market-monitor/src/store.ts @@ -0,0 +1,309 @@ +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; + } + + /** 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: { + classId: string; + windowHours: number; + totalQuantity: number; + totalValueUsd: number; + retirementCount: number; + demandIndex: number; + summary: string; + }): void { + this.db + .prepare( + `INSERT INTO retirement_summaries + (class_id, window_hours, total_quantity, total_value_usd, + retirement_count, demand_index, summary, captured_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)` + ) + .run( + row.classId, + row.windowHours, + row.totalQuantity, + row.totalValueUsd, + row.retirementCount, + row.demandIndex, + row.summary, + new Date().toISOString() + ); + } + + getBaselineDemand(classId: string, lookbackDays: number): number { + // Average of the demand_index values captured in the prior window. + // Used as the reference point for the new demand_index computation. + const cutoff = new Date(Date.now() - lookbackDays * 86_400_000).toISOString(); + const row = this.db + .prepare( + `SELECT AVG(demand_index) as avg FROM retirement_summaries + WHERE class_id = ? AND captured_at >= ?` + ) + .get(classId, cutoff) as { avg: number | null }; + return row.avg ?? 0; + } + + // ── Workflow executions ──────────────────────────────────── + + logExecution(exec: { + executionId: string; + workflowId: string; + agentId: string; + status: string; + startedAt: string; + completedAt: string; + result: string; + }): void { + this.db + .prepare( + `INSERT INTO workflow_executions + (execution_id, workflow_id, agent_id, status, started_at, completed_at, result) + VALUES (?, ?, ?, ?, ?, ?, ?)` + ) + .run( + exec.executionId, + exec.workflowId, + exec.agentId, + exec.status, + exec.startedAt, + exec.completedAt, + exec.result + ); + } + + getExecutionCount(): number { + const row = this.db + .prepare("SELECT COUNT(*) as cnt FROM workflow_executions") + .get() as { cnt: number }; + return row.cnt; + } + + close(): void { + this.db.close(); + } +} + +export const store = new Store(); diff --git a/agent-003-market-monitor/src/types.ts b/agent-003-market-monitor/src/types.ts new file mode 100644 index 0000000..317ca5f --- /dev/null +++ b/agent-003-market-monitor/src/types.ts @@ -0,0 +1,158 @@ +// ============================================================ +// Regen Ledger ecocredit marketplace types +// ============================================================ + +export interface CreditClass { + id: string; + admin: string; + credit_type: CreditType; + metadata: string; +} + +export interface CreditType { + abbreviation: string; + name: string; + unit: string; + precision: number; +} + +export interface CreditBatch { + denom: string; + project_id: string; + issuer: string; + start_date: string; + end_date: string; + total_amount: string; + metadata: string; + open: boolean; +} + +export interface BatchSupply { + batch_denom: string; + tradable_amount: string; + retired_amount: string; + cancelled_amount: string; +} + +export interface SellOrder { + id: string; + seller: string; + batch_denom: string; + quantity: string; + ask_denom: string; + ask_amount: string; + disable_auto_retire: boolean; + expiration: string; +} + +/** Synthesized trade record (from filled sell orders or tx logs) */ +export interface Trade { + id: string; + batchDenom: string; + classId: string; + seller: string; + buyer: string; + quantity: number; + pricePerCredit: number; // USD-equivalent + askDenom: string; + executedAt: string; +} + +/** Synthesized retirement record */ +export interface Retirement { + txHash: string; + batchDenom: string; + classId: string; + retiree: string; + quantity: number; + jurisdiction: string | null; + reason: string | null; + retiredAt: string; +} + +// ============================================================ +// OODA loop types (shared shape mirrors agent-002) +// ============================================================ + +export interface OODAExecution { + executionId: string; + workflowId: string; + status: "running" | "completed" | "failed" | "escalated"; + observations: TObserve; + orientation: TOrient | null; + decision: TDecide | null; + actions: TAct | null; + startedAt: Date; + completedAt: Date | null; + error: string | null; +} + +// ============================================================ +// Workflow-specific types +// ============================================================ + +export type AlertLevel = "NORMAL" | "HIGH" | "CRITICAL"; +export type AnomalySeverity = "INFO" | "WARNING" | "CRITICAL"; + +/** Price anomaly detection (WF-MM-01) */ +export interface PriceAnomaly { + tradeId: string; + batchDenom: string; + classId: string; + seller: string; + quantity: number; + pricePerCredit: number; + classMedian: number; + batchMedian: number; + zScoreVsClass: number; + zScoreVsBatch: number; + sampleSizeClass: number; + sampleSizeBatch: number; + severity: AnomalySeverity; + detectedAt: string; + confidence: number; +} + +/** Liquidity monitoring (WF-MM-02) */ +export interface LiquiditySnapshot { + classId: string; + totalListedQuantity: number; + totalListedValueUsd: number; + sellOrderCount: number; + lowestAskUsd: number; + highestAskUsd: number; + medianAskUsd: number; + meanAskUsd: number; + depthUsd: number; // sum of top-10 sell orders + healthScore: number; // 0-1 + health: "HEALTHY" | "DEGRADED" | "CRITICAL"; + capturedAt: string; +} + +/** Retirement tracking (WF-MM-03) */ +export interface RetirementSummary { + classId: string; + windowHours: number; + retirementCount: number; + totalQuantity: number; + totalValueUsd: number; + uniqueRetirees: number; + topRetiree: string | null; + topRetireeQuantity: number; + pctWithJurisdiction: number; + demandIndex: number; // 0-100, relative to trailing baseline + capturedAt: string; +} + +// ============================================================ +// Output types +// ============================================================ + +export interface OutputMessage { + workflow: string; + subjectId: string; // trade id / class id / batch denom + title: string; + content: string; + alertLevel: AlertLevel; + timestamp: Date; +} diff --git a/agent-003-market-monitor/src/utils.ts b/agent-003-market-monitor/src/utils.ts new file mode 100644 index 0000000..eb53ab3 --- /dev/null +++ b/agent-003-market-monitor/src/utils.ts @@ -0,0 +1,78 @@ +/** + * Shared numeric and parsing helpers used by more than one workflow. + * + * These started out duplicated across the three WF-MM workflows. + * Centralizing them here is the DRY fix asked for by the PR #99 + * Gemini review, and it gives the unit-test suite a single source of + * truth to pin so a regression in one workflow cannot silently diverge + * from the rest. + */ + +/** + * Deterministic statistical median. Returns 0 for empty input so + * callers do not need a special-case branch upstream — an empty + * sample set unambiguously means "no signal" in every callsite. + */ +export function median(values: number[]): number { + if (values.length === 0) return 0; + const sorted = [...values].sort((a, b) => a - b); + const mid = Math.floor(sorted.length / 2); + return sorted.length % 2 === 0 + ? (sorted[mid - 1]! + sorted[mid]!) / 2 + : sorted[mid]!; +} + +/** + * Sample standard deviation (n-1 denominator). Returns 0 for + * sample sizes below 2 because a single observation carries no + * dispersion information. + */ +export function stddev(values: number[], mean: number): number { + if (values.length <= 1) return 0; + const sumSq = values.reduce((acc, v) => acc + (v - mean) ** 2, 0); + return Math.sqrt(sumSq / (values.length - 1)); +} + +/** + * Extract the credit class identifier from a Regen batch denom. + * + * Regen batch denoms follow the shape + * `----` + * (for example, `C01-001-20240101-20241231-001`). The class id is + * always the leading token before the first dash. Strings without a + * dash are returned unchanged. + */ +export function classIdFromBatchDenom(denom: string): string { + const idx = denom.indexOf("-"); + return idx > 0 ? denom.slice(0, idx) : denom; +} + +/** + * Whether a Cosmos denom corresponds to a USD-pegged stablecoin that + * this MVP is willing to treat as 1:1 USD for pricing. Used to + * filter out non-USD sell orders so they do not pollute the baseline + * with arbitrary 1:1-valued noise. + */ +export function isUsdStableDenom(denom: string): boolean { + const d = denom.toLowerCase(); + return d.includes("usdc") || d.includes("usdt") || d.includes("dai"); +} + +/** + * Demand index on a bounded 0-100 scale. Inputs are rolling and a + * class with no trailing activity gets 0. The index is intentionally + * simple — it exists so the narrative layer has a single number to + * anchor the "demand up / demand down" story. Defined here so the + * unit-test suite can import it without pulling in the workflow's + * Store singleton (which opens SQLite on load). + */ +export function computeDemandIndex( + totalQuantity: number, + retirementCount: number, + uniqueRetirees: number +): number { + const volumeComponent = Math.min(60, Math.log10(Math.max(1, totalQuantity)) * 20); + const countComponent = Math.min(20, retirementCount * 2); + const breadthComponent = Math.min(20, uniqueRetirees * 4); + return Math.round(volumeComponent + countComponent + breadthComponent); +} diff --git a/agent-003-market-monitor/src/workflows/liquidity-monitor.test.ts b/agent-003-market-monitor/src/workflows/liquidity-monitor.test.ts new file mode 100644 index 0000000..2e8e705 --- /dev/null +++ b/agent-003-market-monitor/src/workflows/liquidity-monitor.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect } from "vitest"; +import { askUsd, scoreHealth } from "./liquidity-monitor.js"; +import type { SellOrder } from "../types.js"; +import { config } from "../config.js"; + +// Small helper to build a well-typed SellOrder for a test case. +function makeOrder(overrides: Partial = {}): SellOrder { + return { + id: "order-test", + seller: "regen1seller", + batch_denom: "C-001-20240101-20241231-001", + quantity: "100", + ask_denom: "uusdc", + ask_amount: "5000", + disable_auto_retire: false, + expiration: "2030-01-01T00:00:00Z", + ...overrides, + }; +} + +// ============================================================ +// askUsd — per-unit ask price in USD +// ============================================================ + +describe("askUsd", () => { + it("divides ask_amount by quantity for a normal order", () => { + const order = makeOrder({ ask_amount: "5000", quantity: "100" }); + expect(askUsd(order)).toBe(50); + }); + + it("returns 0 when quantity is zero", () => { + expect(askUsd(makeOrder({ quantity: "0" }))).toBe(0); + }); + + it("returns 0 when quantity is negative", () => { + expect(askUsd(makeOrder({ quantity: "-10" }))).toBe(0); + }); + + it("returns 0 when ask_amount is not a finite number", () => { + expect(askUsd(makeOrder({ ask_amount: "not-a-number" }))).toBe(0); + }); + + it("returns 0 when quantity is not a finite number", () => { + expect(askUsd(makeOrder({ quantity: "NaN" }))).toBe(0); + }); + + it("handles fractional prices correctly", () => { + const order = makeOrder({ ask_amount: "125", quantity: "50" }); + expect(askUsd(order)).toBe(2.5); + }); +}); + +// ============================================================ +// scoreHealth — liquidity health tier classifier +// ============================================================ + +describe("scoreHealth", () => { + const floor = config.market.liquidityDepthFloor; // 5000 in the default config + + it("returns CRITICAL when depth is below half the floor", () => { + // depthUsd = 2000, floor = 5000 → 2000 < 2500 → CRITICAL + const result = scoreHealth(floor * 0.4, 5); + expect(result.tier).toBe("CRITICAL"); + }); + + it("returns DEGRADED when depth is below the floor but above half", () => { + // depthUsd = 3000 (floor 5000 → half 2500, so above half), + // 10 orders so countScore = 0.5, depthRatio = 3000/10000 = 0.3, + // score = 0.3*0.7 + 0.5*0.3 = 0.21 + 0.15 = 0.36 — not CRITICAL + // (0.36 >= 0.3), not HEALTHY (3000 < 5000), so DEGRADED. + const result = scoreHealth(floor * 0.6, 10); + expect(result.tier).toBe("DEGRADED"); + }); + + it("returns HEALTHY when depth clears the floor and order count is strong", () => { + // depthUsd = 15000, 20 orders → depthRatio clamped to 1.0, countScore = 1.0, score = 1.0 + const result = scoreHealth(floor * 3, 20); + expect(result.tier).toBe("HEALTHY"); + expect(result.score).toBeCloseTo(1.0, 10); + }); + + it("returns DEGRADED when depth is above the floor but score drops below 0.6", () => { + // depth = exactly floor, sellOrderCount = 2 → depthRatio = 0.5, countScore = 0.1, score = 0.38 + // 0.38 < 0.6 → DEGRADED + const result = scoreHealth(floor, 2); + expect(result.tier).toBe("DEGRADED"); + expect(result.score).toBeLessThan(0.6); + }); + + it("caps the depth score at 1.0 even for very high depth", () => { + const result = scoreHealth(floor * 100, 50); + expect(result.score).toBeLessThanOrEqual(1.0); + }); + + it("caps the count score at 20 orders", () => { + const low = scoreHealth(floor * 2, 20); + const high = scoreHealth(floor * 2, 200); + expect(low.score).toBeCloseTo(high.score, 10); + }); + + it("scores zero depth and zero orders at 0.0", () => { + const result = scoreHealth(0, 0); + expect(result.score).toBe(0); + expect(result.tier).toBe("CRITICAL"); + }); +}); diff --git a/agent-003-market-monitor/src/workflows/liquidity-monitor.ts b/agent-003-market-monitor/src/workflows/liquidity-monitor.ts new file mode 100644 index 0000000..0eb680e --- /dev/null +++ b/agent-003-market-monitor/src/workflows/liquidity-monitor.ts @@ -0,0 +1,231 @@ +import { LedgerClient } from "../ledger.js"; +import { store } from "../store.js"; +import { config } from "../config.js"; +import { describeLiquidity } from "../monitor.js"; +import { output } from "../output.js"; +import type { OODAWorkflow } from "../ooda.js"; +import type { + LiquiditySnapshot, + SellOrder, + CreditClass, + AlertLevel, +} from "../types.js"; +import { median, classIdFromBatchDenom } from "../utils.js"; + +/** + * WF-MM-02: Liquidity Monitoring & Reporting + * + * Trigger: periodic (hourly) OR significant_trade (volume > $10k) + * Layer: 1 (Fully Automated) + * + * OODA: + * Observe — Fetch all open sell orders, group by credit class + * Orient — Compute liquidity metrics per class (listed value, + * bid-ask spread proxy, top-10 depth, health score) + * Decide — Generate per-class liquidity reports. Flag classes + * whose depth falls below the configured floor. + * Act — Persist snapshot, output report, alert on degradation. + */ + +interface Observations { + classes: CreditClass[]; + ordersByClass: Map; +} + +interface Orientation { + snapshots: LiquiditySnapshot[]; +} + +interface Decision { + reports: { + snapshot: LiquiditySnapshot; + previous: LiquiditySnapshot | null; + report: string; + alertLevel: AlertLevel; + }[]; +} + +interface Actions { + saved: number; + alertsSent: number; +} + +export function askUsd(order: SellOrder): number { + const ask = Number(order.ask_amount); + const qty = Number(order.quantity); + if (!Number.isFinite(ask) || !Number.isFinite(qty) || qty <= 0) return 0; + // MVP: treat ask as already-USD; price oracle integration is future work. + return ask / qty; +} + +export function scoreHealth( + depthUsd: number, + sellOrderCount: number +): { score: number; tier: LiquiditySnapshot["health"] } { + const depthRatio = Math.min(1, depthUsd / (config.market.liquidityDepthFloor * 2)); + const countScore = Math.min(1, sellOrderCount / 20); + const score = depthRatio * 0.7 + countScore * 0.3; + + let tier: LiquiditySnapshot["health"]; + if (depthUsd < config.market.liquidityDepthFloor * 0.5 || score < 0.3) { + tier = "CRITICAL"; + } else if (depthUsd < config.market.liquidityDepthFloor || score < 0.6) { + tier = "DEGRADED"; + } else { + tier = "HEALTHY"; + } + + return { score, tier }; +} + +export function createLiquidityMonitorWorkflow( + ledger: LedgerClient +): OODAWorkflow { + return { + id: "WF-MM-02", + name: "Liquidity Monitoring & Reporting", + + async observe(): Promise { + const [classes, sellOrders] = await Promise.all([ + ledger.getCreditClasses(), + ledger.getSellOrders(500), + ]); + + const ordersByClass = new Map(); + for (const order of sellOrders) { + const classId = classIdFromBatchDenom(order.batch_denom); + const bucket = ordersByClass.get(classId) || []; + bucket.push(order); + ordersByClass.set(classId, bucket); + } + + return { classes, ordersByClass }; + }, + + async orient(obs: Observations): Promise { + const snapshots: LiquiditySnapshot[] = []; + const capturedAt = new Date().toISOString(); + + // Snapshot every class that currently has any open sell orders. + // Classes with zero open orders are intentionally skipped — + // that's a "no market" state, distinct from a degraded market. + for (const [classId, orders] of obs.ordersByClass) { + if (orders.length === 0) continue; + + const prices = orders + .map((o) => askUsd(o)) + .filter((p) => p > 0) + .sort((a, b) => a - b); + if (prices.length === 0) continue; + + const quantities = orders + .map((o) => Number(o.quantity)) + .filter((q) => Number.isFinite(q) && q > 0); + const totalListedQuantity = quantities.reduce((a, b) => a + b, 0); + + const listedValues = orders.map((o) => { + const p = askUsd(o); + const q = Number(o.quantity); + return Number.isFinite(p) && Number.isFinite(q) ? p * q : 0; + }); + const totalListedValueUsd = listedValues.reduce((a, b) => a + b, 0); + + // Pre-compute askUsd once per order so the comparator is a plain + // number subtraction instead of recomputing the ratio on every + // sort invocation (sort can call the comparator O(n log n) times). + const pricedOrders = orders + .map((order) => ({ order, price: askUsd(order) })) + .sort((a, b) => a.price - b.price); + const top10 = pricedOrders.slice(0, 10); + const depthUsd = top10.reduce((acc, { order, price }) => { + const q = Number(order.quantity); + return acc + (Number.isFinite(price) && Number.isFinite(q) ? price * q : 0); + }, 0); + + const lowestAskUsd = prices[0]!; + const highestAskUsd = prices[prices.length - 1]!; + const medianAskUsd = median(prices); + const meanAskUsd = prices.reduce((a, b) => a + b, 0) / prices.length; + + const { score, tier } = scoreHealth(depthUsd, orders.length); + + snapshots.push({ + classId, + totalListedQuantity, + totalListedValueUsd, + sellOrderCount: orders.length, + lowestAskUsd, + highestAskUsd, + medianAskUsd, + meanAskUsd, + depthUsd, + healthScore: score, + health: tier, + capturedAt, + }); + } + + return { snapshots }; + }, + + async decide(orientation: Orientation): Promise { + const reports: Decision["reports"] = []; + + for (const snapshot of orientation.snapshots) { + const prevRow = store.getLatestLiquiditySnapshot(snapshot.classId); + let previous: LiquiditySnapshot | null = null; + if (prevRow) { + try { + previous = JSON.parse(prevRow.snapshot) as LiquiditySnapshot; + } catch { + previous = null; + } + } + + const report = await describeLiquidity(snapshot, previous); + + const alertLevel: AlertLevel = + snapshot.health === "CRITICAL" + ? "CRITICAL" + : snapshot.health === "DEGRADED" + ? "HIGH" + : "NORMAL"; + + reports.push({ snapshot, previous, report, alertLevel }); + } + + return { reports }; + }, + + async act(decision: Decision): Promise { + let saved = 0; + let alertsSent = 0; + + for (const { snapshot, report, alertLevel } of decision.reports) { + store.saveLiquiditySnapshot( + snapshot.classId, + JSON.stringify(snapshot), + snapshot.health + ); + saved++; + + await output({ + workflow: "WF-MM-02", + subjectId: snapshot.classId, + title: `Liquidity ${snapshot.health} (score ${snapshot.healthScore.toFixed(2)})`, + content: report, + alertLevel, + timestamp: new Date(), + }); + + if (alertLevel !== "NORMAL") alertsSent++; + } + + if (decision.reports.length === 0) { + console.log(" No credit classes with open sell orders."); + } + + return { saved, alertsSent }; + }, + }; +} diff --git a/agent-003-market-monitor/src/workflows/price-anomaly-detection.test.ts b/agent-003-market-monitor/src/workflows/price-anomaly-detection.test.ts new file mode 100644 index 0000000..36a8435 --- /dev/null +++ b/agent-003-market-monitor/src/workflows/price-anomaly-detection.test.ts @@ -0,0 +1,187 @@ +import { describe, it, expect } from "vitest"; +import { + median, + stddev, + classifyAnomaly, + classIdFromBatchDenom, + isUsdStableDenom, +} from "./price-anomaly-detection.js"; + +// ============================================================ +// median — deterministic statistical median +// ============================================================ + +describe("median", () => { + it("returns 0 for an empty list", () => { + expect(median([])).toBe(0); + }); + + it("returns the single value for a one-element list", () => { + expect(median([42])).toBe(42); + }); + + it("returns the middle value for an odd-length list", () => { + expect(median([1, 5, 3])).toBe(3); + expect(median([7, 2, 9, 4, 5])).toBe(5); + }); + + it("returns the mean of the two middle values for an even-length list", () => { + expect(median([1, 2, 3, 4])).toBe(2.5); + expect(median([10, 20])).toBe(15); + }); + + it("does not mutate the input array", () => { + const input = [3, 1, 2]; + median(input); + expect(input).toEqual([3, 1, 2]); + }); + + it("handles negative numbers correctly", () => { + expect(median([-5, -1, -3])).toBe(-3); + expect(median([-4, -2, 2, 4])).toBe(0); + }); + + it("handles floating point values", () => { + expect(median([0.1, 0.2, 0.3])).toBeCloseTo(0.2, 10); + expect(median([1.5, 2.5, 3.5, 4.5])).toBe(3); + }); +}); + +// ============================================================ +// stddev — sample standard deviation +// ============================================================ + +describe("stddev", () => { + it("returns 0 for an empty list", () => { + expect(stddev([], 0)).toBe(0); + }); + + it("returns 0 for a single value (no variance possible)", () => { + expect(stddev([42], 42)).toBe(0); + }); + + it("computes sample stddev for a two-element list", () => { + // [1, 3], mean 2, sum of sq = 1 + 1 = 2, divide by n-1 = 2, sqrt = sqrt(2) + expect(stddev([1, 3], 2)).toBeCloseTo(Math.SQRT2, 10); + }); + + it("computes sample stddev for a known five-element list", () => { + // [2, 4, 4, 4, 5, 5, 7, 9] has a textbook stddev of 2 (sample) + // but let's use a cleaner one: [1, 2, 3, 4, 5] mean 3 + // sum of sq dev = 4 + 1 + 0 + 1 + 4 = 10, / 4 = 2.5, sqrt ≈ 1.5811 + const values = [1, 2, 3, 4, 5]; + const mean = values.reduce((a, b) => a + b, 0) / values.length; + expect(stddev(values, mean)).toBeCloseTo(Math.sqrt(2.5), 10); + }); + + it("uses n-1 denominator (sample, not population)", () => { + // Population stddev of [1, 2] with mean 1.5 would be 0.5 + // Sample stddev is sqrt(0.5/1) ≈ 0.7071 + expect(stddev([1, 2], 1.5)).toBeCloseTo(Math.SQRT1_2, 10); + }); +}); + +// ============================================================ +// classifyAnomaly — severity classifier +// ============================================================ + +describe("classifyAnomaly", () => { + it("returns INFO for zero z-scores", () => { + expect(classifyAnomaly(0, 0)).toBe("INFO"); + }); + + it("returns INFO for z-scores below the WARNING threshold (2.0)", () => { + expect(classifyAnomaly(1.9, 1.5)).toBe("INFO"); + expect(classifyAnomaly(-1.9, 1.5)).toBe("INFO"); + }); + + it("returns WARNING at the exact WARNING threshold", () => { + expect(classifyAnomaly(2.0, 0)).toBe("WARNING"); + expect(classifyAnomaly(0, 2.0)).toBe("WARNING"); + }); + + it("returns WARNING between 2.0 and CRITICAL", () => { + expect(classifyAnomaly(2.5, 1.0)).toBe("WARNING"); + expect(classifyAnomaly(3.4, 0)).toBe("WARNING"); + }); + + it("returns CRITICAL at the exact CRITICAL threshold (3.5)", () => { + expect(classifyAnomaly(3.5, 0)).toBe("CRITICAL"); + expect(classifyAnomaly(0, 3.5)).toBe("CRITICAL"); + }); + + it("returns CRITICAL above the CRITICAL threshold", () => { + expect(classifyAnomaly(5, 1)).toBe("CRITICAL"); + expect(classifyAnomaly(10, 0)).toBe("CRITICAL"); + }); + + it("picks the larger absolute z-score from the two inputs", () => { + expect(classifyAnomaly(1.5, 4.0)).toBe("CRITICAL"); + expect(classifyAnomaly(2.8, 1.0)).toBe("WARNING"); + }); + + it("treats negative z-scores symmetrically with positive", () => { + expect(classifyAnomaly(-3.6, 0)).toBe("CRITICAL"); + expect(classifyAnomaly(-2.1, 0)).toBe("WARNING"); + }); +}); + +// ============================================================ +// classIdFromBatchDenom — class ID extractor +// ============================================================ + +describe("classIdFromBatchDenom", () => { + it("extracts the class ID from a canonical Regen batch denom", () => { + expect(classIdFromBatchDenom("C01-001-20240101-20241231-001")).toBe("C01"); + }); + + it("handles single-letter class codes", () => { + expect(classIdFromBatchDenom("C-001-20240101-20241231-001")).toBe("C"); + expect(classIdFromBatchDenom("BT-001-20240101-20241231-001")).toBe("BT"); + }); + + it("returns the whole string when no dash is present", () => { + expect(classIdFromBatchDenom("UNKNOWN")).toBe("UNKNOWN"); + }); + + it("handles an empty string", () => { + expect(classIdFromBatchDenom("")).toBe(""); + }); + + it("returns the whole string when the first character is a dash", () => { + // Degenerate but deterministic — the slice is empty before the dash. + expect(classIdFromBatchDenom("-leading-dash")).toBe("-leading-dash"); + }); +}); + +// ============================================================ +// isUsdStableDenom — stablecoin denom check +// ============================================================ + +describe("isUsdStableDenom", () => { + it("recognizes USDC denoms case-insensitively", () => { + expect(isUsdStableDenom("usdc")).toBe(true); + expect(isUsdStableDenom("USDC")).toBe(true); + expect(isUsdStableDenom("ibc/USDC-regen")).toBe(true); + }); + + it("recognizes USDT", () => { + expect(isUsdStableDenom("usdt")).toBe(true); + expect(isUsdStableDenom("ibc/USDT-axelar")).toBe(true); + }); + + it("recognizes DAI", () => { + expect(isUsdStableDenom("dai")).toBe(true); + expect(isUsdStableDenom("ibc/DAI-cosmos")).toBe(true); + }); + + it("rejects REGEN and other non-stable denoms", () => { + expect(isUsdStableDenom("uregen")).toBe(false); + expect(isUsdStableDenom("uatom")).toBe(false); + expect(isUsdStableDenom("eco-credits")).toBe(false); + }); + + it("rejects the empty string", () => { + expect(isUsdStableDenom("")).toBe(false); + }); +}); diff --git a/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts b/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts new file mode 100644 index 0000000..2bf5430 --- /dev/null +++ b/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts @@ -0,0 +1,255 @@ +import { LedgerClient } from "../ledger.js"; +import { store } from "../store.js"; +import { config } from "../config.js"; +import { describePriceAnomaly } from "../monitor.js"; +import { output } from "../output.js"; +import type { OODAWorkflow } from "../ooda.js"; +import type { + Trade, + PriceAnomaly, + AnomalySeverity, + SellOrder, +} from "../types.js"; +import { + median, + stddev, + classIdFromBatchDenom, + isUsdStableDenom, +} from "../utils.js"; + +// Re-exported so the existing unit-test suite can import helpers from +// this workflow file (the tests pre-date the utils.ts extraction). +export { median, stddev, classIdFromBatchDenom, isUsdStableDenom }; + +/** + * WF-MM-01: Price Anomaly Detection + * + * Trigger: SellOrderCreated OR SellOrderFilled + * Layer: 1 (alerts) / Layer 2 (if action needed) + * SLA: Alert within one polling cycle of the trade landing on-chain + * + * OODA: + * Observe — Fetch current sell orders + record as synthetic trade + * observations. When Ledger MCP provides real fill data, + * the observe phase swaps in real trades. + * Orient — For each new observation, compute z-score vs class + * median and batch median using the trailing window. + * Decide — Classify severity (INFO / WARNING / CRITICAL) per the + * thresholds in config.market. Only surface WARNING or + * CRITICAL. Dedupe against previous alerts for this trade. + * Act — Persist, generate narrative via Claude, output to channels. + * + * Determinism: all numeric decisions (median, z-score, severity) are + * computed locally. Claude is only used for the narrative report. + */ + +interface Observations { + newTrades: Trade[]; +} + +interface Orientation { + anomalies: PriceAnomaly[]; +} + +interface Decision { + alertsToPublish: PriceAnomaly[]; +} + +interface Actions { + alerted: number; + skipped: number; +} + +/** + * Convert a sell order into a synthetic "trade observation" for this + * agent. Until Ledger MCP provides filled-trade events, the agent uses + * the ask price as a proxy signal — an unusually high/low ask price + * still indicates a market structure anomaly worth surfacing. + */ +function sellOrderToTrade(order: SellOrder, classId: string): Trade | null { + const qty = Number(order.quantity); + const ask = Number(order.ask_amount); + if (!Number.isFinite(qty) || !Number.isFinite(ask) || qty <= 0) return null; + + // Only USD stablecoin asks are priced — anything else would require + // an oracle we don't have yet, and letting non-USD orders into the + // baseline would pollute the class/batch medians with 1:1-priced + // garbage. Downstream code can plug in a real price oracle. + if (!isUsdStableDenom(order.ask_denom)) return null; + const pricePerCredit = ask / qty; + + return { + id: order.id, + batchDenom: order.batch_denom, + classId, + seller: order.seller, + buyer: "", + quantity: qty, + pricePerCredit, + askDenom: order.ask_denom, + executedAt: new Date().toISOString(), + }; +} + +export function classifyAnomaly( + zClass: number, + zBatch: number +): AnomalySeverity { + const maxZ = Math.max(Math.abs(zClass), Math.abs(zBatch)); + if (maxZ >= config.market.criticalZScore) return "CRITICAL"; + if (maxZ >= config.market.warningZScore) return "WARNING"; + return "INFO"; +} + +export function createPriceAnomalyDetectionWorkflow( + ledger: LedgerClient +): OODAWorkflow { + return { + id: "WF-MM-01", + name: "Price Anomaly Detection", + + async observe(): Promise { + const sellOrders = await ledger.getSellOrders(200); + + const newTrades: Trade[] = []; + for (const order of sellOrders) { + const classId = classIdFromBatchDenom(order.batch_denom); + const trade = sellOrderToTrade(order, classId); + if (trade) { + newTrades.push(trade); + store.recordTrade({ + tradeId: trade.id, + classId: trade.classId, + batchDenom: trade.batchDenom, + pricePerCredit: trade.pricePerCredit, + quantity: trade.quantity, + executedAt: trade.executedAt, + }); + } + } + + return { newTrades }; + }, + + async orient(obs: Observations): Promise { + const anomalies: PriceAnomaly[] = []; + + for (const trade of obs.newTrades) { + const classSamples = store.getTradesByClass( + trade.classId, + config.market.classMedianWindowDays + ); + const batchSamples = store.getTradesByBatch( + trade.batchDenom, + config.market.batchMedianWindowDays + ); + + if (classSamples.length < config.market.minSamples) continue; + + // 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 - classMean) / classStd : 0; + + const batchMedian = batchSamples.length > 0 ? median(batchSamples) : classMedian; + const batchMean = + batchSamples.length > 0 + ? batchSamples.reduce((a, b) => a + b, 0) / batchSamples.length + : classMean; + const batchStd = batchSamples.length > 1 ? stddev(batchSamples, batchMean) : classStd; + const zBatch = batchStd > 0 ? (trade.pricePerCredit - batchMean) / batchStd : 0; + + const severity = classifyAnomaly(zClass, zBatch); + if (severity === "INFO") continue; + + // Confidence rises with sample size and drops when class and + // batch z-scores disagree (which often indicates the batch is + // just thinly traded rather than a real anomaly). + const sampleConfidence = Math.min(1, classSamples.length / 30); + const agreement = + Math.sign(zClass) === Math.sign(zBatch) && Math.abs(zBatch) > 0 + ? 1 + : 0.6; + const confidence = Math.max(0, Math.min(1, sampleConfidence * agreement)); + + anomalies.push({ + tradeId: trade.id, + batchDenom: trade.batchDenom, + classId: trade.classId, + seller: trade.seller, + quantity: trade.quantity, + pricePerCredit: trade.pricePerCredit, + classMedian, + batchMedian, + zScoreVsClass: zClass, + zScoreVsBatch: zBatch, + sampleSizeClass: classSamples.length, + sampleSizeBatch: batchSamples.length, + severity, + detectedAt: new Date().toISOString(), + confidence, + }); + } + + return { anomalies }; + }, + + async decide(orientation: Orientation): Promise { + const alertsToPublish: PriceAnomaly[] = []; + + for (const anomaly of orientation.anomalies) { + const existing = store.getAnomalyAlertSeverity(anomaly.tradeId); + // Dedupe: don't re-alert at the same or lower severity. + if (existing === "CRITICAL") continue; + if (existing === "WARNING" && anomaly.severity !== "CRITICAL") continue; + + alertsToPublish.push(anomaly); + } + + return { alertsToPublish }; + }, + + async act(decision: Decision): Promise { + let alerted = 0; + let skipped = 0; + + for (const anomaly of decision.alertsToPublish) { + const report = await describePriceAnomaly(anomaly); + + store.saveAnomalyAlert({ + tradeId: anomaly.tradeId, + severity: anomaly.severity, + zClass: anomaly.zScoreVsClass, + zBatch: anomaly.zScoreVsBatch, + report, + }); + + await output({ + workflow: "WF-MM-01", + subjectId: anomaly.tradeId, + title: `${anomaly.severity} — ${anomaly.classId} / ${anomaly.batchDenom}`, + content: report, + alertLevel: anomaly.severity === "CRITICAL" ? "CRITICAL" : "HIGH", + timestamp: new Date(), + }); + alerted++; + } + + if (decision.alertsToPublish.length === 0) { + console.log(" No new price anomalies above threshold."); + } + + return { alerted, skipped }; + }, + }; +} diff --git a/agent-003-market-monitor/src/workflows/retirement-tracking.test.ts b/agent-003-market-monitor/src/workflows/retirement-tracking.test.ts new file mode 100644 index 0000000..05e1008 --- /dev/null +++ b/agent-003-market-monitor/src/workflows/retirement-tracking.test.ts @@ -0,0 +1,152 @@ +import { describe, it, expect } from "vitest"; +// 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"; + +// ============================================================ +// computeDemandIndex — bounded 0-100 demand signal +// ============================================================ + +describe("computeDemandIndex", () => { + it("returns 0 for zero-activity inputs", () => { + expect(computeDemandIndex(0, 0, 0)).toBe(0); + }); + + it("caps the volume component at 60", () => { + expect(computeDemandIndex(1e100, 0, 0)).toBe(60); + }); + + it("caps the count component at 20 (10 retirements)", () => { + 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)", () => { + 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", () => { + expect(computeDemandIndex(1e100, 50, 50)).toBe(100); + }); + + it("returns a mid-range value for moderate activity", () => { + expect(computeDemandIndex(10000, 5, 3)).toBe(82); + }); + + it("uses log10 scaling so each order of magnitude adds ~20 pre-cap", () => { + 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)", () => { + 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", () => { + 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 new file mode 100644 index 0000000..4124310 --- /dev/null +++ b/agent-003-market-monitor/src/workflows/retirement-tracking.ts @@ -0,0 +1,243 @@ +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 { Retirement, RetirementSummary } from "../types.js"; +import { classIdFromBatchDenom, computeDemandIndex } from "../utils.js"; + +/** + * WF-MM-03: Retirement Pattern Analysis + * + * Trigger: MsgRetire events (LCD tx-search) + * Layer: 1 (Fully Automated) + * + * OODA: + * 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. + * + * 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 Observations { + retirements: Retirement[]; +} + +interface Orientation { + summariesByClass: Map; +} + +interface Decision { + reports: { + summary: RetirementSummary; + baselineDemand: number; + report: string; + }[]; +} + +interface Actions { + saved: number; + alertsSent: number; +} + +/** + * 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(), + classPriceUsd: Map = new Map() +): 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; + + // 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, + 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 { + return { + id: "WF-MM-03", + name: "Retirement Pattern Analysis", + + async observe(): Promise { + // 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. + // + // 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 }; + }, + + async orient(obs: Observations): Promise { + // 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 }; + }, + + async decide(orientation: Orientation): Promise { + const reports: Decision["reports"] = []; + + for (const summary of orientation.summariesByClass.values()) { + const baselineDemand = store.getBaselineDemand(summary.classId, 7); + const report = await describeRetirementSummary(summary, baselineDemand); + reports.push({ summary, baselineDemand, report }); + } + + return { reports }; + }, + + async act(decision: Decision): Promise { + let saved = 0; + let alertsSent = 0; + + for (const { summary, baselineDemand, report } of decision.reports) { + store.saveRetirementSummary({ + classId: summary.classId, + windowHours: summary.windowHours, + totalQuantity: summary.totalQuantity, + totalValueUsd: summary.totalValueUsd, + retirementCount: summary.retirementCount, + demandIndex: summary.demandIndex, + summary: report, + }); + saved++; + + // Alert if demand index jumped meaningfully above the baseline. + // Threshold of +15 index points mirrors the agent character's + // "moderate-high" boundary (65 → 72 example). + const jumped = baselineDemand > 0 && summary.demandIndex - baselineDemand >= 15; + const alertLevel = jumped ? "HIGH" : "NORMAL"; + if (alertLevel !== "NORMAL") alertsSent++; + + await output({ + workflow: "WF-MM-03", + subjectId: summary.classId, + title: `Retirement summary (demand ${summary.demandIndex}${baselineDemand ? ` vs ${baselineDemand.toFixed(0)}` : ""})`, + content: report, + alertLevel, + timestamp: new Date(), + }); + } + + if (decision.reports.length === 0) { + console.log(" No retirement activity detected in any class."); + } + + return { saved, alertsSent }; + }, + }; +} diff --git a/agent-003-market-monitor/tsconfig.json b/agent-003-market-monitor/tsconfig.json new file mode 100644 index 0000000..e6a11e8 --- /dev/null +++ b/agent-003-market-monitor/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "src/**/*.test.ts", "vitest.config.ts"] +} diff --git a/agent-003-market-monitor/vitest.config.ts b/agent-003-market-monitor/vitest.config.ts new file mode 100644 index 0000000..ce36a74 --- /dev/null +++ b/agent-003-market-monitor/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts"], + environment: "node", + }, +});